React.useState with animation and useEffect - javascript

I am trying to implement animation using a wrapper component using useState and useEffect.
If a certain value on the props change I would like that to trigger the start of the animation:
const propVal = getter(props);
const mounted = useRef(true);
useEffect(() => {
//code to initialize and start animation removed
//because a started animation uses requestAnimationFrame
//a component may want to set state for animation while not
//mounted anymore, prevent the error by setting mounted to false
return () => (mounted.current = false);
}, [propVal]);//<--if propVal changes the animation starts
Full code is here
The propVal could be the id of an item (new item will animate in) or a property called deleted indicating an item has been removed (should animate out)
I am trying to create a smaller code example to re produce the problem I am facing but was not able (yet) to do so.
The problem is that if I delete an item of then the mounted.current = false (returned callback from useEffects) part gets called even though the component did not unmount.
When I change the code to return () => false && (mounted.current = false); basically removing the safeguard to prevent unmounted components from animating then deleting an item will animate without error. This tells me that the component is not unmounted but somehow the returned callback for onUnmount is called.
Sorry for not (yet) being able to provide a simpler reproduction of this problem. Maybe someone knows why the callback would be called when the component is obviously not unmounted (removing the safe guard does not cause errors and animates as expected)

If I understood correctly, you should set the mounted ref to true in the effect.
Otherwise, any change to the propVal dependency will stop recurAnimate from working.
Say the value changes from an id => to "deleted",
The cleanup effect runs but the component isn't unmounted,
then the effect runs again with the new value, but the isMounted ref
keeps recurAnimate locked.
edit: The cleanup function returned from the effect !== componentWillUnmount.
It will run every time the effect will be rerun, and then lastly when the component unmounts.

Related

What is the correct way to memoize this useEffect dependency?

I use useEffect to listen for a change in location.pathname to auto-scroll back to the top of the page (router) when the route changes. As I have a page transition (that takes pageTransitionTime * 1000 seconds), I use a timer that waits for the page transition animation to occur before the reset takes place. However, on the first load/mount of the router (after a loading page), I do NOT want to wait for the animation as there is no page animation.
Observe the code below, which works exactly as intended:
useEffect(() => {
const timer = setTimeout(() => {
window.scrollTo(0,0)
}, firstVisit.app ? 0 : pageTransitionTime * 1000 )
return () => clearTimeout(timer)
}, [location.pathname, pageTransitionTime])
The error I face here is that firstVisit.app isn't in the dependency array. I get this error on Terminal:
React Hook useEffect has a missing dependency: 'firstVisit.app'. Either include it or remove the dependency array react-hooks/exhaustive-deps
firstVisit.app is a global redux variable that is updated in the same React component by another useEffect, setting it to false as soon as the router is mounted (this useEffect has no dependency array, so it achieves it purpose instantly).
// UPON FIRST MOUNT, SET firstVisit.app TO FALSE
useEffect(() => {
if (firstVisit.app) {
dispatch(setFirstVisit({
...firstVisit,
app: false
}))
}
})
The problem is, when I include firstVisit.app in the dependency array in the first useEffect, the page will auto-reset scroll to (0,0) after pageTransitionTime, affecting the UX.
A bit of Googling lead me to find that I may need to memoize firstVisit.app but I'm not entirely sure how or the logic behind doing so?
React's log message tried to say that in your first useEffect hook callback function body you used some value that excluded in the list of dependencies. That's correct, please refer: useEffect Hook
The array of dependencies is not passed as arguments to the effect
function. Conceptually, though, that’s what they represent: every
value referenced inside the effect function should also appear in the
dependencies array. In the future, a sufficiently advanced compiler
could create this array automatically.
We recommend using the exhaustive-deps rule as part of our
eslint-plugin-react-hooks package. It warns when dependencies are
specified incorrectly and suggests a fix.
So, add to the second array parameter of useEffect firstVisit.app, and then check whether this warning/error is gone or not.
Also, you don't need to memoize primitive values, like boolean values (true/false), React is smart enough to not run your callback function again, when your boolean dependency hasn't changed after re-rendering.
[Edit]
If you want to run some logic in useEffect except initial render, i.e. to skip the first time render, you can use:
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
doYourTransition();
} else {
isMounted.current = true;
}
}, [deps]);
More complicated one with custom hook, but can be helpful based on your needs and wishes:
const useEffectAfterMount = (cb, deps) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
cb();
} else {
isMounted.current = true;
}
}, deps);
}
useEffectAfterMount(() => {
// the logic here runs always, but not on initial render
}, [firstVisit.app]);
I think, you got the idea. Of course, you can move these hooks into a separate file and to reuse them across the project and minimize the amount of code in your current component as well.

React/Next-js : Getting TypeError: Cannot read properties of undefined (reading 'id'), but the object is clearly not empty?

I am having an issue with a Next-js React checkbox list snippet after extracting it into the sandbox.
whenever I clicked the checkbox, I get the error:
TypeError: Cannot read properties of undefined (reading 'id')
which originated from line 264:
setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));
but at the top of the index.js I have defined the static JSON
and in useEffect() I update the pageData state with:
setPageData({
currentPage: threadsDataJSON.threads.current_page,
currentThreads: threadsDataJSON.threads.data,
totalPages: totalPages,
totalThreads: threadsDataJSON.threads.total,
});
so why when I clicked the checkbox it throws the error?
my sandbox link: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js
It looks like your useEffect on line 280 only triggers once you've checked a box (for some reason), so until you trigger that useEffect, pageData.currentThreads remains empty, which is where the error you're running into comes from.
I'd suggest moving all the state initialization from the useEffect into the useState call itself. E.g.
// Bad
const [something, setSomething] = useState(/* fake initial state */);
useEffect(() => {
setSomething(/* real initial state */)
}, []);
// Good
const [something, setSomething] = useState(/* real initial state */);
Here's a fork of your sandbox with this fix.
This is occurring because in Home you've created the handleOnChange function which is passed to the List component that is then passed to the memoized Item component. The Item component is kept the same across renders (and not rerendered) if the below function that you've written returns true:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked
);
}
This means that the Item component holds the first initial version of handleOnChange function that was created when Home first rendered. This version of hanldeOnChange only knows about the initial state of pageData as it has a closure over the initial pageData state, which is not the most up-to-date state value. You can either not memoize your Item component, or you can change your itemPropsAreEqual so that Item is rerendered when your props.handleOnChange changes:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked &&
prevItem.handleOnChange === nextItem.handleOnChange // also rerender if `handleOnChange` changes.
);
}
At this point you're checking every prop passed to Item in the comparison function, so you don't need it anymore and can just use React.memo(Item). However, either changing itemPropsAreEqual alone or removing itemPropsAreEqual from the React.memo() call now defeats the purpose of memoizing your Item component as handleOnChange gets recreated every time Home rerenders (ie: gets called). This means the above check with the new comparison function will always return false, causing Item to rerender each time the parent Home component rerenders. To manage that, you can memoize handleOnChange in the Home component by wrapping it in a useCallback() hook, and passing through the dependencies that it uses:
const handleOnChange = useCallback(
(iindex, id) => {
... your code in handleOnChange function ...
}
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created
This way, a new handleOnChange reference is only created when needed, causing your Item component to rerender to use the new up-to-date handleOnChange function. There is also the useEvent() hook which is an experimental API feature that you could look at using instead of useCallback() (that way Item doesn't need to rerender to deal with handleOnChange), but that isn't available yet as of writing this (you could use it as a custom hook for the time being though by creating a shim or using alternative solutions).
See working example here.

How do I avoid this stale closure?

I am working on building a simple React slider which will expose internal methods up to its parent via a ref and I am having trouble with what I suspect to be a stale closure, but I can't fully understand what is actually happening. Hoping someone can help me understand here.
Here is a simplified version of the code that I want to work:
const Slider = forwardRef((props, ref) => {
const sliderRef = useRef();
const [slides, dispatchSlides] = useReducer(reducer, []);
sliderRef.current = {
countSlides: () => {
return slides.length
},
};
useImperativeHandle(ref, () => sliderRef.current);
return null;
After this component mounts, its children will render and fill up the slides reducer with information on their positioning and visibility using IntersectionObserver. This part works, so I have kept it out of this example for simplicity. For our sake, just assume that slides is immediately populated with objects after mount, and that a user will manually call countObjects from the parent component much later after slides has been populated.
In the parent component, if I execute countSlides from the ref, I will always see slides.length === 0, no matter how many slides are actually present. I assume this is because the original countSlides method is a stale closure.
Now, what I don't fully understand, is that if I adjust this line:
useImperativeHandle(ref, () => sliderRef.current);
to this:
useImperativeHandle(ref, () => () => {
countSlides: () => sliderRef.current.countSlides()
});
the stale closure is fixed and everything works as intended. But this is duplicative code and I'm just not sure what is even different between the two cases. I do not want to repeat myself redefining many methods within the useImperativeHandle hook, but much more importantly, I want to understand what the difference is between the two examples above.
Thank you!
EDIT Adding full example:
https://codesandbox.io/s/ssr-slider-6ywf9
As you commented that the problem arose only when writing like onClick={ slider?.current?.prev } instead of onClick={() => { slider?.current?.prev() }}
I have tried with my sandbox that I provided and got the same problem.
There're a few things here:
useRef doesn't trigger re-renders itself, which means even a ref is updated, no re-renders follow.
Without re-renders, what's bound to onClick will not be updated.
So, if we write like onClick={slider?.current?.prev}, what happens is:
The ref is initially undefined, which means onClick is undefined as well
No re-render is triggered, so, even if ref is updated with a new value, onClick stays undefined
But, if we write like onClick={() => { slider?.current?.prev() }}, what happens is:
slider?.current?.prev is initially undefined
onClick is bound to that anonymous function
slider?.current?.prev is updated, we have the expected function
When the button is clicked, the function is called, which triggers the latest value of slider?.current?.prev

React "can't perform state update on unmounted component" with timeout

Questions about this warning have been asked and answered countless times on the Internet, yet - maybe actually because of that - I'm having difficulty finding a comment which touches on my own situation.
I'm working on an autosave feature, whereby when you start typing into the component's form it starts a timer. On completion, a) it dispatches an action (which is working fine) and b) it clears the timer state so that next time the user types it knows it can start a new one.
The issue comes when I unmount the component before the timer is complete: when it does expire I get the Can't perform a React state update on an unmounted component warning as I try to clear the timer state.
Now, just about all the solutions I've found online for this suggest I should create an isMounted state variable and check it before running the relevant setAutosave(null) state call. Except that - as far as I'm aware - the nature of Javascript timers means that the values available to the setTimeout callback (or Promise callback, for that matter) are those when the timer was started - when of course, the component was mounted.
Effectively, I'm stuck between a) the autosave feature requiring a state reset if the component is mounted, b) React demanding that the state reset cannot occur if the component isn't mounted, and c) the timer preventing any checking (that I can think of) of whether the component is or isn't mounted. Any ideas?
const { dispatch } = useContext(MyContext)
const [autosave, setAutosave] = useState(null)
const save = () => {
clearTimeout(autosave) // in case you manually submit the form
setAutosave(null)
dispatch({ type: "SAVE" }) // this line works fine
}
const onChange = () => {
if (!autosave) {
const timeoutId = setTimeout(save, 30000)
setAutosave(timeoutId)
}
}
<form onChange={onChange} onSubmit={save}>
...
I am not sure though, Try change your state using componentWillUnmount if your component is class-based component and useEffect functional components

Read prev props: Too many renders

I want someone to confirm my intuition on below problem.
My goal was to keep track (similar to here) of props.personIdentId and when it changed, to call setSuggestions([]).
Here is code:
function Identification(props) {
let [suggestions, setSuggestions] = useState([]);
let prevPersonIdentId = useRef(null)
let counter = useRef(0) // for logs
// for accessing prev props
useEffect(() => {
prevPersonIdentId.current = props.personIdentId;
console.log("Use effect", props.personIdentId, prevPersonIdentId.current, counter.current++)
});
// This props value has changed, act on it.
if (props.personIdentId != prevPersonIdentId.current) {
console.log("Props changed", props.personIdentId, prevPersonIdentId.current, counter.current++)
setSuggestions([])
}
But it was getting in an infinite loop as shown here:
My explanation why this happens is that: initially when it detects the prop has changed from null to 3, it calls setSuggestions which schedules a re-render, then next re-render comes, but previous scheduled useEffect didn't have time to run, hence the prevPersonIdentId.current didn't get updated, and it again enters and passes the if condition which checks if prop changed and so on. Hence infinite loop.
What confirms this intuition is that using this condition instead of old one, fixes the code:
if (props.personIdentId != prevPersonIdentId.current) {
prevPersonIdentId.current = props.personIdentId;
setSuggestions([])
}
Can someone confirm this or add more elaboration?
useEffect - is asynchronous function!
And you put yours condition in synchronous part of component. Off course synchronous part runs before asynchronous.
Move condition to useEffect
useEffect(() => {
if (personIdentId !== prevPersonIdentId.current) {
prevPersonIdentId.current = personIdentId;
console.log(
"Use effect",
personIdentId,
prevPersonIdentId.current,
counter.current++
);
setSuggestions([]);
}
});
It could be read as:
When mine component updates we check property personIdentId for changes and if yes we update ref to it's value and run some functions
It looks like this is what's happening:
On the first pass, props.personId is undefined and
prevPersonIdentId.current is null. Note that if (props.personIdentId !=
prevPersonIdentId.current) uses != so undefined is coerced to
null and you don't enter the if.
Another render occurs with the same conditions.
props.personId now changes, so you enter the if.
setSuggestions([]) is called, triggering a re-render.
loop forever
Your useEffect is never invoked, because you keep updating your state and triggering re-renders before it has a chance to run.
If you want to respond to changes in the prop, rather than attempting to roll your own change-checking, you should just use useEffect with the value you want to respond to in a dependency array:
useEffect(() => {
setSuggestions([])
}, [props.personId] );

Categories

Resources