I was learning react and came across the concept of useEffect. So I was trying useEffects with resize event listeners and when doing so even after applying the square bracket which should be used only to run the useEffect once, I am repeatedly getting the updated size of the screen when trying the change the browser size.
below is the code, could some one please let me know if I have some code issue or is this how it is supposed to work?
import React, { useState, useEffect } from "react";
// cleanup function
// second argument
const UseEffectCleanup = () => {
const [size, setsize] = useState(window.innerWidth);
const checkSize = () => {
console.log("check size");
setsize(window.innerWidth);
};
useEffect(() => {
console.log("use effect");
window.addEventListener("resize", checkSize);
return () => {
console.log("Cleanup");
window.removeEventListener("resize", checkSize);
};
}, []);
return (
<>
<h2>Windows Width</h2>
<h2>{size}</h2>
</>
);
};
export default UseEffectCleanup;
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
I am facing an abnormal output on the browser from React while using useEffect hook.
I would request you to please have a look at the code. You can copy and paste the code on any online IDE that supports React to visualize the behavior on the browser.
I want the counter to increment after every 1 second. But with the code it stucks after 10.
import { useState, useEffect } from "react";
function App() {
const initialState = 0;
const [count, setCount] = useState(initialState);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
// return () => {
// clearInterval(interval);
// };
}, [count]);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
export default App;
I want to know the reason for that. Why is it happening?
But when I do cleanup with useEffect to do componentWillUnmoint() it behaves normal and renders the counter every second properly. I have intentionally comment cleanup part of code useEffect.
You are adding an interval on every render, soon enough, your thread will be overloaded with intervals.
I guess you wanted to run a single interval, its done by removing the closure on count by passing a function to state setter ("functional update"):
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
export default App;
I am new in react. can anyone explain why the loading is not updating its value. on console the loading is 1
import React, { useState, useEffect } from "react";
function App() {
useEffect(() => {
hai();
}, []);
const [loading, setLoading] = useState(1);
const hai = () => {
console.log("............");
setLoading(2);
console.log(loading);
};
return <></>;
}
export default App;
Also if there are two state variables, and rearrange the set function, whole application breaks
const [loading, setLoading]=useState(false)
const [datas, setDatas]=useState([])
//works fine if loading is set second
const hai = () => {
setDatas(res) //fetched from external api
setLoading(true)
};
//throws error in applicaton
const hai = () => {
setLoading(true)
setDatas(res) //fetched from external api
};
console.log(datas)
You are testing your loading value in a wrong way,this is how you should be doing it:
useEffect(() => {
console.log(loading);
}, [loading]);
and remove console.log(loading) from the function hai
Whenever you want to access an updated value of some variable then put it inside an useEffect and put the value which you want to check inside the useEffect's dependency array.
I am extracting a custom hook with a onRes parameter;
function useApi(onRes) {
useEffect(() => {
api().then((res) => {
onRes && onRes(res);
});
}, [onRes]);
}
to use this hook:
import useApi from './useApi';
function App() {
const [x, setX] = useState(0);
useApi({
onRes: () => {}
})
return (
<div onClick={() => setX(Math.random())}>{x}</div>
)
}
notice that every time <App/> renders, onRes will change, and the useApi hooks will run again
my question is should wrap onRes with useCallback ? or I just inform the hook users to be careful with this onRes parameter ?
function useApi(onRes) {
const onResCb = useCallback(onRes, []); // should I do this ?
useEffect(() => {
api().then((res) => {
onResCb && onResCb(res);
});
}, [onResCb]);
}
Just remove onRes from the dependencies array of useEffect, this will make sure the effect will run only on mount
function useApi(onRes) {
useEffect(() => {
api().then((res) => {
onRes && onRes(res);
});
}, []);
}
second option, define the function that you pass in with useCallback with an empty dependencies array and pass it to useApi and keep your current hook the same
const onRes = useCallback(() => {
console.log('hi')
}, []);
useApi(onRes);
This blog post might be useful: When to useMemo and useCallback
I would think that using callback is not relevant here since you're not doing heavy computation. useCallback would actually be less performant than using the function as-is.
Besides, you can save an API call if onRes is undefined.
function useApi(onRes) {
useEffect(() => {
if(! onRes){
return
}
api().then(onRes);
}, [onRes]);
}
after reading this article,Refs to the Rescue!, I finally get inspired.
we can store onRes with a ref, and call it when needed.
function useApi(onRes) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = onRes;
});
useEffect(() => {
api().then((res) => {
savedCallback.current && savedCallback.current(res);
});
}, []);
}