2

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

1 Answer 1

5

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.

Sign up to request clarification or add additional context in comments.

1 Comment

thanks, you made my day (fixed a bug at 2.25 AM)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.