Make React useEffect hook not run on initial render - javascript

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>

We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>

You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...

I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....

Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.

#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};

a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).

I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}

This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}

#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;

Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}

You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>

If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);

All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App

const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

Related

React render component only for few seconds

In my existing react component, I need to render another react component for a specific time period.
As soon as the parent component mounts/or data loads, the new-component (or child component) should be visible after 1-2 seconds and then after another few seconds, the new-component should be hidden. This needs to be done only if there is no data available.
This is what currently I've tried to achieve:
import React, { useState, useEffect } from "react";
function App() {
const [showComponent, setShowComponent] = useState(false);
const sampleData = [];
useEffect(() => {
if (sampleData.length === 0) {
setTimeout(() => {
setShowComponent(true);
}, 1000);
}
}, [sampleData]);
useEffect(() => {
setTimeout(() => {
setShowComponent(false);
}, 4000);
}, []);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
The current implementation is not working as expected. The new-component renders in a blink fashion.
Here is the working snippet attached:
Any help to resolve this is appreciated!
Every time App renders, you create a brand new sampleData array. It may be an empty array each time, but it's a different empty array. Since it's different, the useEffect needs to rerun every time, which means that after every render, you set a timeout to go off in 1 second and show the component.
If this is just a mock array that will never change, then move it outside of App so it's only created once:
const sampleData = [];
function App() {
// ...
}
Or, you can turn it into a state value:
function App() {
const [showComponent, setShowComponent] = useState(false);
const [sampleData, setSampleData] = useState([]);
// ...
}
I have modified the code to work, hope this how you are expecting it to work.
import React, { useState, useEffect } from "react";
const sampleData = [];
// this has to be out side or passed as a prop
/*
reason: when the component render (caued when calling setShowComponent)
a new reference is created for "sampleData", this cause the useEffect run every time the component re-renders,
resulting "<h2>found error</h2>" to flicker.
*/
function App() {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
if (sampleData.length === 0) {
const toRef = setTimeout(() => {
setShowComponent(true);
clearTimeout(toRef);
// it is good practice to clear the timeout (but I am not sure why)
}, 1000);
}
}, [sampleData]);
useEffect(() => {
if (showComponent) {
const toRef = setTimeout(() => {
setShowComponent(false);
clearTimeout(toRef);
}, 4000);
}
}, [showComponent]);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
You can try this for conditional rendering.
import { useEffect, useState } from "react";
import "./styles.css";
const LoadingComponent = () => <div>Loading...</div>;
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const onLoadEffect = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
setTimeout(() => {
setIsError(true);
}, 10000);
};
useEffect(onLoadEffect, []);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div className="App">
{isError ? (
<div style={{ color: "red" }}>Something went wrong</div>
) : (
<div>Data that you want to display</div>
)}
</div>
);
}
I needed to do imperatively control rendering an animation component and make it disappear a few seconds later. I ended up writing a very simple custom hook for this. Here's a link to a sandbox.
NOTE: this is not a full solution for the OP's exact use case. It simply abstracts a few key parts of the general problem:
Imperatively control a conditional render
Make the conditional "expire" after duration number of milliseconds.

Not able to figure out this error: Can't perform a React state update on an unmounted component [duplicate]

Problem
I am writing an application in React and was unable to avoid a super common pitfall, which is calling setState(...) after componentWillUnmount(...).
I looked very carefully at my code and tried to put some guarding clauses in place, but the problem persisted and I am still observing the warning.
Therefore, I've got two questions:
How do I figure out from the stack trace, which particular component and event handler or lifecycle hook is responsible for the rule violation?
Well, how to fix the problem itself, because my code was written with this pitfall in mind and is already trying to prevent it, but some underlying component's still generating the warning.
Browser console
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.
in TextLayerInternal (created by Context.Consumer)
in TextLayer (created by PageInternal) index.js:1446
d/console[e]
index.js:1446
warningWithoutStack
react-dom.development.js:520
warnAboutUpdateOnUnmounted
react-dom.development.js:18238
scheduleWork
react-dom.development.js:19684
enqueueSetState
react-dom.development.js:12936
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:356
_callee$
TextLayer.js:97
tryCatch
runtime.js:63
invoke
runtime.js:282
defineIteratorMethods/</prototype[method]
runtime.js:116
asyncGeneratorStep
asyncToGenerator.js:3
_throw
asyncToGenerator.js:29
Code
Book.tsx
import { throttle } from 'lodash';
import * as React from 'react';
import { AutoWidthPdf } from '../shared/AutoWidthPdf';
import BookCommandPanel from '../shared/BookCommandPanel';
import BookTextPath from '../static/pdf/sde.pdf';
import './Book.css';
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: () => void;
pdfWrapper: HTMLDivElement | null = null;
isComponentMounted: boolean = false;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
constructor(props: any) {
super(props);
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
);
}
componentDidMount = () => {
this.isComponentMounted = true;
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
this.isComponentMounted = false;
window.removeEventListener("resize", this.setDivSizeThrottleable);
};
render = () => (
<div className="Book">
{ this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }
<div className={this.getPdfContentContainerClassName()}>
<BookCommandPanel
bookTextPath={BookTextPath}
/>
<div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
<AutoWidthPdf
file={BookTextPath}
width={this.state.pdfWidth}
onLoadSuccess={(_: any) => this.onDocumentComplete()}
/>
</div>
<BookCommandPanel
bookTextPath={BookTextPath}
/>
</div>
</div>
);
getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';
onDocumentComplete = () => {
try {
this.setState({ hidden: false });
this.setDivSizeThrottleable();
} catch (caughtError) {
console.warn({ caughtError });
}
};
}
export default Book;
AutoWidthPdf.tsx
import * as React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
interface IProps {
file: string;
width: number;
onLoadSuccess: (pdf: any) => void;
}
export class AutoWidthPdf extends React.Component<IProps> {
render = () => (
<Document
file={this.props.file}
onLoadSuccess={(_: any) => this.props.onLoadSuccess(_)}
>
<Page
pageNumber={1}
width={this.props.width}
/>
</Document>
);
}
Update 1: Cancel throttleable function (still no luck)
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: ((() => void) & Cancelable) | undefined;
pdfWrapper: HTMLDivElement | null = null;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
componentDidMount = () => {
this.setDivSizeThrottleable = throttle(
() => {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
},
500,
);
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
window.removeEventListener("resize", this.setDivSizeThrottleable!);
this.setDivSizeThrottleable!.cancel();
this.setDivSizeThrottleable = undefined;
};
render = () => (
<div className="Book">
{ this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }
<div className={this.getPdfContentContainerClassName()}>
<BookCommandPanel
BookTextPath={BookTextPath}
/>
<div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
<AutoWidthPdf
file={BookTextPath}
width={this.state.pdfWidth}
onLoadSuccess={(_: any) => this.onDocumentComplete()}
/>
</div>
<BookCommandPanel
BookTextPath={BookTextPath}
/>
</div>
</div>
);
getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';
onDocumentComplete = () => {
try {
this.setState({ hidden: false });
this.setDivSizeThrottleable!();
} catch (caughtError) {
console.warn({ caughtError });
}
};
}
export default Book;
Here is a React Hooks specific solution for
Error
Warning: Can't perform a React state update on an unmounted component.
Solution
You can declare let isMounted = true inside useEffect, which will be changed in the cleanup callback, as soon as the component is unmounted. Before state updates, you now check this variable conditionally:
useEffect(() => {
let isMounted = true; // note mutable flag
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []); // adjust dependencies to your needs
const Parent = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
Parent:
<button onClick={() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"} Child
</button>
{mounted && <Child />}
<p>
Unmount Child, while it is still loading. It won't set state later on,
so no error is triggered.
</p>
</div>
);
};
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useEffect(() => {
let isMounted = true;
fetchData();
return () => {
isMounted = false;
};
// simulate some Web API fetching
function fetchData() {
setTimeout(() => {
// drop "if (isMounted)" to trigger error again
// (take IDE, doesn't work with stack snippet)
if (isMounted) setState("data fetched")
else console.log("aborted setState on unmounted component")
}, 4000);
}
}, []);
return <div>Child: {state}</div>;
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Extension: Custom useAsync Hook
We can encapsulate all the boilerplate into a custom Hook, that automatically aborts async functions in case the component unmounts or dependency values have changed before:
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data);
});
return () => { isActive = false };
}, [asyncFn, onSuccess]);
}
// custom Hook for automatic abortion on unmount or dependency change
// You might add onFailure for promise errors as well.
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data)
else console.log("aborted setState on unmounted component")
});
return () => {
isActive = false;
};
}, [asyncFn, onSuccess]);
}
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useAsync(simulateFetchData, setState);
return <div>Child: {state}</div>;
};
const Parent = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
Parent:
<button onClick={() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"} Child
</button>
{mounted && <Child />}
<p>
Unmount Child, while it is still loading. It won't set state later on,
so no error is triggered.
</p>
</div>
);
};
const simulateFetchData = () => new Promise(
resolve => setTimeout(() => resolve("data fetched"), 4000));
ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
More on effect cleanups: Overreacted: A Complete Guide to useEffect
To remove - Can't perform a React state update on an unmounted component warning, use componentDidMount method under a condition and make false that condition on componentWillUnmount method. For example : -
class Home extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true;
ajaxVar
.get('https://domain')
.then(result => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
If above solutions dont work, try this and it works for me:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
There is a hook that's fairly common called useIsMounted that solves this problem (for functional components)...
import { useRef, useEffect } from 'react';
export function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, []);
return isMounted;
}
then in your functional component
function Book() {
const isMounted = useIsMounted();
...
useEffect(() => {
asyncOperation().then(data => {
if (isMounted.current) { setState(data); }
})
});
...
}
Checking if a component is mounted is actually an anti pattern as per React documentation. The solution to the setState warning is rather to leverage on the use of an AbortController:
useEffect(() => {
const abortController = new AbortController() // creating an AbortController
fetch(url, { signal: abortController.signal }) // passing the signal to the query
.then(data => {
setState(data) // if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return // if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort() // stop the query by aborting on the AbortController on unmount
}
}, [])
For asynchronous operations that aren't based on the Fetch API, there still should be a way to cancel these asynchronous operations, and you should rather leverage these than just checking if a component is mounted. If you are building your own API, you can implement the AbortController API in it to handle it.
For more context, the check if a component is mounted is an anti pattern as React is checking internally if the component is mounted to display that warning. Doing the same check again is just a way to hide the warning, and there are some easier ways to hide them than adding this piece of code on a big part of a codebase.
Source: https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934
I had this warning possibly because of calling setState from an effect hook (This is discussed in these 3 issues linked together).
Anyway, upgrading the react version removed the warning.
React already removed this warning
but here is a better solution (not just workaround)
useEffect(() => {
const abortController = new AbortController() // creating an AbortController
fetch(url, { signal: abortController.signal }) // passing the signal to the query
.then(data => {
setState(data) // if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return // if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort()
}
}, [])
The solution from #ford04 didn't worked to me and specially if you need to use the isMounted in multiple places (multiple useEffect for instance), it's recommended to useRef, as bellow:
Essential packages
"dependencies":
{
"react": "17.0.1",
}
"devDependencies": {
"typescript": "4.1.5",
}
My Hook Component
export const SubscriptionsView: React.FC = () => {
const [data, setData] = useState<Subscription[]>();
const isMounted = React.useRef(true);
React.useEffect(() => {
if (isMounted.current) {
// fetch data
// setData (fetch result)
return () => {
isMounted.current = false;
};
}
}
});
try changing setDivSizeThrottleable to
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
{ leading: false, trailing: true }
);
I know that you're not using history, but in my case I was using the useHistory hook from React Router DOM, which unmounts the component before the state is persisted in my React Context Provider.
To fix this problem I have used the hook withRouter nesting the component, in my case export default withRouter(Login), and inside the component const Login = props => { ...; props.history.push("/dashboard"); .... I have also removed the other props.history.push from the component, e.g, if(authorization.token) return props.history.push('/dashboard') because this causes a loop, because the authorization state.
An alternative to push a new item to history.
Add a ref to a jsx component and then check it exist
function Book() {
const ref = useRef();
useEffect(() => {
asyncOperation().then(data => {
if (ref.current) setState(data);
})
});
return <div ref={ref}>content</div>
}
I had a similar issue thanks #ford04 helped me out.
However, another error occurred.
NB. I am using ReactJS hooks
ndex.js:1 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
What causes the error?
import {useHistory} from 'react-router-dom'
const History = useHistory()
if (true) {
history.push('/new-route');
}
return (
<>
<render component />
</>
)
This could not work because despite you are redirecting to new page all state and props are being manipulated on the dom or simply rendering to the previous page did not stop.
What solution I found
import {Redirect} from 'react-router-dom'
if (true) {
return <redirect to="/new-route" />
}
return (
<>
<render component />
</>
)
If you are fetching data from axios and the error still occurs, just wrap the setter inside the condition
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);
I have 2 solutions for this error:
return:
If you are used hook and useEffect, So put a return end of useEffect.
useEffect(() => {
window.addEventListener('mousemove', logMouseMove)
return () => {
window.removeEventListener('mousemove', logMouseMove)
}
}, [])
componentWillUnmount:
If you are used componentDidMount, So put componentWillUnmount next to it.
componentDidMount() {
window.addEventListener('mousemove', this.logMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.logMouseMove)
}
The isMounted approach is an anti-pattern in most cases because it doesn't actually clean up/cancel anything, it just avoids changing state on unmounted components, but does nothing with pending asynchronous tasks. The React team recently removed the leak warning because users keep creating a lot of anti-patterns to hide the warning rather than fix its cause.
But writing cancellable code in plain JS can be really tricky. To fix this I made my own lib useAsyncEffect2 with custom hooks, built on top of a cancellable promise (c-promise2) for executing cancellable async code to reach its graceful cancellation. All async stages (promises), including deep ones, are cancellable. This means that the request here will be automatically aborted if its parent context is canceled. Of course, any other asynchronous operation can be used instead of a request.
useAsyncEffect Demo with plain useState usage (Live Demo):
import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent({url}) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
setText("fetching...");
const json = (yield cpAxios(url)).data;
setText(`Success: ${JSON.stringify(json)}`);
},
[url]
);
return (
<div>
<div>{text}</div>
<button onClick={cancel}>
Cancel request
</button>
</div>
);
}
useAsyncEffect Demo with internal states usage (Live Demo):
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent({ url, timeout }) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(url).timeout(timeout)).data;
},
{ states: true, deps: [url] }
);
return (
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
<button onClick={cancel} disabled={done}>
Cancel async effect (abort request)
</button>
</div>
);
}
Class component using decorators (Live demo)
import React, { Component } from "react";
import { ReactComponent } from "c-promise2";
import cpAxios from "cp-axios";
#ReactComponent
class TestComponent extends Component {
state = {
text: ""
};
*componentDidMount(scope) {
const { url, timeout } = this.props;
const response = yield cpAxios(url).timeout(timeout);
this.setState({ text: JSON.stringify(response.data, null, 2) });
}
render() {
return (<div>{this.state.text}</div>);
}
}
export default TestComponent;
More other examples:
Axios request with errors handling
Fetch weather by coords
Live search
Pause & Resume
Progress capturing
Edit: I just realized the warning is referencing a component called TextLayerInternal. That's likely where your bug is. The rest of this is still relevant, but it might not fix your problem.
1) Getting the instance of a component for this warning is tough. It looks like there is some discussion to improve this in React but there currently is no easy way to do it. The reason it hasn't been built yet, I suspect, is likely because components are expected to be written in such a way that setState after unmount isn't possible no matter what the state of the component is. The problem, as far as the React team is concerned, is always in the Component code and not the Component instance, which is why you get the Component Type name.
That answer might be unsatisfactory, but I think I can fix your problem.
2) Lodashes throttled function has a cancel method. Call cancel in componentWillUnmount and ditch the isComponentMounted. Canceling is more "idiomatically" React than introducing a new property.
UPDATE DO NOT USE MY ORIGINAL ANSWER AS IT DOES NOT WORK
This answer was based on the use of cancelable promises and a note in makecancelable which I migrated to use hooks. However, it appears it does not cancel a chain of async/await and even cancelable-promise does not support canceling of a chain of awaits
Doing a bit more research on this, it appears that some internal Google reasons prevented cancelable promises from coming into the standard.
Further more, there was some promise with Bluebird which introduces cancelable promises, but it does not work in Expo or at least I haven't seen an example of it working in Expo.
The accepted answer is the best. Since I use TypeScript I had adapted the code with a few modifications (I explicitly set the dependencies since the accepted answer's implicit dependencies appear to give a re-render loop on my app, added and use async/await rather than promise chain, pass a ref to the mounted object so that an async/await chain can be canceled earlier if needed)
/**
* This starts an async function and executes another function that performs
* React state changes if the component is still mounted after the async
* operation completes
* #template T
* #param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
* it has a copy of the mounted ref so an await chain can be canceled earlier.
* #param {(asyncResult: T) => void} onSuccess this gets executed after async
* function is resolved and the component is still mounted
* #param {import("react").DependencyList} deps
*/
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
(async () => {
const x = await asyncFunction(mountedRef);
if (mountedRef.current) {
onSuccess(x);
}
})();
return () => {
mountedRef.current = false;
};
}, deps);
}
Original answer
Since I have many different operations that are async, I use the cancelable-promise package to resolve this issue with minimal code changes.
Previous code:
useEffect(() =>
(async () => {
const bar = await fooAsync();
setSomeState(bar);
})(),
[]
);
New code:
import { cancelable } from "cancelable-promise";
...
useEffect(
() => {
const cancelablePromise = cancelable(async () => {
const bar = await fooAsync();
setSomeState(bar);
})
return () => cancelablePromise.cancel();
},
[]
);
You can alsowrpte it in a custom utility function like this
/**
* This wraps an async function in a cancelable promise
* #param {() => PromiseLike<void>} asyncFunction
* #param {React.DependencyList} deps
*/
export function useCancelableEffect(asyncFunction, deps) {
useEffect(() => {
const cancelablePromise = cancelable(asyncFunction());
return () => cancelablePromise.cancel();
}, deps);
}
In my case of a login-like screen, the fetch was done in a onClick handler of a parent component, who passed that handler down to the child, whom placed .catch and .finally on it.
In the .then case a redirect (and hence unmount) would happen as normal operation, and only in cases of fetch error would the child stay mounted on-screen.
My solution was moving the setState and all other code from the .finally to the .catch since the child is guaranteed to be mounted in the .catch case. And in the .then case nothing needed doing because of the guaranteed unmount.
Based on #ford04 answer, here is the same encapsulated in a method :
import React, { FC, useState, useEffect, DependencyList } from 'react';
export function useEffectAsync( effectAsyncFun : ( isMounted: () => boolean ) => unknown, deps?: DependencyList ) {
useEffect( () => {
let isMounted = true;
const _unused = effectAsyncFun( () => isMounted );
return () => { isMounted = false; };
}, deps );
}
Usage:
const MyComponent : FC<{}> = (props) => {
const [ asyncProp , setAsyncProp ] = useState( '' ) ;
useEffectAsync( async ( isMounted ) =>
{
const someAsyncProp = await ... ;
if ( isMounted() )
setAsyncProp( someAsyncProp ) ;
});
return <div> ... ;
} ;
Depending on how you open your webpage, you may not be causing a mounting. Such as using a <Link/> back to a page that was already mounted in the virtual DOM, so requiring data from a componentDidMount lifecycle is caught.
Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then they react cannot update the state. here is the example code for this. write this line before every state Update.
if(!isScreenMounted.current) return;
Here is the Complete Code
import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () => isScreenMounted.current = false
},[])
const ConvertFileSubmit = () => {
if(!isScreenMounted.current) return;
setUpLoading(true)
var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
formdata.append("file",file);
fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if(!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.log('error', error)
});
}
return(
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}
export default SearchScreen;
const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})
I solved this problem by providing all the params that are used in the useEffect hook
The code reported the bug:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId]);
The code after fix:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId,level]);
Can see that , problems solved after I provided all the params(including the level param) that supposed to pass through.
I had a similar problem and solved it :
I was automatically making the user logged-in by dispatching an action on redux
( placing authentication token on redux state )
and then I was trying to show a message with this.setState({succ_message: "...")
in my component.
Component was looking empty with the same error on console : "unmounted component".."memory leak" etc.
After I read Walter's answer up in this thread
I've noticed that in the Routing table of my application ,
my component's route wasn't valid if user is logged-in :
{!this.props.user.token &&
<div>
<Route path="/register/:type" exact component={MyComp} />
</div>
}
I made the Route visible whether the token exists or not.
In my case the issue was that the parent component was hidding the child because of a condition change in the child component.
So what I did was to change the condition so the child component was always shown.
What was happening:
const ParentComponent:FC = () => {
...
if (someCondition) {
return null;
}
return (
<>
Some cool text here
<ChildModalComponent message="this is a cool modal" />
</>
)
}
const ChildModalComponent: FC = () => {
...
const handleSubmit = () => {
setSomeCondition(true);
}
}
So after clicking submit the modal was automatically hidden becasue of the parent condition (someCondition).
How did I fix it?
I changed the place where the someCondition was checked in the Parent component, so the child component was always shown:
const ParentComponent:FC = () => {
...
return (
<>
{!someCondition && <>Some cool text here</>
<ChildModalComponent message="this is a cool modal" />
</>
)
}
I faced same warning, not it is fixed. To fix the issue, I removed the useRef() variable check in useEffect()
Earlier, the code was
const varRef = useRef();
useEffect(() => {
if (!varRef.current)
{
}
}, []);
Now, the code is
const varRef = useRef();
useEffect(() => {
//if (!varRef.current)
{
}
}, [])
Hope, it helps...
Inspired by the accepted answer by #ford04 I had even better approach dealing with it, instead of using useEffect inside useAsync create a new function that returns a callback for componentWillUnmount :
function asyncRequest(asyncRequest, onSuccess, onError, onComplete) {
let isMounted=true
asyncRequest().then((data => isMounted ? onSuccess(data):null)).catch(onError).finally(onComplete)
return () => {isMounted=false}
}
...
useEffect(()=>{
return asyncRequest(()=>someAsyncTask(arg), response=> {
setSomeState(response)
},onError, onComplete)
},[])
const handleClick = async (item: NavheadersType, index: number) => {
const newNavHeaders = [...navheaders];
if (item.url) {
await router.push(item.url); =>>>> line causing error (causing route to happen)
// router.push(item.url); =>>> coreect line
newNavHeaders.forEach((item) => (item.active = false));
newNavHeaders[index].active = true;
setnavheaders([...newNavHeaders]);
}
};
The simplest and most compact solution (with an explanation) is seen below as a one-liner solution.
useEffect(() => { return () => {}; }, []);
The useEffect() example above returns a callback function triggers React to finish its unmount portion of its life-cycle prior to updating state.
That very simplistic solution is all that is needed. In addition, it also works unlike the fictional syntax provided by #ford04 and #sfletche . By the way, the below code snippet from #ford04 is purely imaginary syntax (#sfletche , #vinod , #guneetgstar , and #Drew Cordano used the very same imaginary syntax).
data => {       <--- Fictional/Imaginary Syntax
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
All of my linters and all the linters of my entire team will not accept it and they report Uncaught SyntaxError: unexpected token: '=>'. I am surprised that no one caught the imaginary syntax. Would anyone who has participated in this question-thread, particularly among the up-voters, explain to me how they got the solutions to work for them?
Inspired by #ford04 answer I use this hook, which also takes callbacks for success, errors, finally and an abortFn:
export const useAsync = (
asyncFn,
onSuccess = false,
onError = false,
onFinally = false,
abortFn = false
) => {
useEffect(() => {
let isMounted = true;
const run = async () => {
try{
let data = await asyncFn()
if (isMounted && onSuccess) onSuccess(data)
} catch(error) {
if (isMounted && onError) onSuccess(error)
} finally {
if (isMounted && onFinally) onFinally()
}
}
run()
return () => {
if(abortFn) abortFn()
isMounted = false
};
}, [asyncFn, onSuccess])
}
If the asyncFn is doing some kind of fetch from back-end it often makes sense to abort it when the component is unmounted (not always though, sometimes if ie. you're loading some data into a store you might as well just want to finish it even if component is unmounted)

Why eventListener re-render React Component

I am creating a stopwatch in React.js and i am wondering why window.addEventListener('keydown', callback) re-render my component?
import { useEffect, useState } from 'react';
import './App.scss';
import Timer from './Timer';
import Button from './Button';
import Time from './Time';
const App = () => {
const [isRunning, setIsRunning] = useState(false);
const [start, setStart] = useState(new Time(0));
const [stop, setStop] = useState(new Time(0));
const handleStart = () => {
const now = new Date();
setIsRunning(true);
setStart(new Time(now));
setStop(new Time(now));
};
const handleStop = () => {
setIsRunning(false);
setStop(new Time(new Date()));
};
const getTime = () => {
if (isRunning) {
return new Time(new Date().getTime() - start.origin);
} else {
return new Time(stop.origin - start.origin);
}
};
const handleKeyDown = (key) => {
console.log(key.code === 'Space');
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div className="stopwatch">
<Timer getTime={getTime} />
<div className="buttons">
<Button role={'start'} callback={handleStart} />
<Button role={'stop'} callback={handleStop} />
</div>
</div>
);
};
export default App;
When i click start and then stop after let's say 3s. <Timer /> show correctly time that has passed, but then when i press Space on keyboard <Timer /> is re-rendering, showing new time. Then, when i switch my web-browser to VSCode and again to web-browser, <Timer /> isn't re-rendering
Here is my Timer component
import { memo, useEffect, useRef } from 'react';
const Timer = ({ getTime }) => {
const timer = useRef();
console.log('timer rendered');
useEffect(() => {
function run() {
const time = getTime().formatted();
timer.current.textContent = `${time.m}:${time.s}.${time.ms}`;
requestAnimationFrame(run);
}
run();
return () => {
cancelAnimationFrame(run);
};
});
return <div ref={timer} className="timer"></div>;
};
export default memo(Timer);
no matter if I use [] in both or none of useEffect nothing changes.
As #davood-falahati says, adding an empty array as a second argument to useEffect would probably be desirable. From the docs:
... If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works. ...
In your use case:
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);

Trying to change a state in React Hooks with Interval

I have a problem trying to change a state in react using setInterval.
Why does the function "alert" always show a "null" message?
import React, { useEffect, useState } from 'react';
const Playlist = (props) => {
const [test, setTest] = useState("");
useEffect(() => {
setInterval(load, 3000)
}, []);
const load = async () => {
alert(test)
setTest("TEST");
}
return (
<div>
{test}
</div>
)
}
export default Playlist
Issue :
is callback passed into setInterval's closure and it accesses the test variable in the first render.
Solution :
You can clearInterval and setInterval as soon as there is any changes in your state
const { useState , useEffect , useCallback } = React;
const App = () => {
const [test, setTest] = useState("");
useEffect(() => {
const intrvl = setInterval(load, 3000);
return () => clearInterval(intrvl);
}, [test]);
const load = () => {
alert(test)
setTest("TEST");
}
return (
<div>
{test}
</div>
)
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
The problem is that useEffect captures the test from the first render which is equal to empty string. We never re-apply the effect so the closure in setInterval always references the test from the first render.
NOTE: setInterval does not describe a process in time — once you set the interval, you can’t change anything about it except clearing it.
When we pass an empty [] array of dependencies to useEffect, it only runs the useEffect on mount and cleanup on unmount of the component.
One way is to add test to the array of dependencies so anytime there is an update on test, the useEffect will be called.
useEffect(() => {
const intervalId = setInterval(load, 3000);
return () => clearInterval(intervalId);
}, [test]);
But since we are using the test inside useEffect, we can just remove the empty [] array of dependencies so the useEffect will be called every time we are setting the state (component is re-rendered) and the new setInterval will be created with accessing to the latest test state like below
useEffect(() => {
const intervalId = setInterval(load, 3000);
return () => clearInterval(intervalId);
});
Another possible way is to update the state inside the interval's closure like
useEffect(() => {
const id = setInterval(() => {
setTest("TEST");
}, 3000);
return () => clearInterval(id);
});
const App = () => {
const [test, setTest] = useState("");
useEffect(() => {
const intervalId = setInterval(load, 3000);
return () => clearInterval(intervalId);
});
const load = () => {
alert(test)
setTest("TEST");
}
return (
<div>
{test}
</div>
)
}
Example of interval with setting count so you can see the state change

How to access state when component unmount with React Hooks?

With regular React it's possible to have something like this:
class NoteEditor extends React.PureComponent {
constructor() {
super();
this.state = {
noteId: 123,
};
}
componentWillUnmount() {
logger('This note has been closed: ' + this.state.noteId);
}
// ... more code to load and save note
}
In React Hooks, one could write this:
function NoteEditor {
const [noteId, setNoteId] = useState(123);
useEffect(() => {
return () => {
logger('This note has been closed: ' + noteId); // bug!!
}
}, [])
return '...';
}
What's returned from useEffect will be executed only once before the component unmount, however the state (as in the code above) would be stale.
A solution would be to pass noteId as a dependency, but then the effect would run on every render, not just once. Or to use a reference, but this is very hard to maintain.
So is there any recommended pattern to implement this using React Hook?
With regular React, it's possible to access the state from anywhere in the component, but with hooks it seems there are only convoluted ways, each with serious drawbacks, or maybe I'm just missing something.
Any suggestion?
useRef() to the rescue.
Since the ref is mutable and exists for the lifetime of the component, we can use it to store the current value whenever it is updated and still access that value in the cleanup function of our useEffect via the ref's value .current property.
So there will be an additional useEffect() to keep the ref's value updated whenever the state changes.
Sample snippet
const [value, setValue] = useState();
const valueRef = useRef();
useEffect(() => {
valueRef.current = value;
}, [value]);
useEffect(() => {
return function cleanup() {
console.log(valueRef.current);
};
}, []);
Thanks to the author of https://www.timveletta.com/blog/2020-07-14-accessing-react-state-in-your-component-cleanup-with-hooks/. Please refer this link for deep diving.
useState() is a specialized form of useReducer(), so you can substitute a full reducer to get the current state and get around the closure problem.
NoteEditor
import React, { useEffect, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "set":
return action.payload;
case "unMount":
console.log("This note has been closed: " + state); // This note has been closed: 201
break;
default:
throw new Error();
}
}
function NoteEditor({ initialNoteId }) {
const [noteId, dispatch] = useReducer(reducer, initialNoteId);
useEffect(function logBeforeUnMount() {
return () => dispatch({ type: "unMount" });
}, []);
useEffect(function changeIdSideEffect() {
setTimeout(() => {
dispatch({ type: "set", payload: noteId + 1 });
}, 1000);
}, []);
return <div>{noteId}</div>;
}
export default NoteEditor;
App
import React, { useState, useEffect } from "react";
import "./styles.css";
import NoteEditor from "./note-editor";
export default function App() {
const [notes, setNotes] = useState([100, 200, 300]);
useEffect(function removeNote() {
setTimeout(() => {
setNotes([100, 300]);
}, 2000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{notes.map(note => (
<NoteEditor key={`Note${note}`} initialNoteId={note} />
))}
</div>
);
}
I wanted to chime in with an answer for this in case someone else runs into this. If you need more than one value in your useEffect unmount function, it's important to make sure that the correct dependencies are being used. So the accepted answer works fine because it's just one dependency, but start including more dependencies, and it gets complicated. The amount of useRef's you need get out of hand. So instead, what you can do is a useRef that is the unmount function itself, and call that when you unmount the component:
import React, { useRef, useState, useContext, useCallback, useEffect } from 'react';
import { Heading, Input } from '../components';
import { AppContext } from '../../AppContext';
export const TitleSection: React.FC = ({ thing }) => {
const { updateThing } = useContext(AppContext);
const [name, setName] = useState(thing.name);
const timer = useRef(null);
const onUnmount = useRef();
const handleChangeName = useCallback((event) => {
setName(event.target.value);
timer.current !== null && clearTimeout(timer.current);
timer.current = setTimeout(() => {
updateThing({
name: name || ''
});
timer.current = null;
}, 1000);
}, [name, updateThing]);
useEffect(() => {
onUnmount.current = () => {
if (thing?.name !== name) {
timer.current !== null && clearTimeout(timer.current);
updateThing({
name: name || '',
});
timer.current = null;
}
};
}, [thing?.name, name, updateThing]);
useEffect(() => {
return () => {
onUnmount.current?.();
};
}, []);
return (
<>
<Heading as="h1" fontSize="md" style={{ marginBottom: 5 }}>
Name
</Heading>
<Input
placeholder='Grab eggs from the store...'
value={name}
onChange={handleChangeName}
variant='white'
/>
</>
);
};

Categories

Resources