Children useCallback dependency hell - javascript

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.

Related

Most React idiomatic way of updating state in sibling component without updating parent component

So to do a very simplified use case, I have three components:
const ParentComponent = (props) => {
return(
<ChildComponentA ...variousPropsHere />
<ChildComponentB ...variousPropsHere />
}
ChildComponentA is an overlay that shows when a user clicks something, we'll call the value "value".
ChildComponentB doesn't need "value" at all. And in fact I want to prevent ChildComponentB from re-rendering based on the data change, because rendering ChildComponentB is extremely expensive, and it has no need of updating.
But ChildComponentA does need the value and needs to re-render based on "value" updating.
I have considered useContext, refs, useReducer, etc, but none of them are working quite how I want - useContext seems like it will update all components that subscribe to it - plus the state would still be managed in the parent, necessitating a re-render there too. Refs won't re-render ChildComponentA. useReducer also seems like it will cause parent re-render (and thus child re-renders). Memoization helps with the speed of ChildComponentB re-rendering, but it's still pretty slow.
At this point I'm leaning towards just using my global redux store, storing the value there, and clearing it out on component dismount. That would allow me to update the value from ChildComponentB, without subscribing ChildComponentB (or ParentComponent) to the value itself, preventing re-render in these very expensive components.
Is there a more elegant way to do this than the way I am describing?
I just really cannot have ChildComponentB re-render, but must have ChildComponentA re-render based on a value change.
Instead of keeping value in Context or State of parent component, keep there callback from ChildComponentA and call it from ChildComponentB.
This way only sibling component will be rerendered.
Example:
const ChildComponentA = () => {
const context = useContext(SomeContextToStoreCallback)
useEffect(() => {
context.callback = (value) => {
// Do something expensive
}
return () => {
context.callback = undefined;
}
}, [])
// rest of the code....
}
const ChildComponentB = () => {
const context = useContext(SomeContextToStoreCallback)
const handleSomeEvent = (value) => {
context.callback?.(value)
}
// rest of code.....
}
This is only a pseudo code, but you've got an idea.
Something similar is used in large libraries like mui-x

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.

Should component itself prevent unwanted useEffect() calls?

Using useEffect() properly is sometimes not that easy. Imagine we have the following simple app using the Counter component:
import { useState, useEffect } from 'react';
const Counter = ({ onOdd, onEven }) => {
const [count, setCount] = useState(0);
useEffect(
() => {
console.log('Inside useEffect()');
if (count % 2 === 0) {
onEven(count);
} else {
onOdd(count);
}
},
[count, onOdd, onEven]
);
return (
<button
type="button"
onClick={() => setCount(count => count + 1)}
>
{count}
</button>
);
}
const App = () => {
const [isDarkMode, setIsDarkMode] = useState(false);
return (
<div style={{
backgroundColor: isDarkMode ? 'black' : 'white',
}}>
<Counter
onOdd={count => console.log(`Odd count: ${count}`)}
onEven={count => console.log(`Even count: ${count}`)}
/>
<button
type="button"
onClick={() => setIsDarkMode(isDarkMode => !isDarkMode)}
>
Toggle dark mode
</button>
</div>
);
}
export default App;
The app does two things:
It includes a Count button that increments its counter by 1. This components allows to inject two functions: onOdd and onEven. Whenever the counter changes, either onOdd or onEven is called, depending on the counter... being odd or even.
There is also a dark mode toggle. The only purpose I added it is to have something that causes the Counter to re-render for other reason than changing the count.
Now, the app works with one quirk - whenever we toggle the dark/light mode, the onOdd or onEven is being called. That's wrong, but understandable - we're creating new functions on each render, so useEffect() is being called.
I can think of 4 ways to fix this behavior:
Remove onOdd and onEven from useEffect() dependencies. It will fix the behavior, but it's considered a problem. The linter would complain about it, as we're losing data integrity. In theory, if we really change these callbacks, they should be re-run, right? That would be "the React way".
Move the callback functions outside of the App component:
const onOdd = count => console.log(`Odd count: ${count}`);
const onEven = count => console.log(`Even count: ${count}`);
const App = () => {
// ...
return (
// ...
<Counter
onOdd={onOdd}
onEven={onEven}
/>
// ...
);
}
This is a good and fast solution, but it's only possible because we don't use hooks or state inside these callbacks. What if we did?
Using useCallback() in App component:
const App = () => {
// ...
const onOdd = useCallback(
count => console.log(`Odd count: ${count}`),
[]
);
const onEven = useCallback(
count => console.log(`Even count: ${count}`),
[]
);
return (
// ...
<Counter
onOdd={onOdd}
onEven={onEven}
/>
// ...
);
}
Memoizing the callback functions in Counter component. If we had thousands of components using Counter component, it would still mean only one place to memoize these functions. I'm not sure if that makes sense though.
How do React gurus approach this problem? I wanted to keep the example as simple as possible, so option #2 will work perfectly and would probably be preferable. But what if we needed to keep these callbacks inside the App component?
Is it always the parent component responsible to memoize all callbacks it passes to the child? If so, is it a recognized pattern to always memoize all functions passed as props (and perhaps any other objects) with useCallback() or useMemo()?
I'm not properly a React Guru, but I consider all first three approaches to have their sweet spot, the 4th does not make sense. The only one to be careful with is the first one, since removing functions from deps, might lead to stale state issues, so if you know what you are doing, you may suppress lint warn ( I do that sometimes and know many others do that as it has been discussed extensiveley here https://github.com/facebook/react/issues/14920 ), otherwise it's better you avoid this approach.
The point number 2 is preferred everytime you have pure functions, always try to place your pure functions out of React components, inside some other folder like utils, misc, etc...
As per point number 3 that's the preferred way to handle functions declared inside React components, always memoize them with *useCallback* ( or useMemo if you need to perform calculations before to return a function ) , and there's nothing bad with doing that in the parent component. If you find yourself having dozens or hundreds of them and fear code pollution, consider that custom hooks let you to organize your code smartly, you could make a custom hook like useMemoizedHandlers inside your App component, where you create and memoize all your handlers and use it like:
const {
handler1,
handler2,
handler3
} = useMemoizedHandlers()
Options 2 and 3 are both absolutely valid and common, used interchangeably depending on whether the function has render cycle dependencies. Option 1 is a big no no. Option 4 is not really memoization at all - you can create stable references from functions passed as props but you cannot memoize the functions themselves as they've already been created anew.
Is it always the parent component responsible to memoize all callbacks it passes to the child?
In an application context I would say yes as this is the only way to enable React.memo on the consuming component's props. However, libraries will often convert functions to stable refs in the child, in case users forget to memoize themselves (or just as improved DX). Again, this is not the same as memoization, but it does mean that you can avoid the dependency issues highlighted in your question.
Is it a recognized pattern to always memoize all functions passed as props (and perhaps any other objects) with useCallback() or useMemo()?
You will find both memoization maxis and minimalists in the React community so it's hard to say that there's an accepted standard. Generally, you can get away with not doing it until you need it, like your example. However, purely from personal experience, once you do it a few times out of necessity it starts to become a habit as it reduces the possibility that bugs like this can occur.

How to give react components dynamic ids, when React runs code twice?

It's a known React behavior that code runs twice.
However, I'm creating a form builder in which I need to be able to give each form input a dynamic Id and use that Id for a lot of other purposes later. Here's a simple code of an input:
const Text = ({placeholder}) => {
const [id, setId] = useState(Math.random());
eventEmitter.on('global-event', () => {
var field = document.querySelector(`#${id}`); // here, id is changed
});
}
But since Math.random() is a side-effect, it's called twice and I can't create dynamic ids for my form fields.
The reason I'm using document.querySelector can be read here.
My question is, how can I create consistent dynamic ids for my inputs?
It seems you think that useState(Math.random()); is the side-effect causing you issue, but only functions passed to useState are double-invoked.
I think the issue you have is that the eventEmitter.on call is the unintentional side-effect since the function component body is also double invoked.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies <-- this
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- not this
To remedy this I believe you should place the eventEmitter.on logic into an useEffect hook with a dependency on the id state. You should also probably use id values that are guaranteed a lot more uniqueness. Don't forget to return a cleanup function from the effect to remove any active event "listeners", either when id updates, or when the component unmounts. This is to help clear out any resource leaks (memory, sockets, etc...).
Example:
import { v4 as uuidV4 } from 'uuid';
const Text = ({placeholder}) => {
const [id, setId] = useState(uuidV4());
useEffect(() => {
const handler = () => {
let field = document.querySelector(`#${id}`);
};
eventEmitter.on('global-event', handler);
return () => {
eventEmitter.removeListener('global-event', handler);
};
}, [id]);
...
}

React useRef or module scope to store UI-independent state

Let's say there is a UI-independent state called currentSelected in React functional component. It stores the currently selected item and will be used at some time.
There are two ways to store the state, useRef hook or module scope out of component.
useRef hook:
function Example() {
const currentSelected = useRef()
useEffect(() => {
// access currentSelected state
})
function handleClick(item) {
currentSelected.current = item
}
return (
<ul>
{items.map(item => <li onClick={() => handleClick(item)}>item.name</li>)}
</ul>
)
}
module scope:
let currentSelected = null
function Example() {
useEffect(() => {
// access currentSelected state
})
function handleClick(item) {
currentSelected = item
}
return (
<ul>
{items.map(item => <li onClick={() => handleClick(item)}>item.name</li>)}
</ul>
)
}
Which method is more suitable for storing UI-independent state like currentSelected?
And what is the application scenario of useRef and module scope in storing state?
========= Update ===========
UI-independence means that you don't want to trigger re-render after updating the state. In contrast, UI related states do this.
The difference between useRef and a module-scope variable
Just for completeness sake, I'll throw in useState as well.
useState: immutable data that's tied to the component instance and triggers render on change through the setter function.
useRef: mutable data that's also tied to the component instance, but doesn't trigger any renders on change.
A module scope variable: mutable data that's tied to the module, which also doesn't trigger any renders on change since it's completely outside of React.
Use-case for useRef
If you're mounting a component more than once, e.g. using it on two pages, useRef will ensure that each component instance has its own mutable value.
// Here, the Example component could be used in multiple places
// and each instance would successfully keep its own state while
// not triggering renders on changes.
function Example() {
const currentSelected = useRef()
useEffect(() => { /* access currentSelected state */ })
return (
<ul>
{items.map(item => (
<li onClick={() => { currentSelected.current = item }}>{item.name}</li>
))}
</ul>
)
}
Use-case for a module scope variable
If you're looking for a singleton-like pattern or a static-like variable, e.g. for some kind of app-wide shared data, then a module scope variable will make this possible, like in any vanilla JS module.
// Here, the count value will be initialized once and then shared between every
// instances across the app, ever.
let count = 0;
function Example() {
// Won't trigger a render, so some Example instance could still show the old value.
count += 1;
return <span>Combined renders of all the Example components: {count}</span>;
}
Note that it won't trigger a render when count changes, so you shouldn't use it like that.
The caveats
If used only once in a place where the component is also only mounted once, both patterns will appear to behave similarly, but in the end, it's a matter of time until something triggers a remount and then you'll be facing a weird bug.
You could also encounter problems when unit testing a module with a module scope variable in it since it may not be properly reset in-between test cases. A quick workaround is to just export the variable and let the test cases change its value, but be careful not to change it anywhere else. Though this should be evaluated as a case-by-case basis.
First one
For myself choose the first one.Because its works as individual inside function.You could use multiple example component as same class/function .
function check(a){
let one = 'overwrite'+a;
console.log(one)
}
check(1);
check(2);//as individual inside only
Second one
Its overwrite currentSelected variable on each example component execute
let one = null
//second one
function check() {
one = 'overwrite';
console.log(one)
}
console.log('Before =' + one)
check();
console.log('After =' + one);
why not use the useState hook. it's perfect for this secnario.
const [currentItem, setCurrentItem] = useState();
...
{items.map(item => <li onClick={() => setCurrentItem(item)}>item.name</li>)}
another problem with your example is that when you change the ref current property in the handleClick it doesnt triggered render, so the useEffect in your function will not run so you cant access the ref.
Your "currentItem" may be UI-independent, but it would be component-dependent. right? If currentItem is related to this component, you should use the React.useState hook. This is just what the state is for.
Global variables(not constants) should be avoided in OOP programming. For example, there would be only one global variable in well-written OOP code, i.e. theApp.

Categories

Resources