useEffect is a big enough pain in the hip to justify its own page. While powerful and extremely useful, there are common conventions that devs should follow in order to prevent it from causing strange, strange bugs.
Linter: Our linter currently doesn't lint for dep-array inclusion. There's a lot of legacy code that hasn't been updated to properly follow this pattern, so turning it on and updating all components at once would come at too high of a cost to the progress of the app. Our plan is to fix this gradually.
In the interim, devs are resposible for ensuring that every changing value referenced inside the useEffect is also referenced in the dependency array.
This applies to functions inside of the React Component. Functions outside of a react component (ex. a JS utility function) don't rely on state or props, and therefore do not need to be included in the dep array.
- If possible, try to include that function inside of the useEffect itself
- If that gets too messy (code duplication), then see if the function can be refactored into a pure function and move outside of the component.
- If it relies too heavily on state, then its time brings out useCallback as a last resort. useCallback also has a dep array. Be sure to include all dependencies in that array.
Read more about this in the React Docs
Check to see how often logic or async calls are running inside the useEffect. Optimize by escaping only refactoring computationally expensive operations to a function and only running that function when necessary.
As a general rule of thumb
- useEffect is great for fetches and awaiting asynchronous data
- useMemo is good for calculating and updating expensive synchronous operations and saving it to a constant
- lazy-initialization is a great pattern for initializing state once without holding up rendering of the component
Read more about this in Common Utilities and Patterns