Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeError: Cannot add property tableData, object is not extensible #1979

Closed
da1z opened this issue Jun 2, 2020 · 24 comments
Closed

TypeError: Cannot add property tableData, object is not extensible #1979

da1z opened this issue Jun 2, 2020 · 24 comments
Labels
question Further information is requested waiting for author

Comments

@da1z
Copy link

da1z commented Jun 2, 2020

Hello.

Using this table with redux and react-redux.
and getting error:

TypeError: Cannot add property tableData, object is not extensible
(anonymous function)
node_modules/material-table/dist/utils/data-manager.js:278
  275 | 
  276 | this.selectedCount = 0;
  277 | this.data = data.map(function (row, index) {
> 278 |   row.tableData = (0, _objectSpread2["default"])({}, row.tableData, {
      | ^  279 |     id: index
  280 |   });
  281 | 

My code looks like:

  const orders = useSelector(state =>
    state.orders
  );
...
        <MaterialTable
          title="Orders"
          isLoading={isLoadingOrders}
          data={orders}
        />

Am I missing something. Thanks!

@da1z da1z added the question Further information is requested label Jun 2, 2020
@Domino987
Copy link
Collaborator

Can you reproduce that in a sandbox? Because I am using the same architecture and it works for me. Can you also share a console.log of the orders you are adding? Last but not least, can you verify if orders are frozen?

@da1z
Copy link
Author

da1z commented Jun 2, 2020

@Domino987 Ok I will try in sandbox. What do you mean by frozen orders?

also if i do:

const orders = useSelector(state => state.orders.map(o => ({...o, tableData: {}})) );

it works, but this is workaround, not fix

@Domino987
Copy link
Collaborator

Yes you are creating a new object with that map, what happens if you do not add the empty tabledata object to it?
It is possible you freeze your objects so they cannot be changed: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#:~:text=A%20frozen%20object%20can%20no,existing%20properties%20from%20being%20changed.

@da1z
Copy link
Author

da1z commented Jun 2, 2020

@Domino987
Copy link
Collaborator

Object.isFrozen(tableData[0]) returns true, so redux toolkit in fact freezes your obejcts. That is causing the errors. I am closing the issue, since this is not a material table error. But the mutation of the passed objects will be removed in the future.

@chattling
Copy link
Contributor

But the mutation of the passed objects will be removed in the future.

How soon? I now migrated from React Apollo 2.6 to 3.1 and in new version Apollo locking objects in Apollo cache. So when using these objects with material-table, getting same error. Its preventing to upgrade to Apollo 3.0 and higher.
It is specially annoying, because I'm using editable tree data with material-table. So if I use workaround as mentioned above, after I submit the change and mutate it with Apollo, getting new array from Apollo and original tableData missing, the workaround is recreating it, but all the collapse/expand information missing so the whole table collapses.

@chattling
Copy link
Contributor

For those coming here due to issue I mentioned earlier (incompatibility with @apollo/client). I have found a temporary workaround.
Just to give some background first, Apollo has consolidated all their Apollo related packages into @apollo/client. As part of consolidation InMemoryCache was updated based on this blog post via PR #5073, where they introduced immutability for the cache for performance.

In order to avoid the issue of incompatibility between immutable new Apollo cache and requirement of material-table for data to be mutable, I changed InMemoryCache import to be from apollo-cache-inmemory, which most likely will be deprecated soon. Luckily it still works and can be plugged into new Apollo client.

So I think material-table should still implement a way to work with immutable data, since the workaround I mentioned above is only temporary till old Apollo package will be deprecated.

@DominikEngel
Copy link

Hi thanks for the workaround for Apollo. I do not know the exact timeline but I will talk to the other maintainers to get started as soon as possible.

@iamchathu
Copy link

I'm also using new @apollo/client and having same issue after upgrade.

@chattling
Copy link
Contributor

Worked for me:
https://stackoverflow.com/questions/59648434/material-table-typeerror-cannot-add-property-tabledata-object-is-not-extensibl

It works only in some cases. Also same solution was given above by @da1z.
As I explained above in my case I have tree data and table is editable. So every time I mutate, Apollo returns new array and the expand/collapse data created by material-table is lost, so table collapses all nodes.
I am still waiting for material-table to find resolution for this use case.

@tomasbruckner
Copy link

Same problem with Redux toolkit https://redux-toolkit.js.org/

Is there any plan to stop modifying the input data?

@henhen724
Copy link

Same problem with migrating to @apollo/client but I found a more robust solution. If you simply map through the rows and use the function Object.assign({}, buf) you can convert them to writable objects. This works for me. Example:

const [state, setState] = useState<TableState>({
        columns: [
            { title: 'Topic Name', field: 'topic', type: 'string', editable: 'onAdd' },
            { title: 'Current Size', field: 'currSize', type: 'numeric', editable: 'never' },
            { title: 'Packets Expire (T/F)', field: 'expires', type: 'boolean', editable: 'always' },
            { title: 'Experation Time (ms)', field: 'experationTime', type: 'numeric', editable: 'always' },
            { title: 'Is Memory Limited (T/F)', field: 'sizeLimited', type: 'boolean', editable: 'always' },
            { title: 'Memory Limit (bytes)', field: 'maxSize', type: 'numeric', editable: 'always' },
        ],
        data: [],
})
useQuery<BufferQuery>(BufferQuery, {
        onCompleted: (queryData: BufferQuery) => {
            const buffers = queryData.runningBuffers ? queryData.runningBuffers.map(buf => Object.assign({}, buf)) : [];
            setState({ columns: state.columns, data: buffers });
        }
 });
return(<MaterialTable columns={state.columns} data={state.data}
    //...  I deleted all of my on edit stuff here to make this readable                      
 />)

@henhen724
Copy link

PS: Object.assign can also be used if you use mutations. Just use newRow = Object.assign(oldRow, dataFromMutation)

@chattling
Copy link
Contributor

PS: Object.assign can also be used if you use mutations. Just use newRow = Object.assign(oldRow, dataFromMutation)

While it is possible, it is very complex and will have impact on performance, especially as the table data size grows. When using Apollo's useMutation to update a record, it will update the record in the Apollo cache (as long it's ID didn't change, which is the normal behavior for updates). The moment it happens, useQuery will cause the component to re-render with new set of data. So now need to iterate through updated data and for each record iterate through original data to find record by ID, because I'm not sure if Apollo will always give data in same order in array (which also requires to have original data in component state, while normally not required for Apollo data, since Apollo already provide re-render mechanism for data changes).
Again, this is a workaround, and an ugly one. I still hope material-table maintainers will find more elegant solution for this.

@francescovenica
Copy link
Contributor

francescovenica commented Sep 27, 2020

For those coming here due to issue I mentioned earlier (incompatibility with @apollo/client). I have found a temporary workaround.
Just to give some background first, Apollo has consolidated all their Apollo related packages into @apollo/client. As part of consolidation InMemoryCache was updated based on this blog post via PR #5073, where they introduced immutability for the cache for performance.

In order to avoid the issue of incompatibility between immutable new Apollo cache and requirement of material-table for data to be mutable, I changed InMemoryCache import to be from apollo-cache-inmemory, which most likely will be deprecated soon. Luckily it still works and can be plugged into new Apollo client.

So I think material-table should still implement a way to work with immutable data, since the workaround I mentioned above is only temporary till old Apollo package will be deprecated.

how did you make it work? When I using apollo-cache-inmemory I get this error:

Type 'InMemoryCache' is missing the following properties from type 'ApolloCache<unknown>': identify, gc, modify, getFragmentDoc

Another solution is to JSON.parse(JSON.stringify()) but I think it's even worse then loops through the rows :(

@chattling
Copy link
Contributor

how did you make it work? When I using apollo-cache-inmemory I get this error:

Type 'InMemoryCache' is missing the following properties from type 'ApolloCache<unknown>': identify, gc, modify, getFragmentDoc

like this:

import { ApolloClient } from '@apollo/client'
import { InMemoryCache } from 'apollo-cache-inmemory'

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
})

The link is complex, I create it from split of 3 links - uploadLink, WebsocketLink and authLink.

@chattling
Copy link
Contributor

Has anyone looked at this issue? I would like to use InMemoryCache provided by Apollo instead of custom one I have to use as a workaround (see workaround provided above).

As a solution approach recommendation, please allow us to provide material-table function to mutate data object via new property. This way we can use Apollo facility to mutate object in Apollo cache as described here: #4543

@nimarahbari
Copy link

There won't be a real fix for this unless material-table stops changing the table data that it's fed to. This is all because of this tableData being added to the data. @chattling Thanks for the suggestion. For now I will go with that.

@nimarahbari
Copy link

@chattling I get the same error as @francescovenica.

@chattling
Copy link
Contributor

chattling commented Mar 22, 2021

Not sure why it doesn't work for you. It works for me. I can give you full code how I create ApolloClient instance to see if something you missed, but like I said, mine is quite complex..

import { ApolloClient, split } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { WebSocketLink } from '@apollo/client/link/ws'
import { getMainDefinition } from '@apollo/client/utilities'
import { createUploadLink } from 'apollo-upload-client'

import { InMemoryCache } from 'apollo-cache-inmemory'

import { ACCESS_TOKEN, SERVER_URL_CONTEXT, SERVER_PORT } from '../constants'

const uploadLink = createUploadLink({
  uri: `/${SERVER_URL_CONTEXT}/graphql`,
})

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem(ACCESS_TOKEN)
  return {
    headers: {
      ...headers,
      Authorization: token ? `Bearer ${JSON.parse(token)}` : '',
    },
  }
})

const wsLink = new WebSocketLink({
  uri: (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.hostname + `:${SERVER_PORT}/${SERVER_URL_CONTEXT}/subscriptions`,
  options: {
    reconnect: true,
    connectionParams: {
      authToken: localStorage.getItem(ACCESS_TOKEN),
    },
  },
})

const link = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query)
    return kind === 'OperationDefinition' && operation === 'subscription'
  },
  wsLink,
  authLink.concat(uploadLink)
)

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
})

And the versions in package.json are:

"dependencies": {
    "@apollo/client": "^3.3.11",
    "apollo-cache-inmemory": "^1.6.6",
    "apollo-upload-client": "^14.1.3",
    "material-table": "^1.69.2",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",

@weagertron
Copy link

I'm not sure why this issue was closed, this is definitely still a problem. The apollo-cache-inmemory repo has not been updated for a year now and is not ideal to use. The latest version of Apollo Client still presents this error when using it's InMemoryCache package. Is there any scope on when/if this may be fixed? I will likely have to use another library soon as this workaround will be unfit for purpose soon as Apollo Client keeps updating

@Domino987
Copy link
Collaborator

Yes it's closed because mbrn abandoned the project and we cannot fix this without him. Try the community fork material-table-core with many updates and the data will not be mutated anymore.

@weagertron
Copy link

Yes it's closed because mbrn abandoned the project and we cannot fix this without him. Try the community fork material-table-core with many updates and the data will not be mutated anymore.

Ah! Thanks @Domino987, I wasn't aware of that fork. I'll take a look and hopefully I can start to use that

@MuhametSmaili
Copy link

So this happens because the object is frozen and when you pass a frozen object material can not manipulate the data with the frozen object. So you could pass a copy of the object ...
Maybe in the future, it will be fixed, because I do not think this is a good solution. But if you still wanna use material table without changing the behaviour of the frozen object in my case redux-toolkit.

for example:

immutableArrayOfObject =[
        { obj},
        { obj},
        ...
    ]

const newData = immutableArrayOfObject.map(item=>Object.create(item)); 

If there is a better solution please show here 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested waiting for author
Projects
None yet
Development

No branches or pull requests