PF4 Table: Expandable examples have misleading/incorrect arguments for `onCollapse` and include in-place mutation of React state
See original GitHub issueIn 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 callingsetState()
afterwards may replace the mutation you made. Treatthis.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:
- Created 4 years ago
- Reactions:3
- Comments:9 (1 by maintainers)
@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.
@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 totableRows
. So modifying an element in that array will modify it in both places (and you should never directly modify React state without using thesetTableRows
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