Behnam Azimi
Behnam Azimi

Reputation: 2488

State set method did not effect the main object - ReactJS

I have three components in my project, App, DataTable and Table.

The App will render a DataTable that contains the Table component. I just called DataTable with data and columns props in it.

// data sample
const tmpData = [
    {
        name: "Can",
        age: 4,
    }, {
        name: "David",
        age: 44,
    }, {
        name: "Sara",
        age: 14,
    }, {
        name: "Hani",
        age: 24,
    }
]

// main columns array
const tmpColumns = [
    {
        title: "Name",
        accessor: "name",
    }, {
        title: "Age",
        accessor: "age",
    }
]

function App() {
    return (
        <div className="App" style={{background: "#f0f0f0", padding: "1rem"}}>
            <div>App Component:</div>

            <DataTable data={tmpData} columns={tmpColumns}/>
        </div>
    );
}

DataTable component is just for handling selection and filter actions on my table. So, it could manipulate the data and the columns. the Table will render in it and here is the code of DataTable.

function DataTable(props) {

    const [data, setData] = useState(props.data)
    const [columns, setColumns] = useState(props.columns)

    const [selection, setSelection] = useState([])

    useEffect(() => {
        // add select columns after component mount
        handleColumnChange()
    }, [])

    // listen to selection change
    useEffect(() => {

        // selection change log, It change on each select.
        console.log(selection);
    }, [selection])

    function handleRowSelect(rowName) {

        const keyIndex = selection.indexOf(rowName);

        console.log(selection, rowName, keyIndex);

        if (keyIndex === -1)
            setSelection(preSelection => ([...preSelection, ...[rowName]]))
        else
            setSelection(preSelection => preSelection.filter(sl => sl !== rowName))

    }

    function handleColumnChange() {

        // add select column if not added already
        if (!columns.find(col => col.accessor === 'select')) {

            setColumns([
                ...[{
                    title: "Select",
                    accessor: "select",
                    // this method will execute to render checkbox on Select table
                    Cell: function (row) {

                        return <input type="checkbox"
                                      onChange={() => handleRowSelect(row.name, selection)}
                                      checked={selection.includes(row.name)}/>
                    },
                }],
                ...columns,
            ])
        }
    }

    return (
        <div className="DataTable" style={{background: "#e0e0e0", padding: "1rem"}}>
            <div>Data Table:</div>

            <Table {...{data, columns}}/>
        </div>
    )
}

Table component will render columns and suitable data for them. For each column in columns array we have an item to access data (accessor) or an executable method to return custom data (Cell) and here is its code.

function Table(props) {

    const [data, setData] = useState(props.data)

    return (
        <div className="Table" style={{background: "#d5d5d5", padding: "1rem"}}>
            <div>Table</div>

            <table>
                <tbody>
                <tr>
                    {props.columns.map((th, key) => (
                        <th key={key}>{th.title}</th>
                    ))}
                </tr>

                {/* generating data rows */}
                {data.map((dr, key) => (
                    <tr key={key}>
                        {columns.map((col, index) => (
                            <td key={index}>
                                {
                                    // the "Cell" method has high priority than "accessor" selector
                                    (col.Cell && typeof col.Cell === "function") ?
                                        col.Cell(dr) :
                                        dr[col.accessor]
                                }
                            </td>
                        ))}
                    </tr>
                ))}
                </tbody>
            </table>

        </div>
    )
}

As you saw above, to handle row selection I manipulate the columns in the DataTable component by adding a new column at first index of my columns array. Everything works fine for now. But, when I try to select a row, the Call methods of the select column, could not access the selection array state of my DataTable component. and it's my problem!

Actually, on each select, the selection array must update and target checkbox must check. Apparently, there is a copy of the selection that changes (or maybe not changes).

You also could check the whole project on CodeSandbox

Upvotes: 0

Views: 209

Answers (2)

Jojo Tutor
Jojo Tutor

Reputation: 890

I have some fixes to your code, check it out on CodeSandbox.

The idea is that you don't need to put the columns to state, instead you just need to get the columns with the selection box.

I also added a simple optimization to it by implementing React.useMemo to memoized the calculated columns. It will only be re-calculated when the props.columns and selection state changes.

Hope this helps! Happy coding! :)

Upvotes: 1

windmaomao
windmaomao

Reputation: 7680

Ok, your tmpData passed from App to DataTable is read-only. So by design you will not see any change your data along the way.

There're couple of ways to get it working, mostly having something to do to allow your DataTable to pass the change back to App if that happens.

Step 1, you could add one prop called onRowClick on the DataTable,

  <DataTable data={tmpData} onRowClick={row => { console.log(row) } />

Step 2, you need to allow your tmpData to change after the event. You are using hooks, so we can

  const [tmpData, setTmpData] = useState([sampleData])
  return (
    <DataTable data={tmpData} onRowClick={row => { 
      // use setTmpData to adjust the tmpData to get a new instance of tmpData
    } />
  )

Of course for things with this complexity, we normally use useReducer instead of useState, since there's definitely other things that you want to do other than selecting the row :)

Upvotes: 0

Related Questions