Understanding how to trigger page updates in React Hooks
posted 2022.02.11 by Clark Wilkins, Simplexable

I recently had a lot of struggle trying to understand the Rules of Hooks — in particular, how to cause a DOM update via useEffect that would act like the class-based ComponentDidUpdate.

The trick, it seems, is to treat the trigger for useEffect as a separate item from the data you are setting in state — particulary when the data being set is an Object.

Here's the scenario. I set up an array of alarms like this.

const [things, setThings ] = useState();

useEffect( () => {

  const fetchData = async () => {

    const payload = { editing: true, groupId, userId };
    const { data: results, status: statusCode, statusText } = await apiLoader( 'myApiRoute', payload );

    if ( statusCode !== 200 ) {

      console.log ( nowRunning + ': error ' + statusCode );
      return;

    }

    const { rowCount, rows } = results[1];

    setThings( { rowCount, rows } );
    console.log ( 'things was just updated ' );

  }
  fetchData()
    .catch( console.error );

}, [trigger] );

I thought it logical to use things itself as the trigger for useEffect. After all, if the content of things has not changed, why render the DOM again? However, this immediately causes an infinite loop, because when the things object is set by setThings, it's treated as something different. To quote from How to Solve the Infinite Loop of React.useEffect():

“2 objects in JavaScript are equal only if they reference exactly the same object.”

This is a stunningly consequential concept that took a long time for me to find, and I wanted to share it here in the hope of saving someone else the same frustration. You can set trigger to reference anything that's actually a value, and this will work fine — for example things.rowCount, but you cannot trigger off the object itself.

For my purposes, I added:

const [fetchThings, setFetchThings ] = useState( 0 );

Now, useEffect runs at load time which is a default behavior. However, if I make a change that affects the contents of rows as shown above, I just have to call the trigger like this:

setFetchThings( trigger + 1 );

The above changes the value of trigger which is a dependency of the useEffect (see the end of the code above). This change to trigger causes the useEffect to run again, the result is a new things object with the updated data set, and the page gets rendered again.

This is actually even simpler when the trigger item is a simple variable, but, in this case, the things object is an extremely malleable, so using some internal attribute is not an option. The direct object:object comparison does not work (which is what this article starts with), but adding a quasi-trigger that gets updated via useState does the trick.

Some additional comments on state management and renderings are in this older post.