Search code examples
reactjsreact-hooksuse-stateag-grid-react

react hook useState in AgGridColumn onCellClicked function


Currently, I am using functional react components with react hook useState and an AgGridReact Component.

I am displaying an AgGridReact and put a onCellClicked function on a AgGridColumn. So far everything is working. In the onCellClicked function I want to update my state and do something depending on its current value.

Here is the problem:

if I want to use my state get/set (useState hook) inside of the onCellClicked function it is not working as expected. For some reason, I can not update my state.

In a react class component it is working.

EDIT: I experimented for a while and found out that in the onCellClicked function I have only the default value in myText. I can update it once. If I spam the onCellClicked function it will append the text again to the default value from useState("default myText");. I would expect that the string would get longer as often I click on the cell. Just as in my Class Component example. If I use a simple button outside of the AgGridReact <button onClick={() => setMyText(myText + ", test ")}>add something to myText state</button> it is working as expected, the string gets longer every time I click on my <button>. If I change the state of myText via the <button> outside of the AgGridReact and then click on the cell function again the state previously setted through my <button> is lost.

Example react hook component:

import React, { useState } from 'react';
import { AgGridColumn, AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';

function App() {
  const [myText, setMyText] = useState("default myText");
  const [todoListRowData, setTodoListRowData] = useState([]);

  // ....fetch data and set the todoListRowData state.... 

  const myCellClickFunction = (params, x) => {
    // here is the problem:
    // no matter how often I click in the cell myText is every time the default value 'default myText'
    // EDIT: I found out I can update the state here but only from the initial default value once, myText is on every cell click again "default myText" and will be concatenated with "hookCellClicked". So every time I click this cell the state gets again "default myTexthookCellClicked"
    console.log(myText);
    setMyText(myText + "hookCellClicked");
  }

  return (
      <div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
        <AgGridReact rowData={todoListRowData} >
            <AgGridColumn headerName="ID" field="id" maxWidth="50"></AgGridColumn>
            <AgGridColumn headerName="UserId" field="userId" maxWidth="85"></AgGridColumn>
            <AgGridColumn headerName="Title" field="title" minWidth="555"></AgGridColumn>
            <AgGridColumn headerName="completed" field="completed"></AgGridColumn>
            <AgGridColumn headerName="Testcol" onCellClicked={(params) => myCellClickFunction(params)}></AgGridColumn>
        </AgGridReact>
      </div>
}
export default App;

If I do the exact same thing in a class component it is working fine.

Example Class Component:

import React from 'react';
import { AgGridColumn, AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';

class MyClassComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            myClassComponentRowData: [],
            testState: "defaul testState"
        };
    }

    // ....fetch data and set ag grid rowData state....

    handleCellClick = (params) => {
        // here everything works just fine and as expected 
        // every time I click on the cell the state testState updates and it is added ", handleCellClick" every time
        console.log(this.state.testState);
        this.setState({testState: this.state.testState + ", handleCellClick"});
    }

    render() {
        
        return  <div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
                    <AgGridReact rowData={this.state.myClassComponentRowData} >
                        <AgGridColumn headerName="ID" field="id" maxWidth="50"></AgGridColumn>
                        <AgGridColumn headerName="UserId" field="userId" maxWidth="85"></AgGridColumn>
                        <AgGridColumn headerName="Title" field="title" minWidth="555"></AgGridColumn>
                        <AgGridColumn headerName="completed" field="completed"></AgGridColumn>
                        <AgGridColumn headerName="Testcol" onCellClicked={(params) => this.handleCellClick(params)}></AgGridColumn>
                    </AgGridReact>
                </div>
    }
}

export default MyClassComponent;

Am I doing something wrong? I want to use a functional component with react hooks.


Solution

  • There is nothing wrong with the code in your question except that the callback myCellClickFunction references the old state myText which is captured in the previous render call. If you log in the render method, you can see the state is updated properly. This problem is called stale closure.

    function App() {
      const [myText, setMyText] = useState("default myText");
      const [todoListRowData, setTodoListRowData] = useState(rowData);
    
      console.log("render", myText); // prints the latest myText state
      ...
    }
    

    You can see my other answer here about how to get the latest state in callback when using React hook. Here is an example for you to try that out.

    function useExtendedState(initialState) {
      const [state, setState] = React.useState(initialState);
      const getLatestState = () => {
        return new Promise((resolve, reject) => {
          setState((s) => {
            resolve(s);
            return s;
          });
        });
      };
    
      return [state, setState, getLatestState];
    }
    
    function App() {
      const [myText, setMyText, getLatestMyText] = useExtendedState(
        "default myText"
      );
      const myCellClickFunction = async (params) => {
        setMyText(params.value);
        const text = await getLatestMyText();
        console.log("get latest state in callback", text);
      };
      ...
    }
    

    Live Demo

    Edit AgGrid Get Latest State In Callback