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.
The app I'm working on displays a user dashboard on login with a sidebar for navigation. It uses firebase. I do most of my data fetch from firebase in my async componentDidMount() and store the data in my component state. It takes a couple of seconds to finish all fetches. But if the user decides to navigate to another screen before the fetch is complete, I get the
Can't call setState on unmounted component
warning (as expected). So I do some digging and find that
if(this.isMounted()) this.setState({updates:updates})
makes the warning go away, but then I also find that using isMounted is an antipattern.
The official documentation on this issue suggests tracking the mounted state ourselves by setting _isMounted=true in componentDidMount and then set it to false in the componentWillUnmount. The only way I see to achieve this would be through a variable in component state. Turns out, setState doesn't work in componentWillUnmount. [Issue 1] (I tried calling an external function from componentWillUnmount which in turn sets the state variable. Didn't work.)
The documentation suggests another way, to use cancellable promises. But I'm clueless about how to achieve that with await firebase calls. I also couldn't find any way to stop firebase calls mid-track. [Issue 2]
So now I'm stuck with the warning and data leaks.
a. How do I resolve this problem?
b. Is this something I need to take seriously?
It's good practice to check if the component is still mounted when a request completes, if there is a risk of the component unmounting.
You don't need to put _isMounted in your component state since it will not be used for rendering. You can put it directly on the component instance instead.
Example
class MyComponent extends React.Component {
state = { data: [] };
componentDidMount() {
this._isMounted = true;
fetch("/example")
.then(res => res.json())
.then(res => {
if (this._isMounted) {
this.setState({ data: res.data });
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
// ...
}
}
constructor(props) {
super(props);
console.log("props");
this.state = {
userId : "12345",
};
}
componentDidMount() {
console.log("componentDidMount");
Actions.getProductDetails({userId:"123456"});
Actions.getProductDetails.completed.listen(this.gotProductDetails.bind(this));
Actions.cancelOrder.completed.listen(this.cancelOrderCompleted.bind(this));
}
gotProductDetails(data) {
console.log("gotProductDetails");
}
goBack(data) {
console.log("justgoback");
this.props.back();
}
cancelProduct() {
console.log("SDsadsadsad");
Actions.cancelOrder({
orderId:this.state.order.id,
canelMsg:this.state.selectedReason,
userId:this.state.userId
});
}
cancelOrderCompleted(data) {
console.log("cancelOrderCompleted");
this.goBack();
}
My issue is some functions are mounting twice whenever I change the
route and revisit this route again I would show you console.log here
This is for first time I come to this route:
props
cancelOrder.js:190 componentDidMount
cancelOrder.js:197 gotProductDetails
Now I will do cancelProduct call and log will be
SDsadsadsad
cancelOrder.js:221 cancelOrderCompleted
cancelOrder.js:210 justgoback
This is for second time i.e, I will go back from this route and revisit:
props
cancelOrder.js:190 componentDidMount
cancelOrder.js:197 gotProductDetails
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the cancelOrder component.
cancelOrder.js:197 gotProductDetails
Now I will do cancelProduct call and log will be
SDsadsadsad
cancelOrder.js:221 cancelOrderCompleted
cancelOrder.js:210 justgoback
cancelOrder.js:221 cancelOrderCompleted
cancelOrder.js:210 justgoback
In the above log you can see that for the second time line number 197 221 210 executed twice with the error I was not able to solve
I'm using react navigator for route
I checked in release version also, but it is having same error it was told in one Github issue, but was not able to find now.
Every time you run this line
Actions.cancelOrder.completed.listen(this.cancelOrderCompleted.bind(this));
The listen method gets a new function instance every time it runs, so if this page was mounted twice in the app's lifecycle, the cancelOrderCompleted would run twice and one of them probably in an unmounted component which is bad.
Generally I would advise that your getProductDetails would return a Promise. If you don't want to do that, make sure you remove the listeners when your component is unmounted.
And be aware that cancelOrderCompleted.bind(this) creates a new delegate instance that you can't recreate when stopping the listener. Unless you keep it in a data member.
Edit:
Code example -
constructor(props) {
super(props);
console.log("props");
this.state={
userId : "12345",
}
this.getProductDetailsBound = this.gotProductDetails.bind(this);
this.cancelOrderCompletedBound = this.cancelOrderCompleted.bind(this);
}
componentDidMount() {
console.log("componentDidMount")
// Listen before you call getProductDetails, not after
Actions.getProductDetails.completed.listen(this.getProductDetailsBound);
Actions.cancelOrder.completed.listen(this.cancelOrderCompletedBound);
Actions.getProductDetails({userId:"123456"});
}
componentWillUnmount() {
Actions.getProductDetails.completed.stopListening(this.getProductDetailsBound);
Actions.cancelOrder.completed.stopListening(this.cancelOrderCompletedBound);
}
In my project I have a call to an action that makes a webservice call and in turn dispatch actions to the result of the ws, these actions edit the store.
My problem is in :
ComponentDidUpdate () {
If (this.props.messages.length) {
Const items = this.props.messages.filter (this.isDisplayable);
This.timer = setInterval (() => {
If (items.length> 0) {
This.props.popItem (items);
} Else {
ClearInterval (this.timer);
}
}, This.props.interval);
}
}
In fact it is launched several times and I have warnings of
Warning: flattenChildren (...): Encountered two children with the same
key, 1. Child keys must be unique; When two children share a key,
only the first child will be used.
I used the componentDidMount but it launches it before api responds.
my question is:
Is that there is a way to update the component only at the response of my action, or alternatively to pass the warnings ?
try this :
componentWillReceiveProps(nextProps) {
if (this.props.messages === nextProps.messages) return;
i had some probleme and i resolve it by force update
forceUpdate () {
If (this.props.messages.length) {
...
}
}
In my project I have a call to an action that makes a webservice call and in turn dispatch actions to the result of the ws, these actions edit the store.
None of the methods componentDidMount and componentDidUpdate are good.
Observe the Store in Redux and update your component accordingly when the correct action TYPE is found.
Since you are using the Redux architecture, the state for all your components is in a single place — in the Store.
yes i know, but the problem is that componentDidUpdate is called several times which gives me the index error.
This is quite normal in React. Check this lifecycle.
What you should do is the govern the Redux architecture.
I will try today to provide some diagrams for you.
In general, anything you do will be from the global Store.
You may forget the React.Component state, and props you had in the non-Redux applications.
You typically need to use the Wrapper as a context provider around your app, where the context is the property of React.Component.
The context will be passed to all children and grandchildren so this will be the global Store organization.
Then you will need to read the Store from the context, and call the two typical methods: dispatch and subscribe.
I'm experiencing this weird issue where my react-redux store is updating, but is not updating within the function that calls the actions.
this.props.active is undefined, then I set it to an integer with this.props.actions.activeSet(activeProc), but it remains undefined and enters the next if condition.
I know my app is working because everything else works with this.props.active having the correct value.
Is this supposed to happen?
edit:
After doing some testing, it appears that the state remains the same inside the onClick function.
All calls to console.log(this.props) made within the onClick function show no change to the state, but adding setTimeout(() => {console.log(this.props)}, 1) at the end to test shows that the state is being updated.
Other parts of the app are working as intended, with state changes applied immediately.
But I still don't understand what is going on.
Component function code
() => {
console.log(this.props.active); // undefined
if (this.props.active === undefined && this.props.readyQueue.length > 0) {
let activeProc = this.props.readyQueue[0];
this.props.actions.readyPop();
this.props.actions.activeSet(activeProc); // set to an integer
this.props.actions.execStateSet("Running");
}
console.log(this.props.active); // still remains undefined
if (this.props.active === undefined) {
this.props.actions.execStateSet("Idle");
}
}
function mapStateToProps(state, props) {
return {
active: state.ProcessReducer.active,
};
}
Action code
export const activeSet = (procId) => {
return {
type: 'ACTIVE_SET',
procId
}
}
Reducer code
case 'ACTIVE_SET':
return Object.assign({}, state, {
active: action.procId
});
Your Redux state updates synchronously with the dispatch of your action. Your reducer has executed by the time the dispatch call returns.
However, React isn't Redux. Redux tells React-Redux's wrapper component that the state has changed. This also happens before dispatch returns.
React-Redux then tells React that the component needs to be rerendered by calling forceUpdate. React then waits until it feels it's a good time to take care of that. I haven't looked, but it probably uses setImmediate or equivalent but it's async. This allows React to batch updates and maybe there are other reasons.
In any case, the React-Redux wrapper component will get rendered by React when the time comes and it'll use your mapStateToProps to distill theprops out of the state and then passes them to React as props for your actual component. Then, when React feels it's an okay time, it calls your render method or function. It may do all kinds of things in before that, such as calling componentWillReceiveProps or rendering some other component that also needs rendering. In any case it's none of our business. React does its thing. But when your Render function is called, your props will now reflect the new state.
You shouldn't rely on new state in an onClick handler. The onClick should only call the bound action creator, which I guess is now more aptly called an action dispatcher. If something needs to be done with the new state, you should use Redux-Thunk middleware and create a thunked action creator. These have access to getState and if they don't perform any internal async stuff, then the entire action can actually be just as synchronous as a simple dispatch (not that you'd need that in a simple onClick handler).
Finally, React is very asynchronous in nature. Think of it as telling React what you want (component + props) and letting React take it from there. If React needs to know how to turn a component into DOM elements, it'll call your component's render function. How or when React does is thing is an implementation detail that doesn't concern us.