inertiajs / inertia

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.
https://inertiajs.com
MIT License
6.44k stars 430 forks source link

DOMException: Failed to execute 'replaceState' on 'History': #<Object> could not be cloned #775

Closed iamohd-zz closed 1 year ago

iamohd-zz commented 3 years ago

Versions:

Describe the problem:

I am using Vue 3 Draggable package. In the event of dragging an element, I execute Inertia.put method and pass some data to Laravel. This is where this exception occurs.

Steps to reproduce:

For example, in Laravel pass a project with a list of columns to your view. Then use the Draggable component as following:

<Draggable
    v-model="project.columns"
    group="columns"
    item-key="id"
    @end="onColumnPositionChanged"
>
    <template #item="{element: column}">
        <KanbanColumn
             :column="column"
             :key="column.id"
         />
    </template>
</Draggable>

In event of onColumnPositionChanged, trigger Inertia.put method

const onCardPositionChanged = () => {
    Inertia.put('/some-route');
};
reinink commented 3 years ago

I suspect this has something to do with the URL you're submitting to, which I assume isn't /some-route. What's the exact URL that's being used when submitting the request?

iamohd-zz commented 3 years ago

I suspect this has something to do with the URL you're submitting to, which I assume isn't /some-route. What's the exact URL that's being used when submitting the request?

Thanks for the quick reply

The URL is: http://127.0.0.1:8000/back/projects/dummy-slug/columns/sort

iamohd-zz commented 3 years ago

I think the issue is because of trying to push a complex object into the page state. Once I replaced the page state with simple data, it worked without any issue.

This StackOverflow's question is related to this issue https://stackoverflow.com/questions/50618225/uncaught-domexception-failed-to-execute-pushstate-on-history-function

Edit:

I tried to serialize the page object state using JSON.stringify here and it fixed the issue.

https://github.com/inertiajs/inertia/blob/master/packages/inertia/src/router.ts#L386

reinink commented 3 years ago

I think the issue is because of trying to push a complex object into the page state. Once I replaced the page state with simple data, it worked without any issue.

So, how are you getting complex data into your page props? We actually intentionally removed the transformProps option because of this very issue. The data that is saved to history state should just be the props (JSON) that come back from the server.

reinink commented 3 years ago

Also, can you please provide an example of what specifically you're doing to cause this? I don't see how the draggable package/component should have any bearing on what ends up in history state.

iamohd-zz commented 3 years ago

At first, I thought it's because of Vue Draggable, but it has nothing to do with this problem.

Actually, when the data that comes from the server contains nested arrays and you change this data in your view, then this issue occurs.

For example, suppose you are passing this to your view

return Inertia::render('back/projects/tasks/index', [
    'project' => [
        'id'  => $project->id,
        'name' => $project->name,
        'columns' => $project->columns()
              ->orderBy('index')
              ->get()
               ->transform(function (Column $column) {
                   return [
                       'id'    => $column->id,
                       'name'  => $column->name,
                       'index' => $column->index,
                       'cards' => [...]
                    ];
               })
         ]
    ]);

Now in your view, if you change the sort of columns and try to call Inertia.put, you will get the exception.

If it's still not producible, I don't mind giving you access to the repo to check how to produce the issue.

reinink commented 3 years ago

Gotcha. So, there is no reason why you shouldn't be able to have a very complex/nested object structure like this. I do this all the time.

Can you identify specially what part of the data is causing the exception? You might have to systematically remove data until the problem goes away to figure this out. I've never run into an issue where server generated data caused an issue like this.

iamohd-zz commented 3 years ago

I tried to do that. It works when you replace 'columns' with an array of simple data types.

For example

'columns' => [1,2,3,4],

But this exception occurs when you put a nested array

For example;

'columns' => [
    ['id'=>1], ['id'=>2]
],

-- In my case, it's 'cards' that causing this issue since it's an array that is inside 'columns'

reinink commented 3 years ago

Are you sure there is not more to it? I just tried passing the following as props in my own demo app, and it worked without issue.

'columns' => [
    ['id'=>1], ['id'=>2]
],

Does this issue only happen when you make a visit via Inertia.put()? Or does it happen on the initial page load? If it's only happening on Inertia.put(), can you confirm that you're redirecting server-side back to the same page? Can you please show me the response that's coming back from that request?

iamohd-zz commented 3 years ago

If you never change this data in your view then you will not get this exception.

Once you change it in your view and you call Inertia.put this will happen. Also, even when you try to navigate away after changing this data.

reinink commented 3 years ago

Are you use the "remember" feature at all?

iamohd-zz commented 3 years ago

No I am not using it

reinink commented 3 years ago

This is a strange issue. From what it sounds like, there is some type of page object data structure that's causing this error. I am using the example data above that you're saying is causing this issue, and I am unable to reproduce this. Basically, to fix this, I need to be able to reproduce it.

I don't really want access to your full app. Much better is a very simple Laravel/Inertia/Vue3 app that reproduces this issue with just one or two endpoints, and one or two components. If you can provide that, I'll happily look deeper into this. 👍

iamohd-zz commented 3 years ago

Great, I will make a repo to reproduce this issue and post the link here

iamohd-zz commented 3 years ago

Here it is: https://github.com/iamohd/inertia-exception

To produce the exception, change the order of the columns by dragging them, and click on "Call Inertia.put" or "To another page" button

Also, you can try clicking on either of the buttons without changing the order of the columns, you will notice it will work fine in this case.

ysv-a commented 3 years ago

same problem @inertiajs/inertia version: 0.9.2 @inertiajs/inertia-vue3 version: 0.4.7

i am using Howler.js plugin and vuex an error occurs when navigating to another page via inertia-link

Uncaught DOMException: Failed to execute 'replaceState' on 'History': HTMLAudioElement object could not be cloned.

no problems in the old version: @inertiajs/inertia version: 0.8.7 @inertiajs/inertia-vue3 version: 0.3.14

tonychuuy commented 3 years ago

I was having the same issue

Uncaught (in promise) DOMException: The object could not be cloned. router.ts:397:4
    replaceState router.ts:398
    saveScrollPositions router.ts:73
    resetScrollPositions router.ts:91
    g router.ts:381
    (Async: promise callback)
    g router.ts:379
    (Async: promise callback)
    setPage router.ts:373
    handleInitialPageVisit router.ts:52
    init router.ts:41
    setup app.js:43
    callWithErrorHandling runtime-core.esm-bundler.js:155
    setupStatefulComponent runtime-core.esm-bundler.js:7161
    setupComponent runtime-core.esm-bundler.js:7117
    mountComponent runtime-core.esm-bundler.js:5115
    processComponent runtime-core.esm-bundler.js:5090
    patch runtime-core.esm-bundler.js:4684
    componentEffect runtime-core.esm-bundler.js:5227
    reactiveEffect reactivity.esm-bundler.js:42
    effect reactivity.esm-bundler.js:17
    setupRenderEffect runtime-core.esm-bundler.js:5173
    mountComponent runtime-core.esm-bundler.js:5132
    processComponent runtime-core.esm-bundler.js:5090
    patch runtime-core.esm-bundler.js:4684
    render2 runtime-core.esm-bundler.js:5810
    mount runtime-core.esm-bundler.js:4085
    mount runtime-dom.esm-bundler.js:1322
    <anonymous> app.js:60
    InnerModuleEvaluation self-hosted:2384
    evaluation self-hosted:2335

What I noticed is that the exception occurs when I mutate the property, having an array that comes from the backend and then passed it to the vue component as property:

 props: {
    accounts: Array,
  },

I had a computed property which used the accounts prop, and applied a filter and a map methods which then the map method mutated the original array.

The issue was gone when I cloned the accounts array using JSON.parse and JSON.stringify

JSON.parse(JSON.stringify(this.accounts))

Also read the MDN for pushState which has a size limit of 640K, but that wasn't my case. https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API#the_pushstate_method

And supported types, in my case it was a simple array of objects, but can be the issue for @oueki https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types

nikuscs commented 3 years ago

Same error happening here, basically we have deep object ( chart ) that comes from "$page.props.charts.orders"

Payload Demo here : https://api.npoint.io/63d8cf8b37aee1247b5a/orders Please keep in mind more data is being shared along.

When passed via Prop to the component like follows:

<chart :chart="$page.props.charts.orders" />

This will reproduce the error from OP, but if we do like @tonychuuy mentioned and have a prop as follows :

computed:{ ordersChart(){ return JSON.parse(JSON.stringify(this.$page.props.charts.orders)) } }

<chart :chart="ordersChart" />

It will work just fine, im not sure what the issue is, but seems like some deep objects are being converted to Proxy and so fails to replace the state on those.

After some time with the debugger i was able to spot the proxied object :

image

Versions

jechazelle commented 3 years ago

Hi,

Have you found a solution @iamohd @reinink ?

I have a same issue when I use vuedraggable in my project.

I have an error when I drag my item : Uncaught DOMException: Failed to execute 'replaceState' on 'History': # could not be cloned.

My code : https://stackblitz.com/edit/vue-shifting-array-elements-47h7cu

iamohd-zz commented 3 years ago

Hi @jechazelle

As a temporary solution you can clone the prop into a new reactive variable and use it

Just make sure u truly clone it, not making a shallow copy.

ewhicher commented 3 years ago

@reinink @iamohd - I am having the same issue, did you find a fix?

@iamohd - can you show how to do the temporary solution in the mean time?

iamohd-zz commented 3 years ago

@reinink @iamohd - I am having the same issue, did you find a fix?

@iamohd - can you show how to do the temporary solution in the mean time?

For example

import { usePage } from '@inertiajs/inertia-vue3';
import { reactive } from 'vue';

const obj = reactive({ ... usePage().props.value.someObject });
kevnk commented 3 years ago

Hey everyone... Could this maybe be related to a local storage max size?

I ran into this issue when I just stored the entire form like this:

// Threw above error when I updated specific fields in my form
remember: ['form'],

I solved it by removing the exceptionally large parts of the form (in my case, entire models with deeply nested props, related models, and image data):

// Works when I remove those specific problematic and large form props
remember: [
    'form.title',
    'form.label',
    // ... other small field types
],

Now it works great!

kevnk commented 3 years ago

Well... I thought it was working...

I think the issue is because of trying to push a complex object into the page state. Once I replaced the page state with simple data, it worked without any issue.

So, how are you getting complex data into your page props? We actually intentionally removed the transformProps option because of this very issue. The data that is saved to history state should just be the props (JSON) that come back from the server.

This may be my issue too... I'm sending different data than I'm receiving...

Numenorian commented 3 years ago

I ran into the same problem and found another possible workaround:

For whatever reason it appears that if I mutate the array part of the prop with something like array.splice() instead of setting that value to a new array with something like array.filter(), it doesn't seem to trip the error.

For example, let's say I have a prop data structure like this:

  someProp: {
    someInt: 3,
    someArray: [
      {
        name: "someObject1",
        id: 1,
      },
      {
        name: "someObject2",
        id:2,
      }
    ],
    someString:  "string",
  },

..and I want to remove "someObject1" based on it's id.

If I use someArray.filter(), like so, I trip the mysterious "Object could not be cloned" error:

let idToRemove = 1;
this.someProp.someArray = this.someProp.someArray.filter( arrayItem => {
  return arrayItem.id !== idToRemove;
});

If, however, I use someArray.findIndex() and someArray.splice(), I don't trip the error.

let idToRemove = 1;
let indexToRemove = this.someProp.someArray.findIndex( arrayItem => {
  return arrayItem.id == idToRemove;
});
this.someProp.someArray.splice(indexToRemove, 1);

I'm not sure what layer of the JavaScript/Inertia/Vuejs stack is causing this odd behavior, but I hope this information helps to work around and/or fix the issue.

jannescb commented 2 years ago

I ran into the same error, also with draggable and Vue3. The error occurred with nested objects and ONLY if they were already present in the initial state. If the object is built after initialization the error does not occur.

My workaround for the problem is to always initialize the form's deep keys "empty" and populate them after mounting:

I have a content key in my form which is a deeply nested object and i always clone the content coming from a prop after mounting:

const form = useForm({
     content: {}
 })

onMounted(() => {
    form.content = JSON.parse(JSON.stringify(props.page.content));
});
dillingham commented 2 years ago

in a computed property.. which adds bindings for v-bind for when looping over dynamic components..

I did what Numenorian / jannescb suggested and the error went away


let filters = JSON.parse(JSON.stringify(this.filters));

return filters.map(() => {})
ewhicher commented 2 years ago

I ended up using Lodash cloneDeep to clone the data which fixed the issue: https://lodash.com/docs/#cloneDeep

coclav commented 2 years ago

I pulled my hair on this one for months. My solution was similar to this https://github.com/inertiajs/inertia/issues/775#issuecomment-876030983 above

Spoiler : the problem was because a "Proxy" made its way to my state.

image

So I had to find out what couldn't be cloned ?!

image

put a breakpoint there... and started to dig into the e object until i saw this

image

I tried to "unProxy" the value with JSON.parse(JSON.stringify(formField)), and this solved the issue.

roguesherlock commented 2 years ago

Yep. I came across this today and turns out I had proxy object in my state! Thanks guys

pintend commented 2 years ago
<Draggable
    v-model="project.columns"
    group="columns"
    item-key="id"
    @end="onColumnPositionChanged"
>
    <template #item="{element: column}">
        <KanbanColumn
             :column="column"
             :key="column.id"
         />
    </template>
</Draggable>

Try changing v-model="project.columns" to :list="project.columns" i was having a similar issue and this seams to have solved it for me

Source: https://github.com/SortableJS/vue.draggable.next#list

dellow commented 2 years ago

Just to chime in that I'm also seeing this issue with Vue Draggable and Inertia.put.

JSON.parse(JSON.stringify(formField)) does seem to solve the issue.

pintend commented 2 years ago

Just to chime in that I'm also seeing this issue with Vue Draggable and Inertia.put.

JSON.parse(JSON.stringify(formField)) does seem to solve the issue.

Are you using v-model or list?

coclav commented 2 years ago

My understanding of the issue is when you modify a value that came as "props", event if it's nested deep down in an object.

dellow commented 2 years ago

Are you using v-model or list?

v-model

pintend commented 2 years ago

Are you using v-model or list?

v-model

Try using :list to see if you dont need to json parse it

And let us know

danielendrodi commented 2 years ago

I had the same error, I solved it by sending the complex JSON object as a string and JSON.parse it on the front end.

jjjrmy commented 2 years ago

I'm getting this same error with my checkbox component.

<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue','input'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

<template>
  <div>
    <template v-for="option in input.options">
        <input type="checkbox" v-model="value" :name="input.id" :value="{ id: option.id, value: option.value }" />
        <label>{{ option.label }}</label>
    </template>
  </div>
</template>

This fixes it, but just seems dirty/unnecessary

-    emit('update:modelValue', value)
+    emit('update:modelValue', JSON.parse(JSON.stringify(value)))
markvds commented 2 years ago

I had the same problem, also with Vuedraggable. And yes, at first it seems a problem with Inertia (and maybe it is in some way), but the thing is that modifying data that came in as a prop, is never a good idea and you should not change a prop's data (also see the Vue documentation).

What I did to solve the problem, is creating a clone of the prop that came in (processes in my case) and then pass that as v-model on the Vuedraggable component.

export default {
    ...
    props: {
        processes: Array,
    },
    data() {
        const processesClone = this.processes.map((item) => {
            return JSON.parse(JSON.stringify(item));
        });
        return {
            processesClone: processesClone,
        }
    },
    watch() {
        processesClone(processes) {
            this.$inertia.post( ... );
        },
    },
    ...
}
<draggable v-model="processesClone" tag="tbody">
    <template #item="{ element, index }">
        <tr>
            ...
        </tr>
    </template>
</draggable>

You could also add a watcher to change the clone whenever the prop changes.

ewhicher commented 2 years ago

I'm just going to pop this on here: https://web.dev/structured-clone/

It digs in to the new structuredClone() api and lists some of the shortcomings of the JSON.parse(JSON.stringify()) approach.

For anyone running into issues it might turn out to be a better option, or you might find you need a library like Lodash.

Hopefully this is helpful to anyone who has scrolled this far with no success!

mauriciolanner commented 2 years ago

the best solucion

ordago commented 2 years ago

@ewhicher I haven't been able to make structuredClone work. I get this error.

"UnhandledPromiseRejectionWarning: DataCloneError: Proxy object could not be cloned."

But JSON.stringify seems to do the trick.

Edit: The error above war from Firefox. I just noticed the error in Chrome is different

Chrome:

Uncaught (in promise) DOMException: Failed to execute 'replaceState' on 'History': [object Array] could not be cloned.

structuredClone error:

DOMException: Failed to execute 'structuredClone' on 'Window': [object Array] could not be cloned.

jelleroorda commented 1 year ago

I had the same issue when using usePage() inside of the state function of Pinia.

Before:

    state: () => {
        return {
            cart: usePage().props.value.cart as Cart,
        }
    },

    actions: {
        setQuantity(product_id: number, quantity: any) {
            // Here an update to the cart happened
            this.cart.products = this.cart.products.map(productInCart => {
                if (productInCart.product_id !== product_id) {
                    return productInCart;
                }

                return {
                    ...productInCart,
                    quantity: quantity,
                }
            })

            // Then this PUT request will error
            Inertia.put('/cart/set', {
                cart_id: this.cart.id,
                product_id,
                quantity,
            }, {
                preserveScroll: true,
                only: ['cart', 'errors'],
            });
        },
    },

I've fixed it by changing the state to this:

    state: () => {
        const cart =  JSON.parse(JSON.stringify(usePage().props.value.cart))

        return {
            cart: cart as Cart,
        }
    },
rizkhal commented 1 year ago

Passing props into ref instead of change the value of it

dev-charles15531 commented 1 year ago
const obj = reactive({ ... usePage().props.value.someObject });

You loose reactivity doing this, and i dont want that.

jonrcable commented 1 year ago

Had the same issue with a complex form using push. I was able to navigate around it using this following. This thread saved me hours of debugging. Thank you!

                    let current = JSON.parse(JSON.stringify(this.list[field].current));
                    current.push(item);

Where this.list[field].current is a deeply nested array from a part of the form.

jjjrmy commented 1 year ago

Just curious, instead of Stringify then Parse, could we use structuredClone instead to solve this?

DavidClaiborne commented 1 year ago

this did the trick for me;

const form = useForm(structuredClone(toRaw(props.promo)))

https://stackoverflow.com/questions/72632173/unable-to-use-structuredclone-on-value-of-ref-variable

frknasir commented 1 year ago

In my case, I tried writing to a prop which is supposed to be readonly. Undoing that solved it.

reinink commented 1 year ago

Hey! Thanks so much for your interest in Inertia.js and for sharing this issue/suggestion.

In an attempt to get on top of the issues and pull requests on this project I am going through all the older issues and PRs and closing them, as there's a decent chance that they have since been resolved or are simply not relevant any longer. My hope is that with a "clean slate" me and the other project maintainers will be able to better keep on top of issues and PRs moving forward.

Of course there's a chance that this issue is still relevant, and if that's the case feel free to simply submit a new issue. The only thing I ask is that you please include a super minimal reproduction of the issue as a Git repo. This makes it much easier for us to reproduce things on our end and ultimately fix it.

Really not trying to be dismissive here, I just need to find a way to get this project back into a state that I am able to maintain it. Hope that makes sense! ❤️