How do I avoid this stale closure? - javascript

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

Related

Why is React state not updated inside functions? [duplicate]

This question already has answers here:
React - useState - why setTimeout function does not have latest state value?
(2 answers)
Closed 7 months ago.
I have a component that renders a table with objects. This component shows a button that, when pressed, sends the parent a specific object. The parent must set it in the state to display some graphical stuff. The rendering is working correctly, what I don't understand is why I am getting an outdated value after setting the state correctly.
It's not a race condition, React is simply ignoring the updated value of a variable, even when it re-renders the component correctly.
A minimal example:
import { useState } from "react";
import { SomeComponent } from "./SomeComponent";
export default function App() {
const [currentID, setCurrentID] = useState(null);
function getData() {
console.log("Getting data of: ", currentID); // PROBLEM: this is null
}
function setAndRetrieveData(value) {
setCurrentID(value);
// Just to show the problem and discard race conditions.
setTimeout(() => {
getData();
}, 1500);
}
return (
<div className="App">
<h1>Current ID: {currentID}</h1> {/* This works fine */}
<SomeComponent getInfoFor={setAndRetrieveData} />
</div>
);
}
SomeComponent:
export function SomeComponent(props) {
const randomID = 45;
return <button onClick={() => props.getInfoFor(randomID)}>Get info</button>;
}
Even with solutions like useStateCallback the problem persists.
Is there a way to do this without having to use the awful useEffect which is not clear when reading the code? Because the logic of the system is "when this button is pressed, make a request to obtain the information", using the hook useEffect the logic becomes "when the value of currentID changes make a request", if at some point I want to change the state of that variable and perform another action that is not to obtain the data from the server then I will be in trouble.
Thanks in advance
I think this is an issue with the way Javascript closures work.
When you execute a function, it gets bundled with all the data that pertains to it and then gets executed.
The issue is that you call this:
setTimeout(() => {
getData();
}, 1500);
inside setAndRetrieveData(value).
Even though it's inside a setTimeout, the getData() function has been bundled with the information it needs (currentID) at that point in time, not when it actually runs. So it gets bundled with the currentId before the state update takes place
Unfortunately, I would recommend using useEffect. This is the best way to ensure you avoid issues like this and any potential race conditions. Hopefully someone else can provide a different approach!
when setAndRetrieveData is called it sets a state that leads to the component being rerendered to reflect the new state. When the timeout finishes The function getData was created in the previous render. And thus only has access to the state variable from the previous render. That now is undefined.
what you could try is using a useEffect hook that that listens to changes of
currentID.
useEffect(() => {
const timeoutId = setTimeout(() => {
// Do something with the updated value
},1000);
return () => {
// if the data updates prematurely
// we cancel the timeout and start a new one
clearTimeout(timeoutId);
}
},[currentID])

useState element not rerendering when triggered by an event handler in a child component

I have a parent component with a useState that is being displayed. What I want to do is have a child component be able to update this state to change what the parent is displaying. I currently have the following:
function Parent() {
const [myWindow, setMyWindow] = useState(null)
useEffect(() => {
setMyWindow(<Child updateWindowFunc={() => setMyWindow(someNewWindow)} />)
}, []}
return (
<div>
{myWindow}
</div>
)
}
function Child({updateWindowFunc}) {
//Somehow calls updateWindowFunc
}
(Note that my actual code is set up somewhat differently, so don't mind the syntax as much as the general concept.)
I've found that I can get the value of myWindow to change, but the actual display doesn't show any change. I've tried forcing a re-render after the change, and adding a useRef to display useRef.current, but nothing seems to update which window is actually being rendered.
What am I doing wrong?
Edit:
I've found that it works if I switch to a different type of component, but if its just a different element of the same component then there is no re-render. I've been using React.createElement(), so I would think the 'objects' are distinct, but maybe I just misunderstand how this works.
Can you provide more details or working\not working code example that has more info?
Currently it's hard to get, cause that doesn't make any sense since you can just render it like below and that code will have same result as one that you are trying to run
function Parent() {
return (
<div>
<Child />
</div>
)
}
So for better understanding and fixing a problem \ finding a different approach, please, provide more details

Children useCallback dependency hell

From what I understand you use useCallback to prevent rerendering so I've been using it in every function and my spider senses are telling me it already sounds bad.
But the story doesn't ends there, since I've been using it everywhere I'm now passing dependencies to all my child components that they shouldn't need to worry about like in the following example :
EDIT // SANDBOX: https://codesandbox.io/s/bold-noether-0wdnp?file=/src/App.js
Parent component (needs colorButtons and currentColor)
const ColorPicker = ({onChange}) => {
const [currentColor, setCurrentColor] = useState({r: 255, g:0, b: 0})
const [colorButtons, setColorButtons] = useState({0: null})
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
return <div className="color-picker">
<RgbColorPicker color={currentColor} onChange={setCurrentColor} />
<div className="color-buttons">
{
Object.entries(colorButtons).map(button => <ColorButton
//...
currentColor={currentColor}
onClick={handleColorButtons}
colorButtons={colorButtons}
/>)
}
</div>
</div>
}
1st child (needs style and currentColor but gets colorButtons for free from its parent)
const ColorButton = ({currentColor, onClick, id, colorButtons}) => {
const [style, setStyle] = useState({})
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
return <ToggleButton
//...
onClick={handleClick}
style={style}
dependency1={style}
dependency2={currentColor}
dependency3={colorButtons}
>
</ToggleButton>
}
2nd child (only needs its own variables but gets the whole package)
const ToggleButton = ({children, className, onClick, style, data, id, onRef, ...dependencies}) => {
const [isToggled, setIsToggled] = useState(false)
const [buttonStyle, setButtonStyle] = useState(style)
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
return <button
className={className || "toggle-button"}
onClick={handleClick}
style={buttonStyle || {}}
ref={onRef}
>
{children}
</button>
}
Am I doing an anti-pattern and if so, what is it and how to fix it ? Thanks for helping !
React hook useCallback
useCallback is a hook that can be used in functional React components. A functional component is a function that returns a React component and that runs on every render, which means that everything defined in its body get new referential identities every time. An exception to this can be accomplished with React hooks which may be used inside functional components to interconnect different renders and maintain state. This means that if you save a reference to a regular function defined in a functional component using a ref, and then compare it to the same function in a later render, they will not be the same (the function changes referential identity between renderings):
// Render 1
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false due to different identity
fn.current = fnInBody
...
As per the docs, useCallback returns "a memoized version of the callback that only changes if one of the dependencies has changed" which is useful "when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders".
To sum up, useCallback will return a function that maintains its referential identity (e.g. is memoized) as long as the dependencies don't change. The returned function contains a closure with the used dependencies and must thus be updated once the dependencies change.
This results in this updated version of the previous example
// Render 1
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // true
fn.current = fnInBody
...
Your use case
Keeping the above description in mind, let's have a look at your use of useCallback.
Case 1: ColorPicker
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
This function will get a new identity every time colorButtons or currentColor changes. ColorPicker itself rerenders either when one of these two are set or when its prop onChange changes. Both handleColorButtons and the children should be updated when currentColor or colorButtons change. The only time the children benefit from the use of useCallback is when only onChange changes. Given that ColorButton is a lightweight component, and that ColorPicker rerenders mostly due to changes to currentColor and colorButtons, the use of useCallback here seems redundant.
Case 2: ColorButton
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
This is a situation similar to the first case. ColorButton rerenders when currentColor, onClick, id or colorButtons change and the children rerender when handleClick, style, colorButtons or currentColor change. With useCallback in place, the props id and onClick may change without rerendering the children (according to the above visible code at least), all other rerenders of ColorButton will lead to its children rerendering. Again, the child ToggleButton is lightweight and id or onClick are not likely to change more often than any other prop so the use of useCallback seems redundant here as well.
Case 3: ToggleButton
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
This case is elaborate with a lot of dependencies but from what I see, one way or the other, most of the component props will lead to a "new version" of handleClick and with the children being lightweight components, the argument to use useCallback seems weak.
So when should I use useCallback?
As the docs say, use it in the very specific cases when you need a function to have referential equality between renders ...
You have a component with a subset of children that are expensive to rerender and that should rerender much less often than the parent component but rerender due to a function prop changing identity whenever the parent rerenders. To me, this use case also signals bad design and I would attempt to divide the parent component into smaller components but what do I know, maybe this is not always possible.
You have a function in the body of the functional component which is used in another hook (listed as a dependency) which is triggered every time due to the function changing identity whenever the component rerenders. Typically, you can omit such a function from the dependency array by ignoring the lint rule even if this is not by the book. Other suggestions are to place such a function outside the body of the component or inside the hook that uses it, but there might be scenarios where none of this works out as intended.
Good to know connected to this is ...
A function living outside a functional component will always have referential equality between renders.
The setters returned by useState will always have referential equality between renders.
I said in the comments that you can use useCallback when there is function doing expensive calculations in a component that rerenders often but I was a bit off there. Let's say you have a function that does heavy calculations based on some prop that changes less often than a component rerenders. Then you COULD use useCallback and run a function inside it that returns a function with a closure with some computed value
const fn = useCallback(
(
() => {
const a = ... // heavy calculation based on prop c
const b = ... // heavy calculation based on prop c
return () => { console.log(a + b) }
}
)()
, [c])
...
/* fn is used for something, either as a prop OR for something else */
This would effectively avoid calculating a and b every time the component rerenders without c changing, but the more straightforward way to do this would be to instead
const a = useMemo(() => /* calculate and return a */, [c])
const b = useMemo(() => /* calculate and return b */, [c])
const fn = () => console.log(a + b)
so here the use of useCallback just complicates things in a bad way.
Conclusion
It's good to understand more complicated concepts in programming and to be able to use them, but part of the virtue is also to know when to use them. Adding code, and especially code that involves complicated concepts, comes at the price of reduced readability and code that is harder to debug with a lot of different mechanisms that interplay. Therefore, make sure you understand the hooks, but always try to not use them if you can. Especially useCallback, useMemo and React.memo (not a hook but a similar optimization) should, in my opinion, only be introduced when they are absolutely needed. useRef has its very own use cases but should also not be introduced when you can solve your problem without it.
Good work on the sandbox! Always makes it easier to reason about code. I took the liberty of forking your sandbox and refactoring it a bit: sandbox link. You can study the changes yourself if you want. Here is a summary:
It's good that you know and use useRef and useCallback but I was able to remove all the uses, making the code much easier to understand (not only by removing these uses but by also removing the contexts where they are used).
Try to work with React to simplify things. I know this is not a hands-on suggestion but the more you get into React, the more you will realize that you can do things co-operating with React, or you can do things your own way. Both will work but the latter will result in more headache for you and everybody else.
Try to isolate the scope of a component; only delegate data that is necessary to child components and constantly question where you keep your state. Earlier you had click handlers in all three components and the flow was so complicated I didn't even bother to fully understand it. In my version, there is just one click handler in ColorPicker that is being delegated down. The buttons don't have to know what happens when you click them as long as the click handler takes care of that. Closures and the ability to pass functions as arguments are strong advantages of React and Javascript.
Keys are important in React and it's good to see that you use them. Typically, the key should correspond to something that uniquely identifies a specific item. Good to use here would be ${r}_${g}_${b} but then we would only be able to have one sample of each color in the button array. This is a natural limitation but if we don't want it, the only way to assign keys is to assign a unique identifier, which you did. I prefer using Date.now() but some would probably advise against it for some reason. You could use a global variable outside the functional component too if you don't want to use a ref.
Try to do things the functional (immutable) way, and not the "old" Javascript way. For example, when adding to an array, use [...oldArray, newValue] and when assigning to an object, use {...oldObject, newKey: newValue }.
There are more things to say but I think it's better for you to study the refactored version and you can let me know if you wonder about anything.

Why is my RXJS epic in an infinite loop on mount but only called once on button click?

please take a look at this code below
basically what is happening my action is being dispatched here:
useEffect(() => {
fetchData()
setLoaded(true)
}, [])
but for some reason this is infinite looping and causing my action to be dispatched continuously
export const fetchData = () => ({ type: 'GET_USER_DATA' })
and this is triggering my epic
const getUserData = (action$, state$) =>
action$.pipe(
ofType('GET_USER_DATA'),
mergeMap(
(action) =>
ajax
.getJSON(
`myurlishere`,
)
.pipe(map((response) => fetchUserFulfilled(response))),
)
)
which trigger this:
const fetchUserFulfilled = (payload) => ({ type: 'GET_DATA_SUCCESS', data: payload })
this code all works but it's continuously calling it in an infinite loop
however, if I move the code from useEffect to a button call like so:
<button onClick={fetchData}>fetch</button>
it only calls it once, which is what I want
but I need the data to be called onmount. so how do I fix it?
please note I have tried adding various things to the second argument of useEffect but it's having no effect
useEffect(() => {
fetchData()
setLoaded(true)
}, [user.id])
Based solely off the provided code, I don't see any issues. My gut suggest a few options, even though you mentioned some of them I would triple check:
Missing dependencies array as second arg to useEffect, or you're using a variable for it but the variable has an undefined value which would have the same problem.
If you are using useEffect dependencies, perhaps one of them is constantly changing unknowingly. e.g. objects often change in identity between renders, {} !== {}
There is code not shown that is also dispatching the same action, and in fact that useEffect is only running once.
Some parent is rendering one of the ancestors or this component with a key={something} and the value provided changes on each render. If that happens, the component is torn down and recreated every time from scratch.
If you are 100% positive you are providing useEffect(work, []), an empty array as second argument, but the effect is in fact confirmed to be running in an infinite loop, synchronously, then the forth possibility is likely.
If you typed these code examples in the question by hand when posting this, do not trust that you implemented them the same way as what you think your app is doing. Triple check. Ideally have someone else check who didn't write the code. Often the problem is what we think we've told our code to do is not what we've actually told it to do. If you haven't already, your best bet is to step through the code with a debugger so you can see what's happening.
Hope this helps!
this is the code now working:
export const getUserDataEpic = () => {
return ajax
.getJSON(myurl)
.pipe(
map((response) => fetchUserFulfilled(response)),
)
}
I know I'm not listening to the action fired now, but why would that be causing an infinite loop?

How to pass state value to a callback function using React Hooks

Am creating a panel using react-modal, everything seems to be working fine.
But once the modal is opened, I need to set the top value of my modal. I am using hooks, and whenever am trying to update the value, getting into error, hooks can be called only inside the React functions. I tried all the possible ways but not able to solve the issue.
Expectation:
- OnAfterOpen should be called, and the latest state value should be passed to pos prop.
This is what I have tried.
function useTop() {
const [top, setTop] = useState('100px');
useEffect(() => {
const handleResize = () => {
const hasRefClientHeight = contentRef.current && contentRef.current.clientHeight;
hasRefClientHeight < window.innerHeight ? setTop(`calc(100% - ${hasRefClientHeight}px)`) : setTop('100px');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return top;
}
const Panel = () => {
// const newTop = useTop(); -- This is not working as modal content is not ready. Modal Content will be ready only if `onAfterOpen` triggers.
<Modal pos={useTop} onAfterOpen={useTop}><div ref={contentRef}>Test</div></Modal>
};
Indeed, the rules of hooks says:
https://reactjs.org/docs/hooks-rules.html
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
This is necessary for React to be sure that hooks will be called on exact the same order in every render cycle.
It seems that Modal would call useTop only after mounted (opened), i.e. it will probably have skipped the first render, violating the rule of hooks and messing with the order.
One thing you can try:
Pass the ref for your useTop hook and handle it inside of it. If it's null, don't do anything, if it's already mounted, attach the listener and return the cleanup function.
const Panel = () => {
const newTop = useTop(contentRef);
<Modal pos={newTop} onAfterOpen={null}><div ref={contentRef}>Test</div></Modal>
};
Along with newTop you can return a function that triggers recalculation of the newTop. Similar to how useState does. And invoke it onAfterOpen.

Categories

Resources