question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

PF4 Table: Expandable examples have misleading/incorrect arguments for `onCollapse` and include in-place mutation of React state

See original GitHub issue

In the PF4 Table examples and demos which include expandable rows, there is an onCollapse method which looks like this (example 1, example 2):

  onCollapse(event: React.MouseEvent, rowKey: number, isOpen: boolean) {
    const { rows } = this.state;
    /**
     * Please do not use rowKey as row index for more complex tables.
     * Rather use some kind of identifier like ID passed with each row.
     */
    rows[rowKey].isOpen = isOpen;
    this.setState({
      rows
    });
  }

There is also a rowKey prop in TableBody, and based on the above disclaimer, that led me to believe that the rowKey prop would define the value of the rowKey argument of onCollapse and give me a non-numeric reference to which row is being collapsed. In fact, TableBody’s rowKey prop seems unrelated (I’m not sure exactly what it’s for).

The actual code which calls this onCollapse function is here:

onCollapse && onCollapse(event, rowIndex, rowData && !rowData.isOpen, rowData, extraData);

So, the value being passed into the rowKey argument in these examples is actually the rowIndex, and the examples don’t include the rowData or extraData arguments which actually are useful for identifying the row using its other properties.

We should at least change rowKey to rowIndex in the docs here, and possibly also show a use case where something like rowData.id is used instead, so that the disclaimer becomes unnecessary and people don’t copy the problematic code.

The other problem with this code is that it mutates React state in-place (rows[rowKey].isOpen = isOpen;), which the React docs say never to do:

Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.

Even though setState is called in the next line and this is probably safe to do here, it is a bad practice to spread. Instead the function should do something like:

    const { rows } = this.state;
    const row = rows[rowIndex]; // or rows.find(...) if not using rowIndex
    const newRows = [...rows];
    newRows[rows.indexOf(row)] = { ...row, isOpen };
    this.setState({ rows: newRows });

And we should look for other examples of mutating state in-place like this to fix.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:3
  • Comments:9 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
mturleycommented, Aug 26, 2021

@thaorell also, I would recommend not storing the array of rows in state at all, and instead storing very minimal state describing what rows are expanded etc. And then in your render function you can define a new rows array on each render by mapping over your API data and returning row objects (or JSX if you use the newer composable table) with properties based on the underlying data and your minimal state. If you want to see what I mean you can look at the examples I’ve rewritten in the incomplete PR I linked above. That approach helps avoid duplicating your API data in state and everything becomes more predicable.

1reaction
mturleycommented, Aug 27, 2021

@thaorell I’m not certain exactly why this breaks it, but I can tell you that the issue has to do with JS/TS object/array references. Without spread, you are setting newRows to be a reference to tableRows. So modifying an element in that array will modify it in both places (and you should never directly modify React state without using the setTableRows function you get from useState because React doesn’t expect that and it can cause weird behavior).

With the spread operator here, newRows is a new array with all the same elements as the state array. So you can mutate it safely without changing anything else, then use your mutated copy in setTableRows.

I’m not sure what internally is making your in-place state mutation break things when the in-place mutations in the examples aren’t, but it’s some kind of react internal magic. We should just avoid it in general for reasons like this.

Also, I’m working on a PR to clean up all of these examples that should help, hoping to wrap that up next week. https://github.com/patternfly/patternfly-react/pull/6168

Read more comments on GitHub >

github_iconTop Results From Across the Web

Expanded row getting collapsed on state update on react-table
I have tried the below code. it's kept opening that nested row but inside the subcomponent nested table didn't show up (Please find...
Read more >
You Probably Don't Need Derived State – React Blog
We did not provide many examples, because as a general rule, derived state should be used sparingly. All problems with derived state that...
Read more >
A simple table library with built in expandable rows and ...
A simple table library with built in sorting, pagination, selection, expandable rows and customizable styling. View Demo View Github. React Data ...
Read more >
Exploring React's State Propagation - SitePoint
Following his article on working with data in React, Eric Greene of the Microsoft Developer Network looks at state propagation in React.
Read more >
react-data-table-component - npm
There are 88 other projects in the npm registry using ... Demo and Examples ... yarn add react-data-table-component styled-components ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found