-
-
Save jaydenseric/a67cfb1b809b1b789daa17dfe6f83daa to your computer and use it in GitHub Desktop.
import React from 'react' | |
export const useIsMounted = () => { | |
const ref = React.useRef(false) | |
const [, setIsMounted] = React.useState(false) | |
React.useEffect(() => { | |
ref.current = true | |
setIsMounted(true) | |
return () => (ref.current = false) | |
}, []) | |
return () => ref.current | |
} |
@timoxley I have since found ways to avoid having to use a useIsMounted
hook, but from my rusty memory, the state is updated arbitrarily in the useEffect
callback to trigger a re-render? I worked through a lot of gotchas to get it going.
Use this one instead: https://github.com/jmlweb/isMounted
It doesn't use state for this, which is good.
yarn add ismounted
Sorry, please expalain for me why use ref above.
i coded like this. it work welllll.
import React, { useState, useEffect } from "react";
export default () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
console.log("Run o day");
});
useEffect(() => {
console.log("Run");
}, []);
return () => mounted;
};
Thank so much
if you write like me, when your component didmount setState will re-render for set new state, so using ref. Thank you
A better way and with explanation of how useEffect
works:
export function useIsMounted(): { current: boolean } {
// component is certainly mounted from the beginning
const componentIsMounted = useRef(true)
useEffect(() => {
// when non-SSR + (ComponentDidMount or ComponentDidUpdate):
// do nothing.
// when non-SSR + ComponentWillUnmount:
return () => { componentIsMounted.current = false }
}, [])
return componentIsMounted
}
And if you want it short =]
export function useIsMounted(): { current: boolean } {
const componentIsMounted = useRef(true)
useEffect(() => () => { componentIsMounted.current = false }, [])
return componentIsMounted
}
@ivawzh "Mounted" usually refers to the component being rendered to the DOM in a browser. I can see how your hook could be useful though, maybe it would be better named something like useDidUnMount
, and invert the boolean it returns.
"Mounted" usually refers to the component being rendered to the DOM in a browser
@jaydenseric I think you have misunderstood something. My version of useIsMounted
will return the same result of yours. Which is exactly what you said. I.e.
const isMounted = useIsMounted()
isMounted.current
// return true when the component is still mounted.
// return false when the component is unmounted.
Comparing to your original code, my version is different at
- no redundant state
const [, setIsMounted] = React.useState(false)
. - no redundant mutation at
ComponentDidMount
andComponentDidUpdate
events, as yourref.current = true
.
My version of useIsMounted will return the same result of yours.
I don't think so?
- Mine returns a function (returning boolean), yours returns a ref.
- Doesn't yours result in
true
during SSR? Mine doesn't.
I came up with his hook over a year ago, so I can't remember all the gotchas properly - I faintly recall there were a few a lot of the published useIsMounted
hooks didn't cover. In the time since, I have managed to completely avoid using this hook. I think I have used a similar pattern to your hook in the implementation of more complex hooks before.
Mine returns a function (returning boolean), yours returns a ref.
Right, I missed that part. But that's just a minor interface difference. Yours is a function that a user will call ()
and then return ref.current
; mine is returning ref
, and then the user will call .current
which also returns ref.current
.
Doesn't yours result in true during SSR? Mine doesn't.
Yes. But I believe that's the right way to be. Component is actually mounted during SSR, it's just the didMount
event is not fired until client-side React.hydrate()
. E.g. if you have async data fetch in a component and consider isMounted
as false
during SSR, so that skip setting state after that async fetch, then the first page returned from SSR will not be complete. And that data from async fetch will not be SEO-able.
Returning a function will trigger effects on each re-render if you're using it like this:
const isMounted = useIsMounted()
useEffect(() => {
...will be triggered on each re-render...
}, [isMounted])
Instead of returning a new instance of function each time, memoize it:
import { useCallback, useEffect, useRef } from 'react'
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const checker = useCallback(() => {
return isMounted.current;
}, []);
return checker;
};
const isMounted = useIsMounted();
useEffect(() => {
fetch(...).then({
if (isMounted()) { ... }
})
}, [isMounted])
I'm curious why React wouldn't do this in useState
itself?
@geoguide really trying to understand this myself.
Do you guys have the unit test of the short way as well :)?
Use this one instead: https://github.com/jmlweb/isMounted
It doesn't use state for this, which is good.
yarn add ismounted
Thanks the trick was to return the ref itself not ref.current that way it doesn't need state.
I'm curious why React wouldn't do this in
useState
itself?
Well the warning doesn't mention where it happens, it just says it happened which means they don't know where this problem occurs to solve it, probably useState is not aware at all on which component it is, to see if it was unmounted not run it, just saying I'm not sure though.
I hope I'm not too late to the party. This is working fine for me so far.
import { useCallback, useEffect, useRef } from 'react';
export function useIsMounted(): { current: boolean } {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const mounted = useCallback(() => {
return isMounted;
}, []);
return mounted();
}
import { useState, useEffect } from 'react';
import { useIsMounted } from '@/helper/useIsMounted';
export default function TestIsMounted () {
const [state, stateSet] = useState(false);
const isMounted = useIsMounted();
useEffect(() => {
isMounted.current && stateSet(true);
}, [state])
...
return (...)
}
why don’t return isMount.current? When you use the hook, what you want is just true or false. Does return isMount.current break the hook?
Hi Daveteu,
You are correct. I tried returning isMount.current and got it to work. Thank you.
export default function useIsMounted(): boolean {
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const mounted = useCallback(() => {
return isMounted.current;
}, []);
return isMounted()
}
const isMounted = useIsMounted();
useEffect(() => {
isMounted && stateSet(true);
}, [state])
What's the point of the setState
part? I think the isMount
hook itself is sufficient.
Think about it, if you don't change the state you will not be able to for instance use useEffect on the result of that hook, basically a rerender won't occur, of course you can reach the value if you want to and it is changed but you can't monitor the changes.
Normally you don't use the result from the hook as a dependency of a useEffect. You only use it for check.
E.g.
useEffect(() => {
if (isMounted) {
do something.
}
},[props])
Just be aware that what you want from a useIsMounted
hook is not for it to return a boolean, but to return a way to check on demand the mounted status. For example, maybe you awaited a loading promise and want to update some state, but want to first check the component is still mounted before doing so to avoid a React set state on unmounted component error. If you read a simple boolean that was created at the same render that started the loading side effect, it will be stale. The component may be unmounted because that boolean is out of date.
I don't actually have a need for a useIsMounted
hook anymore, because I use a useForceUpdate
pattern instead, for example: https://github.com/jaydenseric/graphql-react/blob/f7d3a17edb99dc3ffabe8598151df8bf942e2258/useLoadingEntry.mjs#L22
@daveteu You've got a point there, I don't remember why but I needed it in cases like that dependency, it should work without setState for you though.
@tamvo22 @daveteu It's not correct to return isMounted()
from hook because it will reflect the mounted state at the time of hook call, not when async function finishes. I.e. the hook needs to return a 'checker function', not result of the function.
But the function needs to be memoized, otherwise when it's added to useEffect dependencies (eslint and it's autofix will suggest doing this), it will trigger effect recalculation on each re-render. So I think this implementation is correct: https://gist.github.com/jaydenseric/a67cfb1b809b1b789daa17dfe6f83daa?permalink_comment_id=3401782#gistcomment-3401782
@timoxley I have since found ways to avoid having to use a
useIsMounted
hook, but from my rusty memory, the state is updated arbitrarily in theuseEffect
callback to trigger a re-render? I worked through a lot of gotchas to get it going.
The state update is in fact an instance of the forceUpdate
pattern.
@zmeyc Thank you for the clarification! I tested with a delayed API calls and found that you are right. As you pointed out, it only reflected the initial mount status hook call. All this time, I was under the assumption that isMounted would return the useRef value at the time of the API request. When I pass the function isMounted() to call during the API return, it does provide the correct mounted value when my test component mounted and unmounted. Thanks again cheers!
One more option to the initial value being true or false debate:
const useIsMounted = (initialValue = true) => {
const ref = useRef(initialValue)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
return ref
}
Let that value be passed in. If true
then it behaves like its mounted initially and only changes to false
when it's unmounted. Useful to stop yourself from setting state when an async action is done after a component was unmounted. But if false
then it's treated like the original solution where it still flips to false
when unmounted but also flips to true
when ready in the client.
@jaydenseric why
setIsMounted(true)