React use-effect [duplicate] - reactjs

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.

Related

Unsubscribe from listener in hook or in screen component?

I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.

Why React detect the function reference as new?

I have theorical question about custom hooks and use effect when redux is involved.
Let`s assume I have this code:
//MyComponent.ts
import * as React from 'react';
import { connect } from 'react-redux';
const MyComponentBase = ({fetchData, data}) => {
React.useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data?.name}</data>
}
const mapStateToProps= state => {
return {
data: dataSelectors.data(state)
}
}
const mapDispatchToProps= {
fetchData: dataActions.fetchData
}
export const MyComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponentBase);
This works as expected, when the component renders it does an async request to the server to fetch the data (using redux-thunk). It initializes the state in the reduces, and rerender the component.
However we are in the middle of a migration to move this code to hooks. Se we refactor this code a little bit:
//MyHook.ts
import { useDispatch, useSelector } from 'react-redux';
import {fetchDataAction} from './actions.ts';
const dataState = (state) => state.data;
export const useDataSelectors = () => {
return useSelector(dataState);
}
export const useDataActions = () => {
const dispatch = useDispatch();
return {
fetchData: () => dispatch(fetchDataAction)
};
};
//MyComponent.ts
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, [fetchData]);
return <div>{data?.name}</data>
}
With this change the component enters in an infite loop. When it renders for the first time, it fetches data. When the data arrives, it updates the store and rerender the component. However in this rerender, the useEffect says that the reference for fetchData has changed, and does the fetch again, causing an infinite loop.
But I don't understand why the reference it's different, that hooks are defined outside the scope of the component, they are not removed from the dom or whateverm so their references should keep the same on each render cycle. Any ideas?
useDataActions is a hook, but it is returning a new object instance all the time
return {
fetchData: () => dispatch(fetchDataAction)
};
Even though fetchData is most likely the same object, you are wrapping it in a new object.
You could useState or useMemo to handle this.
export const useDataActions = () => {
const dispatch = useDispatch();
const [dataActions, setDataActions] = useState({})
useEffect(() => {
setDataActions({
fetchData: () => dispatch(fetchDataAction)
})
}, [dispatch]);
return dataActions;
};
first of all if you want the problem goes away you have a few options:
make your fetchData function memoized using useCallback hook
don't use fetchData in your useEffect dependencies because you don't want it. you only need to call fetchData when the component mounts.
so here is the above changes:
1
export const useDataActions = () => {
const dispatch = useDispatch();
const fetchData = useCallback(() => dispatch(fetchDataAction), []);
return {
fetchData
};
};
the 2nd approach is:
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, []);
return <div>{data?.name}</data>
}

Custom hook fires every render

I want to run a callback when a component mounts, but not on each render. Using useEffect and an empty dependency array created warnings. I read on a different thread which I now can't find that this can be achieved by creating a custom hook:
import {useRef} from "react";
const useInitialise = (callback) => {
//TODO this is running every render!
const runOnce = useRef(false);
if (!runOnce.current) {
callback();
runOnce.current = true;
}
}
export default useInitialise;
Usage:
useInitialise(() => {
//callback
});
This is instead of using:
useEffect(() => {
//callback
}, []);
As this generates warnings. I understand that the hook will be called on each render, but why is the runOnce.current not preventing the callback being run twice?
Found the thread!
I should use a state to track mounted/unmounted, then useEffect to call the callback only if either mounted or callback change:
import {useEffect, useState} from "react";
const useInitialise = (callback) => {
const [mounted, setMounted] = useState(false)
useEffect(() => {
if (!mounted) {
setMounted(true);
callback();
}
}, [mounted, callback]);
}
export default useInitialise;

Why is React useEffect clean-up function being run right after the useEffect callback, and then never again?

I have a small React app with a component that has a button that opens a small menu, and I'd like it to close the menu when the user clicks anywhere outside the component.
function setupDocumentClickEffect(onClick = (() => {})) {
console.log('Add doc click');
document.addEventListener('click', onClick);
return () => { // Clean-up
console.log('Remove doc click');
document.removeEventListener('click', onClick);
};
}
function MyComponent() {
const [open, setOpen] = useState(false);
// Set up an effect that will close the component if clicking on the document outside the component
if (open) {
const close = () => { setOpen(false); };
useEffect(setupDocumentClickEffect(close), [open]);
}
const stopProp = (event) => { event.stopPropagation(); };
const toggleOpen = () => { setOpen(!open); };
// ...
// returns an html interface that calls stopProp if clicked on the component itself,
// or toggleOpen if clicked on a specific button.
}
When the component is first opened, it will run both the callback and the cleanup immediately. Console will show: Add doc click and Remove doc click. If the component is closed and then re-opened, it acts as expected with just Add doc click, not running clean-up... but then clean-up is never run again.
I suspect I'll have to re-structure this so it doesn't use if (open), and instead runs useEffect each time? But I'm not sure why the clean-up runs the way it does.
A few things are wrong here. The first argument to a useEffect should be a callback function, which you're returning from setupDocumentClickEffect, this means that the return value of setupDocumentClickEffect(close) will just be run immediately on mount, and never again.
It should look more like this:
useEffect(() => {
if (!open) {
return;
}
console.log('Add doc click');
document.addEventListener('click', close);
return () => { // Clean-up
console.log('Remove doc click');
document.removeEventListener('click', close);
};
}, [open]);
The other thing that is wrong here is that you are breaking the rules of hooks: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
You should not define a hook in a conditional.
EDIT
To elaborate on what is happening in your current useEffect, it basically boils down to if you wrote something like this:
if (open) {
const close = () => { setOpen(false); };
console.log('Add doc click');
document.addEventListener('click', close);
useEffect(() => {
console.log('Remove doc click');
document.removeEventListener('click', close);
}, [open]);
}
So you would want to throw that function inside of the useEffect() hook and avail yourself of useRef like so:
import React, { useEffect, useState, useRef } from 'react';
const MyComponent = ({ options, selected }) => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const setupDocumentClickEffect = (event) => {
// this if conditional logic assumes React v17
if (ref.current && ref.current.contains(event.target)) {
return;
}
setOpen(false);
};
document.body.addEventListener('click', setupDocumentClickEffect);
return () => {
document.body.removeEventListener('click', setupDocumentClickEffect);
};
}, []);
}
So since it's a menu, I imagine you build your list via a map() function somewhere that in this example, I am calling options which is why you see it passed as props in your MyComponent and you want to render that list of options from the menu:
import React, { useEffect, useState, useRef } from 'react';
const MyComponent = ({ label, options, selected, onSelectedChange }) => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const setupDocumentClickEffect = (event) => {
// this if conditional logic assumes React v17
if (ref.current && ref.current.contains(event.target)) {
return;
}
setOpen(false);
};
document.body.addEventListener('click', setupDocumentClickEffect);
return () => {
document.body.removeEventListener('click', setupDocumentClickEffect);
};
}, []);
const renderedOptions = options.map((option) => {
if (option.value === selected.value) {
return null;
}
return (
<div
key={option.value}
className="item"
onClick={() => {
onSelectedChange(option);
}}
>
{option.label}
</div>
);
});
return (
<div ref={ref} className="ui form">
// the rest of your JSX code here including
// renderedOptions below
{renderedOptions}
</div>
);
};
export default MyComponent;
So I added some props to your MyComponent and also showed you how to implement that useRef which will be important in pulling this off as well.
I suspect it's because you're calling setupDocumentClickEffect(close) immediately inside of useEffect(). Using a deferred call like useEffect(() => setupDocumentClickEffect(close), []) is what you want.
It might not break the useEffect hook, but it would be better practice to incorporate your if(open) within setupDocumentClickEffect() instead of wrapping your hook in it.

React-hooks. Can't perform a React state update on an unmounted component

I get this error:
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 a useEffect cleanup
function.
when fetching of data is started and component was unmounted, but function is trying to update state of unmounted component.
What is the best way to solve this?
CodePen example.
default function Test() {
const [notSeenAmount, setNotSeenAmount] = useState(false)
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [])
async function updateNotSeenAmount() {
let data // here i fetch data
setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
}
async function anotherFunction() {
updateNotSeenAmount() //it can trigger update too
}
return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
The easiest solution is to use a local variable that keeps track of whether the component is mounted or not. This is a common pattern with the class based approach. Here is an example that implement it with hooks:
function Example() {
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
let isCancelled = false;
simulateSlowNetworkRequest().then(() => {
if (!isCancelled) {
setText("done!");
}
});
return () => {
isCancelled = true;
};
}, []);
return <h2>{text}</h2>;
}
Here is an alternative with useRef (see below). Note that with a list of dependencies this solution won't work. The value of the ref will stay true after the first render. In that case the first solution is more appropriate.
function Example() {
const isCancelled = React.useRef(false);
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
fetch();
return () => {
isCancelled.current = true;
};
}, []);
function fetch() {
simulateSlowNetworkRequest().then(() => {
if (!isCancelled.current) {
setText("done!");
}
});
}
return <h2>{text}</h2>;
}
You can find more information about this pattern inside this article. Here is an issue inside the React project on GitHub that showcase this solution.
If you are fetching data from axios(using hooks) 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;
};
}, []);
TL;DR
Here is a CodeSandBox example
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.
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 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 Complete Example
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"
}
})
This answer is not related to the specific question but I got the same 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 a useEffect cleanup function. and as a React newcomer could not find a solution to it.
My problem was related to useState in an unmounted component.
I noticed that I was calling a set state function (setIsLoading) after the function that unmounted my component:
const Login = () => {
const [isLoading, setIsLoading] = useState(false);
const handleLogin = () => {
setIsLoading(true);
firebase.auth().then(
functionToUnMountLoginSection();
// the problem is here
setIsLoading(false);
)
}
}
The correct way is to call setIsLoading when the component is still mounted, before calling the function to unmount/process user login in my specific case:
firebase.auth().then(
setIsLoading(false);
functionToUnMountLoginSection();
)
You add the state related datas into the useEffect body for not rerunning them every rerendering process. This method will solve the problem.
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [notSeenAmount])
REF: Tip: Optimizing Performance by Skipping Effects
Custom Hook Solution (ReactJs/NextJs)
Create a new folder named 'shared' and add two folders named 'hooks', 'utils' in it. Add a new file called 'commonFunctions.js' inside utils folder and add the code snippet below.
export const promisify = (fn) => {
return new Promise((resolve, reject) => {
fn
.then(response => resolve(response))
.catch(error => reject(error));
});
};
Add a new file called 'fetch-hook.js' inside hooks folder and add the code snippet below.
import { useCallback, useEffect, useRef } from "react";
import { promisify } from "../utils/commonFunctions";
export const useFetch = () => {
const isUnmounted = useRef(false);
useEffect(() => {
isUnmounted.current = false;
return () => {
isUnmounted.current = true;
};
}, []);
const call = useCallback((fn, onSuccess, onError = null) => {
promisify(fn).then(response => {
console.group('useFetch Hook response', response);
if (!isUnmounted.current) {
console.log('updating state..');
onSuccess(response.data);
}
else
console.log('aborted state update!');
console.groupEnd();
}).catch(error => {
console.log("useFetch Hook error", error);
if (!isUnmounted.current)
if (onError)
onError(error);
});
}, []);
return { call }
};
Folder Structure
Our custom hook is now ready. We use it in our component like below
const OurComponent = (props) => {
//..
const [subscriptions, setSubscriptions] = useState<any>([]);
//..
const { call } = useFetch();
// example method, change with your own
const getSubscriptions = useCallback(async () => {
call(
payment.companySubscriptions(userId), // example api call, change with your own
(data) => setSubscriptions(data),
);
}, [userId]);
//..
const updateSubscriptions = useCallback(async () => {
setTimeout(async () => {
await getSubscriptions();
}, 5000);// 5 seconds delay
}, [getSubscriptions]);
//..
}
In our component, we call 'updateSubscriptions' method. It will trigger 'getSubscriptions' method in which we used our custom hook. If we try to navigate to a different page after calling updateSubscriptions method before 5 seconds over, our custom hook will abort state update and prevent that warning on the title of this question
Wanna see opposite?
Change 'getSubscriptions' method with the one below
const getSubscriptions = useCallback(async () => {
const response = await payment.companySubscriptions(userId);
setSubscriptions(response);
}, [userId]);
Now try to call 'updateSubscriptions' method and navigate to a different page before 5 seconds over
Try this custom hook:
import { useEffect, useRef } from 'react';
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
function Example() {
const isMounted = useIsMounted();
const [text, setText] = useState();
const safeSetState = useCallback((callback, ...args) => {
if (isMounted.current) {
callback(...args);
}
}, []);
useEffect(() => {
safeSetState(setText, 'Hello')
});
}, []);
return <h2>{text}</h2>;
}

Resources