Handle Deep Object Comparison in React's useEffect hook with the useRef Hook

InstructorKent C. Dodds

Share this video with your friends

Send Tweet

The second argument to React's useEffect hook is an array of dependencies for your useEffect callback. When any value in that array changes, the effect callback is re-run. But the variables object we're passing to that array is created during render, so our effect will be re-run every render even if the shape of the object is the same. So let's solve this by doing our own equality check from within the effect callback.

Dean
~ 6 years ago

Curious why you needed a second useEffect, Couldn't you of just run this at the bottom of your first useEffect

const previousInputs = useRef()
previousInputs.current = [query, variables]
Kent C. Doddsinstructor
~ 6 years ago

That's a great point Dean! Could definitely use the same one. The reason I used a different one is probably because I knew that I was going to extract it to its own usePrevious hook in the future 😅

Timothy
~ 6 years ago

Hi, this course seems to be a continuation of some other course. What course is this based on?

Kent C. Doddsinstructor
~ 6 years ago

Hi Timothy 👋 It is not a continuation of any other course. But if you want a primer on hooks you can watch my playlist here: https://kcd.im/hooks-and-suspense

Tommy Marshall
~ 5 years ago

Nitpick, but feels weird to declare the previousInputs ref at the bottom. Also, I wonder if it'd be better to be able to pass a function as the second argument to useEffect that would act as the condition?

Kent C. Doddsinstructor
~ 5 years ago

I wonder if it'd be better to be able to pass a function as the second argument to useEffect that would act as the condition?

Unless something's changed and I'm unaware, that's not how useEffect works. It does not accept a function as the second argument.

malcolm-kee Roslan
~ 5 years ago

My understanding of the const keyword is that it will not be hoisted, thus referring previousInput ref before it is declared should throw an error.

Is it my understanding wrong?

Kent C. Doddsinstructor
~ 5 years ago

Yes, you would be correct except I'm accessing the value of previousInput in a closure that is called after render which is why it works. I did it this way because the effect needs to happen second and I wanted to keep things together.

Saloni Jain
~ 5 years ago

Can we not do this with useState instead of useRef ? We can keep track of the previous variables in state. Would it be any different ?

Guilherme
~ 5 years ago

How come previousInputs is not undefined in the first useEffect? You define it later in the code.

Guilherme
~ 5 years ago

Okay, I saw the other answer but apparently I can't delete my previous comment.

Bosn
~ 5 years ago
function deepCompareEquals(a, b){
  // TODO: implement deep comparison here
  // something like lodash
  // return _.isEqual(a, b);
}

function useDeepCompareMemoize(value) {
  const ref = useRef() 
  // it can be done by using useMemo as well
  // but useRef is rather cleaner and easier

  if (!deepCompareEquals(value, ref.current)) {
    ref.current = value
  }

  return ref.current
}

function useDeepCompareEffect(callback, dependencies) {
  useEffect(callback, useDeepCompareMemoize(dependencies))
}
Phyllis Yen
~ 5 years ago

Hi Kent, can you demonstrate how to use useMemo (by passing the flattened variables into the dependency array) in the component containing <Query /> and put the rendering of <Query /> inside the callback of useMemo so that <Query /> doesn't get re-rendered if the variables doesn't change?

François
~ 5 years ago

Hi Kent, Thanks for the course. Something doesn't click for me. In your useEffect, you're sometimes returning without proceeding with setState. To me, it seems like a violation of the Rules of Hooks https://reactjs.org/docs/hooks-rules.html) because the useState hook is somehow in a condition. Yet your code works and React doesn't complain. Is there a corner case where this wouldn't work?

Merijn
~ 5 years ago

@François, the hooks are still called in the same order, just last calls were omitted. So this should just work, unless React expect all hook calls to be there in the next render... not sure about that. It might worth trying to see if this code construct survives the eslint rule (eslint-plugin-react-hooks). If not we are sure not to use this code construct. However something seems wrong with the Github link, the code of this chapter doesn't showup.

Avi Aryan
~ 5 years ago

Is it guaranteed that hooks run and finish in the order in which they are defined? That is, what happens if the useEffect setting the previousInput runs(& finishes) before the query useEffect? In that case, previousInput will be updated to the current prop values and the isEqual check will be always true.

I guess this is not a problem in the current scenario because both useEffect callbacks are synchronous until the use of previousInput variable. Am I getting this correct?

Brandon Aaskov
~ 5 years ago

@Avi I was curious about this too - it looks like useEffect calls are batched just like setting state from a useState hook (note if you're setting state inside an asynchronous method this is not the case)>

In the below code the component will only be re-rendered once after the initial render.

import React, { Fragment, useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function Component() {
  const [a, setA] = useState("a");
  const [b, setB] = useState("b");
  const myRef = useRef();

  console.log("a", a);
  console.log("b", b);
  console.log(JSON.stringify(myRef.current));

  useEffect(() => {
    if (a !== "aa") setA("aa");
    myRef.current = { ...myRef.current, a: "a" };
  });

  useEffect(() => {
    if (b !== "bb") setB("bb");
    myRef.current = { ...myRef.current, b: "b" };
  });

  return (
    <Fragment>
      <div>a = {a}</div>
      <div>b = {b}</div>
      <div>myRef = {JSON.stringify(myRef)}</div>
    </Fragment>
  );
}

function App() {
  return <Component />;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

your console output here will be:

a a
b b
undefined
a aa
b bb
{{ current: { a: "a", b: "b" }}