I'm trying to create a Table component from scratch using React memo component as rows to prevent unecessary re-rendering. The rows cells is an array of React components created from a children function with row data, row id, row index and a commitChange function as parameters. The commitChange function is used to set the Table state from a row. This is the code:
<Table data={[{name: 1}, {name: 2}]}>
{({data, index, id, commitChange})=>
[
<div>Name: {data.name}</div>,
<TextBox value={data.name} onChange={(value)=>commitChange('name', parseInt(value, 10))}/>,
<TextBox value={data.place}/>
]
}
</Table>
TableComponents.jsx
export const Table = ({ data: initialData, maxRows = 10, children: makeChildren, primaryKey = 'ID' }) => {
const [TableData, setTableData] = useState(initialData);
console.log('table re-render')
useEffect(() => {
console.log('--->table data', TableData)
}, [TableData]);
useEffect(() => {
setTableData(initialData)
}, [initialData]);
const renderCells = useCallback((param) => {
return makeChildren(param)
}, [])
const commitChange = useCallback((field, value, index) => {
setTableData(prevTableData => {
const newState = Array.from(prevTableData)
newState[index][field] = value
return [...newState]
})
}, [])
const renderRows = () => {
return TableData.map((row, i) =>
<TableRow
key={row[primaryKey]}
index={i}
rowData={row}
cells={renderCells}
id={row[primaryKey]}
updateTableData={commitChange}
/>
)
}
//
return (
<table>
<tbody>
{
renderRows()
}
</tbody>
</table>
);
}
const TableRow = React.memo(({ index, rowData, cells, id, updateTableData }) => {
console.log('render row' + index, rowData)
useEffect(() => {
console.log('row ' + index + ' data', rowData)
}, [rowData])
const renderCells = () => {
return cells({ data: rowData, index, id, commitChange: (field, value) => updateTableData(field, value, index) }).map((c, i) => {
return (
<td key={i}>
{c}
</td>
)
})
}
return (
<tr>
{renderCells()}
</tr>
)
})
When an element was added to the data props on the Table component the table re-render and render only the added row and this works ok.
But when a row element is edit from the textbox in the second cell the data on the parent Table component was correctly updated but the row not re-render. When I check the previus and next proprieties passed to component with React.memo areEqual function (Documentation here) the data proprieties is the same. The table component is re-render when TableData is update by row and the renderRows function is executed but without re-render of the updated row.
What is the problem? Thanks for help
P.S No I don't want an external library to make custom Table elements
The problem is that you are shallow-copying your TableData (using ...
) so when you update the value here:
newState[index][field] = value
... you are mutating the item in the array but the reference to that item stays the same. One fix would be to do this:
setTableData(prevTableData =>
prevTableData.map((item, i) =>
i === index ? { ...item, [field]: value } : item,
),
)
So now a new object is created when a field is updated.