How to support Reusable State in Effects · reactwg/react-18 · Discussion #18 (original) (raw)
Note: We wrote a new page documenting this behavior in detail on the Beta website: https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development.
Hope this is helpful. Sorry some links from there are 404 because other pages are still being written.
Overview
If you have not yet read the previous post about changes to StrictMode
, you can find it here.
Let’s start by taking a look at an example Component:
function ExampleComponent(props) { useEffect(() => { // Effect setup code...
return () => {
// Effect cleanup code...
};
}, []);
useLayoutEffect(() => { // Layout effect setup code...
return () => {
// Layout effect cleanup code...
};
}, []);
// Render stuff... }
This component declares some effects to be run on mount and unmount. Normally these effects would only be run once (after the component is initially mounted) and the cleanup functions would be run once (after the component is unmounted). In React 18 Strict Mode, the following will happen:
- React renders the component.
- React mounts the component
- Layout effect setup code runs.
- Effect setup code runs.
- React simulates the component being hidden or unmounted.
- Layout effect cleanup code runs
- Effect cleanup code runs
- React simulates the component being shown again or remounted.
- Layout effect setup code runs
- Effect setup code runs
As long as an effect cleans up after itself (by returning a cleanup method when necessary) this should generally not cause problems. Most effects have at least one dependency. Because of this they are probably already resilient to running more than once and may not require any changes.
Effects that only run on mount will likely require changes though. At a high level, the types of effects that will mostly likely require some modification fall into two buckets:
- Effects that require cleanup when unmounting.
- Effects that should only run once (either on mount or when a dependency changes).
Effects that require cleanup should have symmetry.
Whether you’re adding event listeners or interacting with some imperative API– as a general rule, if an effect returns a cleanup function then it should mirror the setup function. Many components today use a variation of the pattern shown below.
// A Ref (or Memo) is used to init and cache some imperative API. const ref = useRef(null); if (ref.current === null) { ref.current = new SomeImperativeThing(); }
// Note this could be useLayoutEffect too; same pattern. useEffect(() => { const someImperativeThing = ref.current; return () => { // And an unmount effect (or layout effect) is used to destroy it. someImperativeThing.destroy(); }; }, []);
If the code above gets unmounted and remounted, the imperative thing will likely be broken. (After all, it was destroyed after the first unmount.) To fix this, we need to (re)initialize the imperative thing on mount.
// Don't use a Ref to initialize SomeImperativeThing!
useEffect(() => { // Initialize an imperative API inside of the same effect that destroys it. // This way it will be recreated if the component gets remounted. const someImperativeThing = new SomeImperativeThing();
return () => { someImperativeThing.destroy(); }; }, []);
Sometimes other functions (like event handlers) also need to interact with the imperative thing. In that case, a ref can be used to share the value.
// Use a Ref to hold the value, but initialize it in an effect. const ref = useRef(null);
useEffect(() => { // Initialize an imperative API inside of the same effect that destroys it. // This way it will be recreated if the component gets remounted. const someImperativeThing = ref.current = new SomeImperativeThing();
return () => { someImperativeThing.destroy(); }; }, []);
const handeThing = (event) => { const someImperativeThing = ref.current; // Now we can call methods on the imperative API... };
Although not as common, the imperative API may also need to be shared with other components. In that case, a lazy initialization function can be used to expose the API.
// This ref holds the imperative thing. // It should only be referenced by the current component. const ref = useRef(null);
// This lazy init function ref can be shared with other components, // although it should only be called from an effect or an event handler. // It should not be called during render. const getterRef = useRef(() => { if (ref.current === null) { ref.current = new SomeImperativeThing(); } return ref.current; });
useEffect(() => { // This component doesn't need to (re)create the imperative API. // Any code that needs it will do this automatically by calling the getter.
return () => { // It's possible that nothing called the getter function, // in which case we don't have to cleanup the imperative code. if (ref.current !== null) { ref.current.destroy(); ref.current = null; } }; }, []);
If you’re unsure about which of the above patterns to use for a particular component, ask us and we’ll help.
Effects that should only run once can use a ref.
Effects that do not require cleanup– even mount effects– might not require any changes to work with the new semantics. Let’s look at an effect that logs impressions to a server.
useEffect(() => { SomeTrackingAPI.logImpression(); }, []);
This effect is meant to log that a user has seen a particular piece of content. What should happen if the content is hidden and then shown again? Should it log a second impression? (That is what the effect would do today if switching tabs remounted the view.) Usually that's the behavior you want, so you don't need to change anything in the code above.
In the rare case where hiding and showing content again doesn't count as a new impression, we could use a ref.
const didLogRef = useRef(false);
useEffect(() => { // In this case, whether we are mounting or remounting, // we use a ref so that we only log an impression once. if (didLogRef.current === false) { didLogRef.current = true;
SomeTrackingAPI.logImpression();
} }, []);
However, usually that is not needed.
The examples above don’t cover every case.
This note covers some of the most common high-level patterns but it’s not an exhaustive list. We plan to write about less common cases in the future. In the meanwhile, if you’re unsure of whether your effect should run more than once– or if your effect doesn’t match one of the patterns shown above– ask us and we’ll help.
Related posts
For more information on the changes to React 18 StrictMode, see: