How to avoid rerunning React useEffects
Jun 12, 2020
One of the gotchas with the React useEffect
hook and other hooks that take a list of dependencies is that
changes to these dependencies are tested for using a standard Javascript equality check.
This is fine for primitive types such as numbers and strings but can be an issue with arrays and other object types.
For example, consider the following React component which takes an array of services
and makes
the named Firebase services available to child components:
export function Firebase({services, children}) { useEffect( () => { setUpFirebase(services) return () => { teardownFirebase() } }, [services] ) return children }
Here, setupFirebase
and teardownFirebase
could be expensive operations that we only want to call when necessary.
In this case, that would just be when the list of services changes.
We could call the component with:
<Firebase services={["auth", "database"]}>
However this would cause setupFirebase
and teardownFirebase
to be called every time the Firebase
component is re-rendered.
This is because a new ["auth", "database"]
array is created each time this line is invoked.
We have specified that useEffect
should only run if services
changes. To determine this it will check the old
value of services
against the new value. So, in this case, unless ["auth", "database"] === ["auth", "database"]
it will
run the effect. But this will always return false
and run the effect because these are different arrays
(even though their contents is the same).
In order to retain the ability to dynamically specify different services
and not change how we invoke
the component, we need to derive something from the services
array that will change only when the list of
required services changes. We can then pass this derived value as a dependency to useEffect
instead.
We know the service names will not contain commas, so a simple approach in this case is to use a comma-separated string of the sorted service names:
export function Firebase({services, children}) { const normalisedServices = [...services].sort().join() useEffect( () => { setUpFirebase(services) return () => { teardownFirebase() } }, [normalisedServices] ) return children }
Javascript strings do compare equal if their contents are the same, so the new test ("auth,database" === "auth,database"
)
will return true
and the effect will not be run.
We are sorting the array so that the order the services are specified in is ignored
(i.e. ["auth", "database"]
and ["database", "auth"]
will also compare equal).
If you want to be really thorough and avoid re-running the effect if duplicate service names are specied then you could use a
Set
to dedupe them first:
const normalisedServices = [...new Set(services)].sort().join()
Then ["auth", "database", "auth"]
and ["database", "auth"]
would also compare equal and not cause the effects to be rerun.