3

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.

Eb Heravi
  • 372
  • 3
  • 14
anvin
  • 97
  • 6

1 Answers1

1

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

NearHuscarl
  • 12,341
  • 5
  • 39
  • 69
  • Hey, thanks for your answer. This is a nice way to add a callback to a setState call. But I think I have another issue with the scope of useState states in a AgGridColumn onCellClicked function. Or do I misunderstand you? I have updated my Question :) – anvin Sep 12 '20 at 08:16
  • @anvin If you need to reference to the latest state when updating state, you need to pass a callback instead. So this is the equivalent to the class-based approach: `setMyText(latestValue => latestValue + ' ' + params.value)`. I've updated the live demo for you. – NearHuscarl Sep 12 '20 at 09:01
  • thanks for your answer. Can you clarify why I need to do it like you said `setMyText(latestValue => latestValue + ' ' + params.value)` outside of the ag grid function I can call `setMyText(myText + ", add something")` and it is working. I am a little confused because I dont know when I have to do setState with an arrow function (like you showed me) and when not? – anvin Sep 13 '20 at 05:41
  • 1
    If u need to reference the last state, use `setState(oldState => modifiedState)`, if u create a completely new state instead of modifying the old state, use `setState(newState)`. The reason u can call `setState()` in `useExtendedState()` and reference `myText` is because `myText` has the latest state *inside* useExtendedState at that time. – NearHuscarl Sep 13 '20 at 05:51