Stale State and Ag-Grid

I'm currently developing a site which allows users to specify a variety of fields, makes a GraphQL API request to a server and then displays the results in an Ag-Grid (a really rather good grid framework for JS (or your framework of choice - React for me)

One of the requirements for this project is that double clicking the id cell should automatically update the grid with just the messages from that ID. This should be simple enough - Ag-Grid provides events, one of which is onCellDoubleClicked. From within this event I could then update the graphQL variables which are stored in context, like so.

const onCellDoubleClicked = (event) => {
  dispatch({ type: "UPDATE", state: { ...state, id: event.value } });
};
const columnTypes = {
  textHeavyColumn: {
    wrapText: true,
    autoHeight: true,
    flex: "1",
  },
  idColumn: {
    onCellDoubleClicked: onCellDoubleClicked,
    onCellClicked: (event) => {
      console.log(state);
    },
  },
};

However, doing this would lead to incorrect things being returned. Through some debugging it became clear that the old state value was being used, rather than the new one - meaning if someone was to update any of the fields these were not being kept.

Weirder still was that I wasn't calling for the state to be updated anywhere else and through useEffects I could confirm no update was occurring until this dispatch call.

The only thing left to assume was that somehow Ag-Grid was storing an old value and using that. And what do you know, this is exactly what was happening.

As onCellDoubleClicked is stored in a dictionary and used when Ag-Grid was created, the value of state was taken and stored then, rather than updating with it. This meant that when the event fired, this old value was the one used.

Stale State

This is what is known as the stale state problem. Our onCellDoubleClicked is what is known as a closure - a type of function that uses a variable from an outer scope (in this case "state"). This leads to old variable values being used. For further reading I'd recommend Dmitri Pavlutin's blog post

Anyway the fix ended up being quite simple for me, I simply created a new dispatch type which takes in the specific key and value to update, thus removing the need for state in the function.

const reducer = (state, action) => {
  console.log(action.state);
  switch (action.type) {
    case "UPDATE":
      return { ...action.state };
    case "KEY_UPDATE":
      return { ...state, [action.key]: action.value };
    default:
      return state;
  }
};

const onCellDoubleClicked = (event) => {
  dispatch({ type: "KEY_UPDATE", key: "id", value: event.value });
};

All rights reserved © Joshua Bosman 2025