Why useEffect running twice and how to handle it well in React? - javascript

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;

useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...

Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

Related

For some reason I am getting 4 responses when I should be getting one using axios? [duplicate]

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

React useEffect with axios

Below is a snippet of code to fetch data from url by axios,
import React, { useState, setEffect, useEffect } from 'react';
import axios from "axios";
import LoadingPage from "./LoadingPage";
import Posts from "./Posts";
const url = "https://api-post*****";
function App() {
const [posts, setPosts] = useState([]);
const fetchPost = async() => {
try {
const response = await axios(url);
return response.data;
} catch (error) {
console.error(error);
}
};
let data = fetchPost();
setPosts(data);
return (
<main>
<div className="title">
<h2> Users Posts </h2>
{posts.length
? <Posts posts={posts} />
: <Loading posts={posts} />
}
</div>
</main>
);
}
export default App;
However, it got the error of
uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite
Question 1: How could this be of too many re-render, there is no loop or something?
To solve this bug, we can use below changes:
const [posts, setPosts] = useState([]);
const fetchPost = async () => {
try {
const response = await axios(url);
setPosts(response.data);
} catch (err) {
console.error(err);
}
};
useEffect(()=> {
fetchPost();
}, [posts])
Question 2: how the useEffect work to avoid too many calls?
Question 3: I always treat react hooks under hood as web socket communications, etc. If that is the case?
When you call setPosts the component will render again, and fetch the data again, at which point you set state with the data forcing a new render which fetches the data...etc.
By using useEffect you can fetch the data, and set state once when the component is first rendered using an empty dependency array.
useEffect(() => {
// Fetch the data
setposts(data);
}, []);
You probably don't want to watch for posts in this useEffect (you can have many) like you're doing in your updated example because you may run into the same issue.
I will only answer number one.
Caveat: the answer is a bit long.
I really hope it will help you to understand a topic that took me a long time to grasp.
Answer one:
To answer this question we should ask our selves two things, a) what is a side effect? and b) how the life cycle of components works?
so, we know that React functional component are pure functions and they should stay that way, you can pass props as parameters and the function will do stuff and return JSX, so far so good.
and for the second part we cannot control React virtual DOM, so the component will render many times during it's lifecycle, so imagine that the virtual DOM decided to check the code and compare between the virtual DOM and the real DOM and in order to do that he will have to check and "run" the code that resides inside that specific component.
the virtual DOM will run the API call, he will find a different result which will cause a new render to that specific component and this process will go on and on as an infinite loop.
when you are using usEffect you can control when this API call will take place and useEffect under the hood makes sure that the this API call ran only one your specific change take place and not the virtual DOM V.S real DOM change.
to summarize, useEffect basically helps you to control the LifeCycle of the component
Please first check your state like this.
useEffect(()=> {
fetchPost();
}, [posts]);

How to handle useEffect being called twice in strictMode for things that should only be run once? [duplicate]

This question already has answers here:
Why useEffect running twice and how to handle it well in React?
(2 answers)
Closed 5 months ago.
This post was edited and submitted for review 5 months ago and failed to reopen the post:
Original close reason(s) were not resolved
I understand that React calls useEffect twice in strict mode, this question is about asking what the correct way of handling it is.
I have recently hit an issue with React useEffect being called twice in strictMode. I'd like to keep strict mode to avoid issues, but I don't see any good way of making sure some specific effects are only run once. This is in a next.js environment, where I specifically want the code to only run in the client once.
For example, given the following component:
import React, { useState, useEffect } from "react";
import ky from "ky";
const NeedsToRunOnce: React.FC = () => {
const [loading, setLoading] = useState(false);
const doSomethingThatOnlyShouldHappenOnce = (data: any) => {
// Do something with the loaded data that should only happen once
console.log(`This log appears twice!`, data);
};
useEffect(() => {
if (loading) return;
console.log("This happens twice despite trying to set loading to true");
setLoading(true);
const fetchData = async () => {
const data = await ky.get("/data/json_data_2000.json").json();
setLoading(false);
doSomethingThatOnlyShouldHappenOnce(data);
};
fetchData();
}, []);
return <div></div>;
};
export default NeedsToRunOnce;
useEffect will be called twice, and both times loading will still be false. This is because strictMode makes it be called twice with the same state. The request will go through twice, and call doSomethingThatOnlyShouldHappenOnce twice. All the console.log calls above will appear twice in the console.
Since I can't modify the state to let my component know that the request has already started happening, how can I stop the get request from happening twice and then calling code that I only want to be called once? (For context, I am initialising an external library with the data loaded in the useEffect, and this library should only be initialised once).
I found a GitHub issue about this where Dan Abramov says:
Usually you’d want to have some kind of cleanup for your effect. It should either cancel your fetch or make sure you ignore its result by setting a variable inside your effect. Once you do this, there should be no actual difference in behavior.
If you cancel your fetch in effect cleanup, only one request will complete in development. However, it also doesn’t matter if another request fires. It’s being ignored anyway, and the stress-testing only happens in development.
While I agree in principle, this is tedious in practice. I've already hit two different use cases in my application where I have some library call or request that needs to only be done once in the client.
What's a reliable way of making sure that a piece of code in useEffect is only run once here? Using a state to keep track of it having already been run doesn't help, since react calls the component twice with the same state in a row.
You can use a ref to execute your useEffect once (first time or second time, as you wish), or just use a customHook. In my case, i execute the useEffect the second time. Swap true and false, to execute it the first time and not the second one.
import { useEffect, useRef } from "react";
export default function useEffectOnce(fn: () => void) {
const ref = useRef(false);
useEffect(() => {
if (ref.current) {
fn();
}
return () => {
ref.current = true;
};
}, [fn]);
}
And to use it, you can pass your callback function in param :
useEffectOnce(() => console.log("hello"));

Triggering useEffect only certain conditions

I have basic understanding of useEffect. Without second parameter (dependency array) it runs on every render. With empty array, it runs on first render. With parameters in array, it runs whenever some of parameters changes.
Say I have useEffect with two dependencies (from GraphQL query): result.data and result.loading. I want useEffect to run if result.data changes, and result.loading is false. Purpose is for example to update Redux store:
useEffect(() => {
if (result.loading) return;
dispatch(updatePhotos([...photos, ...result.data.photos]));
}, [result.data, result.loading]);
But there's a catch: I have to include photos to list of dependencies. However, photos variable will be updated in other place, and it triggers this useEffect again.
How can I run useEffect only when those two variables changes?
I can of course use useState to store variable resultFetched, set it to true in useEffect and then dispatch only if it is false. But at some point I have to change it back to true, and useEffect runs again, since I can't manually change result.data or result.loading.
I'm lost how to properly use useEffect in these situations when there is lots of variables to handle.
Currently I'm building infinite scrolling photo list, where list is loaded part by part via GraphQL. But when user opens some photo and eventually returns to photo list, it is restored from Redux to same state and scroll position as it was before opening the photo.
I have spent countless hours trying to get it work, but this useEffect-thing is spoiling my every attempt. :) They always gets triggered before I want them to trigger, because there is so many changing variables.
Also, sometimes I want to run a function within useEffect (function added to dependency array), and I use useCallback for that function to memoize it. But then I also have to add all variables that function uses to dependency array of that useCallback, so function gets regenerated when those variables changes. That means that useEffect suddenly runs again, because the function in dependency array changes.
Is there really no way to use functions/variables in useEffect, without them to trigger useEffect?
It all depends on how updatePhotos works. If that creates an action then the problem is you are creating the new state in the wrong place. The previous value of photos shouldn’t be used here because as you pointed out, that causes a dependency.
Instead your reducer will have the old value of photos you can use and you simply pass the new request data to your reducer.
Described in more detail here: https://overreacted.io/a-complete-guide-to-useeffect/#decoupling-updates-from-actions
You can have two separate useEffect functions inside the same component and they will work independent one of another. use one for photos and one for data loading. I hope this example helps you to wrap your head around this.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
useEffect(() => {
const id = setInterval(() => {
setCount2((c) => c + step);
}, 1500);
return () => clearInterval(id);
}, [step]);
return (
<div>
<h1>{count}</h1>
<h1>{count2}</h1>
<input value={step} onChange={(e) => setStep(Number(e.target.value))} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
Please refer to this example in sandbox
https://codesandbox.io/s/react-playground-forked-6h0oz

How can we implement componentWillUnmount using react hooks?

The method componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. If we use useEffect with an empty array ([]) as the second argument and put our function in return statement it will be executed after the component is unmounted and even after another component will be mounted. This is done for performance reasons as far as I understand. In order not to delay rendering.
So the question is - how can we call some function using hooks before a component gets unmounted?
What I am trying to do is an application which saves user's input as he types (without submitting form). I use setInterval to save updated text every N seconds. And I need to force save updates before the component will unmount. I don't want to use prompt by react router before navigating. This is an electron application. I appreciate any thoughts or advice on how to implement such functionality.
Update
Unfortunately, Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?. It basically means that cleanup is run after a component is unmounted and it is not the same as executing code in componentWillUnmount(). I can clearly see the sequence of calls if I put console.log statements in the cleanup code and in another component. The question is whether we can execute some code before a component is unmounted using hooks.
Update2
As I can see I should better describe my use case. Let's imagine a theoretical app which holds its data in a Redux store. And we have two components with some forms. For simplicity, we don't have any backend or any async logic. We use only Redux store as data storage.
We don't want to update Redux store on every keystroke. So we keep actual values in the local component's state which we initialize with values from the store when a component mounts. We also create an effect which sets up a setInterval for 1s.
We have the following process. A User types something. Updates are stored in the local component state until our setInterval callback is called. The callback just puts data in the store (dispatches action). We put our callback in the useEffect return statement to force save to store when the component gets unmounted because we want to save data to store in this case as soon as possible.
The problem comes when a user types something in the first component and immediately goes to the second component (faster than 1s). Since the cleanup in our first component will be called after re-rendering, our store won't be updated before the second component gets mounted. And because of that, the second component will get outdated values to its local state.
If we put our callback in componentWillUnmount() it will be called before unmounting and the store will be updated before the next component mounts. So can we implement this using hooks?
componentWillUnmount can be simulated by returning a function inside the useEffect hook. The returned function will be called just before every rerendering of the component. Strictly speaking, this is the same thing but you should be able to simulate any behaviour you want using this.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
})
Update
The above will run every time there is a rerender. However, to simulate the behaviour only on mounting and unmounting (i.e. componentDidMount and componentWillUnmount). useEffect takes a second argument which needs to be an empty array.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
}, [])
See a more detailed explanation of the same question here.
Since the introduction of the useLayoutEffect hook, you can now do
useLayoutEffect(() => () => {
// Your code here.
}, [])
to simulate componentWillUnmount. This runs during unmount, but before the element has actually left the page.
The question here is how do you run code with hooks BEFORE unmount? The return function with hooks runs AFTER unmount and whilst that doesn’t make a difference for most use cases, their are some where it is a critical difference.
Having done a bit of investigation on this, I have come to the conclusion that currently hooks simply does not provide a direct alternative to componentWillUnmount. So if you have a use case that needs it, which is mainly for me at least, the integration of non-React libs, you just have to do it the old way and use a component.
Update: see the answer below about UseLayoutEffect() which looks like it may solve this issue.
I agree with Frank, but the code needs to look like this otherwise it will run only on the first render:
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
This is equivalent to ComponentWillUnmount
Similar to #pritam's answer, but with an abstracted code example. The whole idea of useRef is to allow you to keep track of the changes to the callback and not have a stale closure at the time of execution. Hence, the useEffect at the bottom can have an empty dependency array to ensure it only runs when the component unmounts. See the code demo.
Reusable hook:
type Noop = () => void;
const useComponentWillUnmount = (callback: Noop) => {
const mem = useRef<Noop>();
useEffect(() => {
mem.current = callback;
}, [callback]);
useEffect(() => {
return () => {
const func = mem.current as Noop;
func();
};
}, []);
};
After a bit of research, found that - you could still accomplish this. Bit tricky but should work.
You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
DEMO
However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
I got in a unique situation where the useEffect(() => () => { ... }, []); answers did not work for me. This is because my component never got rendered — I was throwing an exception before I could register the useEffect hook.
function Component() {
useEffect(() => () => { console.log("Cleanup!"); }, []);
if (promise) throw promise;
if (error) throw error;
return <h1>Got value: {value}</h1>;
}
In the above example, by throwing a Promise<T> that tells react to suspend until the promise is resolved. However, once the promise is resolved, an error is thrown. Since the component never gets rendered and goes straight to an ErrorBoundary, the useEffect() hook is never registered!
If you're in a similar situation as myself, this little code may help:
To solve this, I modified my ErrorBoundary code to run a list of teardowns once it was recovered
export default class ErrorBoundary extends Component {
// ...
recover() {
runTeardowns();
// ...
}
// ...
}
Then, I created a useTeardown hook which would add teardowns that needed to be ran, or make use of useEffect if possible. You'll most likely need to modify it if you have nesting of error boundaries, but for my simple usecase, it worked wonderfully.
import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";
const teardowns: (() => void)[] = [];
export function runTeardowns() {
const wiped = teardowns.splice(0, teardowns.length);
for (const teardown of wiped) {
teardown();
}
}
type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);
/**
* Guarantees a function to run on teardown, even when errors occur.
*
* This is necessary because `useEffect` only runs when the component doesn't throw an error.
* If the component throws an error before anything renders, then `useEffect` won't register a
* cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
*
* This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
* However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
*/
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
// We have state we need to maintain about our teardown that we need to persist
// to other layers of the application. To do that, we store state on the callback
// itself - but to do that, we need to guarantee that the callback is stable. We
// achieve this by memoizing the teardown function.
const teardown = useMemo(onTeardown, deps);
// Here, we register a `useEffect` hook to run. This will be the "happy path" for
// our teardown function, as if the component renders, we can let React guarantee
// us for the cleanup function to be ran.
useEffect(() => {
// If the effect gets called, that means we can rely on React to run our cleanup
// handler.
teardown.registered = true;
return () => {
if (isDebugMode) {
// We want to ensure that this impossible state is never reached. When the
// `runTeardowns` function is called, it should only be ran for teardowns
// that have not been able to be hook into `useEffect`.
if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
}
teardown();
if (isDebugMode) {
// Because `teardown.registered` will already cover the case where the effect
// handler is in charge of running the teardown, this isn't necessary. However,
// this helps us prevent impossible states.
teardown.called = true;
}
};
}, deps);
// Here, we register the "sad path". If there is an exception immediately thrown,
// then the `useEffect` cleanup handler will never be ran.
//
// We rely on the behavior that our custom `ErrorBoundary` component will always
// be rendered in the event of errors. Thus, we expect that component to call
// `runTeardowns` whenever it deems it appropriate to run our teardowns.
// Because `useTeardown` will get called multiple times, we want to ensure we only
// register the teardown once.
if (!teardown.pushed) {
teardown.pushed = true;
teardowns.push(() => {
const useEffectWillCleanUpTeardown = teardown.registered;
if (!useEffectWillCleanUpTeardown) {
if (isDebugMode) {
// If the useEffect handler was already called, there should be no way to
// re-run this teardown. The only way this impossible state can be reached
// is if a teardown is called multiple times, which should not happen during
// normal execution.
const teardownAlreadyCalled = teardown.called;
if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
}
teardown();
if (isDebugMode) {
// Notify that this teardown has been called - useful for ensuring that we
// cannot reach any impossible states.
teardown.called = true;
}
}
});
}
}
It does not matter wether the returned function from useEffect gets called before or after the component unmounted: You still have access to the states valuey through the closure:
const [input, setInput] = useState(() => Store.retrieveInput());
useEffect(() => {
return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
}, []);
If you don't manage the input in the components state, your whole structure is broken and should be changed to manage state at the right place. In your case, you should lift the shared input state of the components to the parent.
ReactJS docs on hooks specify this:
Effects may also optionally specify how to “clean up” after them by
returning a function.
So any function you return in your useEffect hook, will be executed when the component unmounts, as well as before re-running the effect due to a subsequent render.

Categories

Resources