Objects as useEffect deps

Objects as useEffect deps
Photo by Tine Ivanič / Unsplash

Some time ago my company decided to switch from native to React Native. For the last 2 years, I have gathered enough experience (I hope) to be able to say a word or two about React.

The first thing I want to cover is a common mistake developers make, even those who come from ReactJS. This mistake is about being not careful with useEffect's deps list.

How useEffect compares dependencies?

When a "dependency" from the deps list changes then useEffect will run. It does a === to check the change. The === is fine for strings, numbers, and booleans however for objects it only compares references, not actual equality.

import "./styles.css";
import { useEffect, useState } from "react";

type SomeObject = {
  id: string
}

export default function App() {
  const [counter, setCounter] = useState(0);

  const someObject: SomeObject | undefined = { id: "fake-id" };

  useEffect(() => {
    if (someObject != null) {
      setCounter((c) => c + 1);
    }
  }, [someObject]);

  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>
        Counter: {counter}
      </Text>
    </View>
  );
}
https://snack.expo.dev/@aborek/useeffect-and-infinite-loop

This code will end up with an infinite useEffect increasing the counter. In this example, es-lint actually tells you going to have problems:

0:00
/

When it's safe to use an object with useEffect?

Not always passing an object into useEffect would end up with an infinite loop. useEffect uses === so as long as the object has a constant reference then you are safe.

State value from useState has a constant reference until the setter is called. The same applies to a state from useReducer or a state stored in Redux. As long you are reading "state" objects, you should be safe.

useQuery from Apollo also keeps constant reference (however I would assume the reference could change during a refetch even if the object has not changed in value)

Transformations - be careful

Sometimes, especially when the backend sends data in a weird format you may want to write a custom hook that would transform the response to a more frontend readable format:

const useFetchPatient = (): Patient => {
  const { data: response } = useQuery(LOAD_PATIENT_QUERY)
  return {
    firstName: response.edges[0].node.firstName,
    lastName: response.edges[0].node.lastName,
  }
}

export default function App() {
  const [counter, setCounter] = useState(0);

  const patient = useFetchPatient()

  useEffect(() => {
    setCounter((c) => c + patient.firstName.length);
  }, [patient]);

  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>
        Counter: {counter}
      </Text>
    </View>
  );
}
https://snack.expo.dev/@aborek/useeffectloop-with-transformed-patient

Even that we have used useQuery which returns a constant reference to its data, we still have an infinite the useEffect. This is because of the transformation which happens inside useFetchPatient. With transformations don't forget to wrap the transformed data with useMemo:

type Patient = {
  firstName: string;
  lastName: string;
} 

const useFetchPatient = (): Patient => {
  const { data: response } = useQuery(LOAD_PATIENT_QUERY)
  const transformed = useMemo((): Patient => ({
    firstName: response.edges[0].node.firstName,
    lastName: response.edges[0].node.lastName,
  }), [response])

  return transformed
}

export default function App() {
  const [counter, setCounter] = useState(0);

  const patient = useFetchPatient()

  useEffect(() => {
    setCounter((c) => c + patient.firstName.length);
  }, [patient]);

  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>
        Counter: {counter}
      </Text>
    </View>
  );
}
https://snack.expo.dev/@aborek/68307f

Recap

When you work with useEffect make sure values which are passed to its dependence have a constant reference. When that requirement is not met, expect your devices to become hot 🔥.

Feel free to play with the examples I have provided.

In the future article, I'm planning to cover how we can deal with callbacks passed to useEffect's deps. Sneak peek: useCallback is the way, but IMO not the best one :)