React hooks callback ref pattern - javascript

I often face this situation with react callbacks:
const MyComponent = ({ onLoad }) => {
useEffect => {
// some stuff
onLoad(/* args */);
}, [onLoad]);
return (<div />);
}
The problem is. I consider my component should only load one time. And with useEffect, i have to set onLoad in the dependencies, this cause any change to the onLoad prop to trigger the effect.
I generally solve this issue with a ref
const MyComponent = ({ onLoad: _onLoad }) => {
const onLoadRef = useRef(_onLoad);
onLoadRef.current = _onLoad;
useEffect => {
// some stuff
onLoadRef.current(/* args */);
}, []); // No dependencies anymore
return (<div />);
}
It works well and solve a lot of similar problems, but i find it a bit ugly, and not really beginner-friendly. I wonder if there is better solutions, or if what i do is an anti-patern ?

As from the comments above: This is a good resource for how to use useEffect
https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount/
This article specifically highlights the main reasons on why you need to think of useEffect differently from the Class Component lifecycle methods.
We often times do some setup when the component first mounts like a
network call or a subscription. We have taught ourselves to think in
terms of "moments in time" with things like componentDidMount(),
componentDidUpdate(), and componentWillUnmount(). It's natural to take
that prior knowledge of React and to seek 1:1 equivalents in hooks. I
did it myself and I think everyone does at first. Often times I'll
hear in my workshops...
"What is the hooks equivalent to [some lifecycle method]?"
The quick answer is that hooks are a paradigm shift from thinking in
terms of "lifecycles and time" to thinking in terms of "state and
synchronization with DOM". Trying to take the old paradigm and apply
it to hooks just doesn't work out very well and can hold you back.
It also gives a good run through of the useEffect and an example of converting from a Class Component to hooks.
Another good source is https://overreacted.io/a-complete-guide-to-useeffect/ from Dan Abramov. I definitely recommend this even though it's very long to read. It really helped me when I first got started using hooks to think about them the right way.
Here's a small excerpt from the beginning of the article.
But sometimes when you useEffect, the pieces don’t quite fit together.
You have a nagging feeling that you’re missing something. It seems
similar to class lifecycles… but is it really? You find yourself
asking questions like:
🤔 How do I replicate componentDidMount with useEffect?
🤔 How do I correctly fetch data inside useEffect? What is []?
🤔 Do I need to specify functions as effect dependencies or not?
🤔 Why do I sometimes get an infinite refetching loop?
🤔 Why do I sometimes get an old state or prop value inside my effect?
When I just started using Hooks, I was confused by all of those
questions too. Even when writing the initial docs, I didn’t have a
firm grasp on some of the subtleties. I’ve since had a few “aha”
moments that I want to share with you. This deep dive will make the
answers to these questions look obvious to you.
To see the answers, we need to take a step back. The goal of this
article isn’t to give you a list of bullet point recipes. It’s to help
you truly “grok” useEffect. There won’t be much to learn. In fact,
we’ll spend most of our time unlearning.
It’s only after I stopped looking at the useEffect Hook through the
prism of the familiar class lifecycle methods that everything came
together for me.
In terms of the original question above, using refs is a good way to be able to not have your effect have specific functions and values as dependencies.
In particular they are good if you "you want to read the latest rather than captured value inside some callback defined in an effect"
For this example from the poster:
const MyComponent = ({ onLoad: _onLoad }) => {
const onLoadRef = useRef(_onLoad);
onLoadRef.current = _onLoad;
useEffect => {
// some stuff
onLoadRef.current(/* args */);
}, []); // No dependencies anymore
return (<div />);
}
This is a completely valid way of doing things, though depending on the args that onLoad takes, and how it works, it might be a good idea to add extra items to the dependency array to make it always in sync.
You could abstract away the wonkiness of the useRef here, but unfortunately the rules of hooks eslint plugin wouldn't recognize it as a ref. It would work, you'd just need to add the onLoadRef to the dependency array, though it would never cause the effect to re-run. It's similar to things like dispatch from react-redux where you know it is stable, but the eslint plugin can't know that.
function useRefUpdater(value) {
const ref = useRef(value);
// I forget where I saw that you should change the ref in a useEffect
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
const MyComponent = ({ onLoad: _onLoad }) => {
const onLoadRef = useRefUpdater(_onLoad)
useEffect(() => {
// some stuff
onLoadRef.current(/* args */);
}, []);
// React Hook useEffect has a missing dependency: 'onLoadRef'. Either include it or remove the dependency array.
return <div />;
};

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.

Is it safe to use hooks in createSelector?

I have just found out that I can use data hooks in createSelector functions and it works. An example:
// This is a normal hook
const useUserReducer = () => {
const userAccessData = useSelector(state => state?.userAccessData)
return userAccessData
}
// Here I use the hook as first argument!
export const useUserReducerFromCreateSelector = createSelector(useUserReducer, (result) => {
console.log(result) // userAccessData printed correctly
return result
})
Then I use it in my component as a normal hook:
const Component = () => {
const result = useUserReducerFromCreateSelector([])
console.log(result) // userAccessData printed correctly
return (
<>
{JSON.stringify(result)}
</>
)
}
I dont see any documentation about this, so I wonder if its safe to use it. It would help me a lot creating reusable selectors.
(I tested while changing the state at various points in time and I always see the correct state)
It is certainly an abuse if it is working. createSelector is only supposed to be a pure state selector function, so naming the returned selector function like a React hook, i.e. useUserReducerFromCreateSelector, is likely to cause some linter warnings eventually.
The potential issue is that Reselect and createSelector creates memoized selector functions. If the input value to a selector doesn't change, then the selector function returns the previously computed selector value. This means that a selector using a React hook like this potentially conditionally calling a React hook which is a violation of the Rules of Hooks.
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,
before any early returns. 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.)
Only Call Hooks from React Functions
Don’t call Hooks from regular JavaScript functions. Instead, you
can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
By following this rule, you ensure that all stateful logic in a
component is clearly visible from its source code.
I don't consider it safe to use any React hook in a selector function like this.
Split out the logic of selecting the state from the useUserReducerFromCreateSelector hook to be used in your selector functions.
Example:
const userAccessData = state => state?.userAccessData || {};
const computedUserAccessData = createSelector(
[userAccessData],
data => {
// logic to compute derived state, etc...
return newUserAccessData;
},
);
I was really intrigued seeing this particular use in the redux-toolkit github repo issues and nobody complaining about it so I decided to ask the same question in the reselect github page.
Here is the response of Mark Erikson (redux maintainer):
No, this is not safe!
You're technically getting away with it because of how you're using
that in a component. But if you were to try to use that selector
outside of a component, it would break.
I'd really recommend sticking with keeping these concepts separate.
Write and name selectors as selectors. Write and name hooks as hooks.
Don't try and mix the two :)
To be clear, the code that you wrote above should run. It's ultimately
"just" composition of functions and calling them in a particular
order.
But given how hooks work, and how selectors work, it's best to keep
those concepts separate when writing the code to avoid confusion.

Why would I use 'useReduce' in react while implementing global scoped states?

So after coding a complete web application with react with props-drilling so deep like you've never seen before, you shouldn't, I've decided to give react 'contexts' a go. I read the react documentation, and I've decided to make my states(which seriously need help because they're so deeply rooted into components after components) in a separate file:
-create a 'context.js' filee
-Inside I'm gonna have: [and notice I have two states inside]
import { createContext } from 'react'
export const context = createContext();
export default function ContextProvider (props) {
const [state1, setState1] = useState();
const [state2, setState2] = useState();
return (
<context.provider value={[state1, setState1, state2, setState2]}>
props.children
</context.provider>
)
}
Now all I have to do is import this component where I want to make these states visible, use destructuring like so:
//inside a functional component
...
const [state1, setState1] = useContext(context); //assuming I only need state1 in this component
...
Now my question is how efficient this approach is. And in some blogs I read about using context with hooks, every one I read used 'useReduce' hook. Could someone explain why 'useReduce' was used, that is what problem does it solve and how. That'll be appreciated.
This is not efficient at all: any time you call setState1() it will cause re-render of <ContextProvider>, and thus of entire app, which is presumably wrapped into <ContextProvider>.
Consider to check my library for global state management with context: https://www.npmjs.com/package/#dr.pogodin/react-global-state :)
I feel like this this question is pretty relevant here: Fast changing states without Redux (at the end of the day, useReducer and useState should be equally efficient in terms of performance? I'm not an expert here though).
Also, useReducer is better for handling complex state, which usually happens when you start using Contexts or Redux.
If you want to have an object like this in your state
user: {
options: { ... },
settings: { ... }
}
You REALLY should use useReducer (especially if you're in a situation where changing in the options relies on knowing what the settings are) and have the complicated logic be handled through the dispatch, rather than jumping through hoops and trying to get it to work with useState. At the end of the day, they do pretty much the same thing in that they "handle state", if you want to know more about the nuances, best way to do it is to read the docs: https://reactjs.org/docs/hooks-reference.html#usereducer.
Again, I'm not an expert but I've made a fair few attempts at looking into "performance in React" and from what I've gathered, rerenders due to state change are rarely the problem. Usually you'll have something in a child component that relies on performing an expensive calculation on a prop, and the correct solution isn't to "prevent the rerender" but instead to do something like "move the expensive calculation up" so it only needs to happen once per render rather than n children per render.

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