React useCallback setting dependency on a function called inside the callback - javascript

I'm working on a web app using react and saw this implemented there. It goes something like this:
const onAddNewAccount = useCallback(async () => {
await refetch();
setOtherState((prev) => {...});
},
[refetch]);
I don't understand why do this, is this actually something correct? Because from what I understand the callback will be called on one of the dependencies changing.
From what I've seen calling refetch won't actually trigger useCallback probably because it is just called right? not changed.
So basically I need help understanding why they did this.

the callback will be called on one of the dependencies changing.
No, it won't be called, it will just be recreated. The purpose of useCallback is to memoize the function and reuse it if nothing has changed. In other words, it's trying to make it so onAddNewAccount from render 1 === onAddNewAccount from render 2.
If refetch changes, then the memoization will break and a new function is created. That way the new onAddNewAccount can call the new refetch if needed. If refetch never changes, then that's great: the memoization can last forever then.

Related

How can I start an async API fetch as early as possible in a React functional component?

The standard way to make an API call in functional React is with useEffect:
function Pizzeria() {
const [pizzas, setPizzas] = useState([])
useEffect(
() => fetchPizzas().then(setPizzas),
[]
)
return (
<div>
{pizzas.map((p, i) => <Pizza pizza={p} key={i} />)}
</div>
)
}
But, as this article points out, useEffect will not fire until after the component has rendered (the first time). Obviously in this trivial case it makes no difference, but in general, it would be better to kick off my async network call as soon as possible.
In a class component, I could theoretically use componentWillMount for this. In functional React, it seems like a useRef-based solution could work. (Allegedly, tanstack's useQuery hook, and probably other libraries, also do this.)
But componentWillMount is deprecated. Is there a reason why I should not do this? If not, what is the best way in functional React to achieve the effect of starting an async call early as possible (which subsequently sets state on the mounted component)? What are the pitfalls?
You're splitting milliseconds here, componentWillMount/render/useEffect all happen at essentially the same time, and the time spent fetching occurs after that. The difference in time from before to after rendering is tiny compared to the time waiting for the network when the request is sent. If you can do the fetch before the component renders, react-query's usePrefetch is nice for that.
Considering the scope of a single component, the earliest possible would be to just make the call in the component's function. The issue here is just that such statement would be executed during every render.
To avoid those new executions, you must keep some kind of "state" (or variable, if you will). You'll need that to mark that the call has been made and shouldn't be made again.
To keep such "state" you can use a useState or, yes, a useRef:
function Pizzeria() {
const pizzasFetchedRef = useRef(false)
const [pizzas, setPizzas] = useState([])
if (!pizzasFetchedRef.current) {
fetchPizzas().then(setPizzas);
pizzasFetchedRef.current = true;
}
Refs are preferred over state for this since you are not rendering the value of pizzasFetched.
The long story...
Yet, even if you use a ref (or state) as above, you'll probably want to use an effect anyway, just to avoid leaks during the unmounting of the component. Something like this:
function Pizzeria() {
const pizzasFetchStatusRef = useRef('pending'); // pending | requested | unmounted
const [pizzas, setPizzas] = useState([])
if (pizzasFetchStatusRef.current === 'pending') {
pizzasFetchStatusRef.current = 'requested';
fetchPizzas().then((data) => {
if (pizzasFetchStatusRef.current !== 'unmounted') {
setPizzas(data);
}
});
}
useEffect(() => {
return () => {
pizzasFetchStatusRef.current = 'unmounted';
};
}, []);
That's a lot of obvious boilerplate. If you do use such pattern, then creating a custom hook with it is the better way. But, yeah, this is natural in the current state of React hooks. See the new docs on fetching data for more info.
One final note: we don't see this issue you pose around much because that's nearly a micro-optimization. In reality, in scenarios where this kind of squeezing is needed, other techniques are used, such as SSR. And in SSR the initial list of pizzas will be sent as prop to the component anyway (and then an effect -- or other query library -- will be used to hydrate post-mount), so there will be no such hurry for that first call.

Why does React/Typescript complain want me to use my return function in useEffect dependency array?

I'm making a checkbox component for my project, and I have it set so that whenever is isChecked state changes, useEffect will run a returnFunction (defined in the props), like so:
export function JCheckbox({ returnFunction }: JCheckboxProps) {
const [isChecked, setIsChecked] = React.useState(defaultState)
React.useEffect(() => {
returnFunction(isChecked)
}, [isChecked])
When I do this, my compiler wants me to include returnFunction in the useEffect dependency array. I'm confused why I would need to track that, it's not used at all anywhere else in the function, and it doesn't change either. The code works perfectly fine without it.
You should add your function (returnFunction) to useEffect dependency because your function reference (returnFunction) may be changed, So react ask you to add your function (returnFunction) in useEffect dependency to recognize that and update them by new function reference.
returnFunction is certainly used as it is called inside the effect, and it could certainly change as the parent component could pass in different props, in which case your component would continue to call the previously passed in function. So the linter is correct, returnFunction is a dependency. However adding it makes it slightly more difficult to detect changes of isChecked, as changing returnFunction would also trigger the effect. Thus it seems way more elegant to call returnFunction from within setIsChecked:
const [isChecked, _setIsChecked] = useState(false);
function setIsChecked(value: boolean) {
returnFunction(value);
_setIsChecked(value);
}
However in the case given it seems that the state should rather be maintained by the parent component, consider lifting it up.
returnFunction is read from props so you probably must include it in the array of dependencies.
You should almost always include every variable and function you declare inside React and use in useEffect callback in the array of dependencies.
Eslint does not expect you to include setState functions and refs that you create in the same component that you execute useEffect.
Edit:
If you have a handleChange event handler and isChecked is toggled whenever the onChange method is invoked, you probably do not need useEffect. Call returnFunction function with the new isChecked value.

Potential bug in "official" useInterval example

useInterval
useInterval from this blog post by Dan Abramov (2019):
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
A Potential Bug
The interval callback may be invoked between the commit phase and the useEffect invocation, causing the old (and hence not up to date) callback to be called. In other words, this may be the execution order:
Render phase - a new value for callback.
Commit phase - state committed to DOM.
useLayoutEffect
Interval callback - using savedCallback.current(), which is different than callback.
useEffect - savedCallback.current = callback;
React's Life Cycle
To further illustrate this, here's a diagram showing React's Life Cycle with hooks:
Dashed lines mean async flow (event loop released) and you can have an interval callback invocation at these points.
Note however, that the dashed line between Render and React updates DOM (commit phase) is most likely a mistake. As this codesandbox demonstrates, you can only have an interval callback invoked after useLayoutEffect or useEffect (but not after the render phase).
So you can set the callback in 3 places:
Render - Incorrect because state changes have not yet been committed to the DOM.
useLayoutEffect - correct because state changes have been committed to the DOM.
useEffect - incorrect because the old interval callback may fire before that (after layout effects) .
Demo
This bug is demonstrated in this codesandebox. To reproduce:
Move the mouse over the grey div - this will lead to a new render with a new callback reference.
Typically you'll see the error thrown in less than 2000 mouse moves.
The interval is set to 50ms, so you need a bit of luck for it to fire right between the render and effect phases.
Use Case
The demo shows that the current callback value may differ from that in useEffect alright, but the real question is which one of them is the "correct" one?
Consider this code:
const [size, setSize] = React.useState();
const onInterval = () => {
console.log(size)
}
useInterval(onInterval, 100);
If onInterval is invoked after the commit phase but before useEffect, it will print the wrong value.
This does not look like a bug to me, although I understand the discussion.
The answer above that suggests updating the ref during render would be a side effect, which should be avoided because it will cause problems.
The demo shows that the current callback value may differ from that in useEffect alright, but the real question is which one of them is the "correct" one?
I believe the "correct" one is the one that has been committed. For one reason, committed effects are the only ones that are guaranteed to have cleanup phase later. (The interval in this question doesn't need a cleanup effect, but other things might.)
Another more compelling reason in this case, perhaps, is that React may pre-render things (either at lower priority, or because they're "offscreen" and not yet visible, or in the future b'c of animation APIs). Pre-rendering work like this should never modify a ref, because the modification would be arbitrary. (Consider a future animation API that pre-renders multiple possible future visual states to make transitions faster in response to user interaction. You wouldn't want the one that happened to render last to just mutate a ref that's used by your currently visible/committed view.)
Edit 1 This discussion mostly seems to be pointing out that when JavaScript isn't synchronous (blocking), when it yields between rendering, there's a chance for other things to happen in between (like a timer/interval that was previously scheduled). That's true, but I don't think it's a bug if this happens during render (before an update is "committed").
If the main concern is that the callback might execute after the UI has been committed and mismatch what's on the screen, then you might want to consider useLayoutEffect instead. This effect type is called during the commit phase, after React has modified the DOM but before React yields back to the browser (aka so no intervals or timers can run in between).
Edit 2 I believe the reason Dan originally suggested using a ref and an effect for this (rather than just an effect) was because updates to the callback wouldn't reset the interval. (If you called clearInterval and setInterval each time the callback changed, the overall timing would be interrupted.)
To attempt to answer your final question strictly:
I can't see any logical harm updating the callback in render() as opposed to useEffect(). useEffect() is never called anything other than after render(), and whatever it is called with will be what the last render was called with, so the only difference logically is that the callback may be more out-of-date by the time the useEffect() is called.
This may be exacerbated by the coming concurrent mode, if there may be multiple calls to render() before a call to useEffect(), but I'm not even sure it works like that.
However: I would say there is a maintenance cost to doing it this way: it implies that it is ok to cause side effects in render(). In general that is not a good idea, and all necessary side effects should really be done in useEffect(), because, as the docs say:
the render method itself shouldn’t cause side effects ... we typically want to perform our effects after React has updated the DOM
So I would recommend putting any side effect inside a useEffect() and having that as a coding standard, even if in certain situations it is OK. And particularly in a blog post by a react core dev that is going to be copied and pasted by "guide" many people it is important to set the right example ;-P
Alternative solution
As for how you can fix your problem, I will just copy and paste my suggested implementation of setInterval() from this answer, which should remove the ambiguity by calling the callback in a separate useEffect(), at which point all state should be consistent and you don't have to worry about which is "correct". Dropping it into your sandbox seemed to solve the problem.
function useTicker(delay) {
const [ticker, setTicker] = useState(0);
useEffect(() => {
const timer = setInterval(() => setTicker(t => t + 1), delay);
return () => clearInterval(timer);
}, [delay]);
return ticker;
}
function useInterval(cbk, delay) {
const ticker = useTicker(delay);
const cbkRef = useRef();
// always want the up to date callback from the caller
useEffect(() => {
cbkRef.current = cbk;
}, [cbk]);
// call the callback whenever the timer pops / the ticker increases.
// This deliberately does not pass `cbk` in the dependencies as
// otherwise the handler would be called on each render as well as
// on the timer pop
useEffect(() => cbkRef.current(), [ticker]);
}
Here is a modification of your example which shows that both/neither approaches are correct: https://codesandbox.io/s/useintervalbug-neither-are-correct-zu2zt?file=/src/App.js
The use of refs is not what you would do in reality but it was necessary to easily detect and report the problem. They do not materially affect the behaviour.
In this example the parent component has created the new "correct" callback, finished rendering and wants that new callback to be used by the child and the timer.
Ultimately there is a race condition between when the "correct" callback ultimately gets passed to useInterval and when the browser decides to call the callback. I do not think it is possible to avoid this.
It makes no difference if you memoize the callback unless of course it has no dependencies and never changes.

Should you pass setter functions into the dependency array of a React hook?

Recently I saw a few examples of passing setter functions into hook dependency arrays in my coworkers' React code, and it doesn't look right to me. For example:
const MyComponent = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
doSomeBigLongNetworkRequest();
setLoading(false);
}, [setLoading, /* other deps */]);
// ...
}
My feeling is that they have misunderstood the purpose of the dependency array, which, as I understand it, is to indicate which pieces of state to monitor so that the hook can fire again when they change, not to simply indicate that the hook needs to use the setLoading function. And since the setLoading function never actually changes, including it in the dependencies does nothing.
Am I correct, or does including the setter in the array make sense somehow? My other thought was that maybe this was just a linter error, since the linter cannot recognize that the function is a setter, and thinks it might change.
I should also add that in the instances I've seen, they've included the setter but not the variable. So in the example above, setLoading, but not loading would be in the dependency array, and the hook does not actually need the value of loading.
Yes, you are right there is no need to include them. Here is quote from docs:
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
In general again based on docs the recommendation about dependency array is:
If you use this optimization, make sure the array includes all values
from the component scope (such as props and state) that change over
time and that are used by the effect. Otherwise, your code will
reference stale values from previous renders.

How can I use an object as initializer for custom hooks without adding complexity/state or inviting future problems?

I just started using hooks in react and am creating a prototype custom hook for a framework.
The hook should take an object as an argument for initialization and cleanup (setting up/removing callbacks for example).
Here is my simplified Code so far:
export function useManager(InitObj) {
const [manager] = useState(() => new Manager());
useEffect(() => {
manager.addRefs(InitObj)
return () => manager.removeRefs(InitObj)
}, [manager]);
return manager;
}
to be used like this:
useManager({ cb1: setData1, cb2: setData2... })
In future Iterations the Manager might be a shared instance, so I need to be able to be specific about what I remove upon cleanup.
I put console.log all over the place to see If i correctly understand which code will be run during a render call. From what I can tell this code does 100% what I expeted it to do!
Unfortunately (and understandably) I get a warning because I did not include InitObj in the effects dependencies. But since I get an object literal simply putting it in there will cause the effect to be cleaned up/rerun on every render call since {} != {} which would be completely unnecessary.
My research so far only revealed blog posts like this one, but here only primitive data is used that is easily classified as "the same" (1 == 1)
So far I have found 3 possible solutions that I am not completely happy with:
using useMemo to memoize the object literal outside the hook
useManager(useMemo(() => { cb: setData }, []))
This adds more responsibility on the developer using my code => not desirable!
using useState inside the hook
const [iniOBj] = useState(InitObj);
A lot better already, but it adds state that does not feel like state. And it costs (minimal) execution time and memory, I would like to avoid that if possible.
using // eslint-disable-next-line react-hooks/exhaustive-deps
Works for sure, but there might still be other dependencies that might be missed if I simply deactivate the warning.
So my question is:
How can I use an object as initializer for custom hooks without adding complexity/state or inviting future problems?
I half expect that the useState option will be my best choice, but since I am new to hooks there might still be something that eluded my understanding so far.

Categories

Resources