Change handlers in react hooks - javascript

For a while a would like to start using react function components with react hooks instead of class extending react component, but there is one thing which discourages me. Here is an example from very first intro of react hooks:
import React, { useState } from 'react'
import Row from './Row'
export default function Greeting(props) {
const [name, setName] = useState('Mary');
function handleNameChange(e) {
setName(e.target.value);
}
return (
<section>
<Row label="Name">
<input
value={name}
onChange={handleNameChange}
/>
</Row>
</section>
)
}
There is a handleNameChange declaration used as a change handler for input. Let's imagine that Greeting component updates really really frequently because of some reason. Does change handle initialize every time on every render? From JavaScript aspect of view how bad is that?

Does change handle initialize every time on every render?
Yes. That's one reason for the useCallback hook.
From JavaScript aspect of view how bad is that?
Fundamentally, it's just creating a new object. The function object and the underlying function code are not the same thing. The underlying code of the function is only parsed once, usually into bytecode or a simple, quick version of compilation. If the function is used often enough, it'll get aggressively compiled.
So creating a new function object each time creates some memory churn, but in modern JavaScript programming we create and release objects all the time so JavaScript engines are highly optimized to handle it when we do.
But using useCallback avoids unnecessarily recreating it (well, sort of, keep reading), by only updating the one we use when its dependencies change. The dependencies you need to list (in the array that's the second argument to useCallback) are things that handleNameChange closes over that can change. In this case, handleNameChange doesn't close over anything that changes. The only thing it closes over is setName, which React guarantees won't change (see the "Note" on useState). It does use the value from the input, but it receives the input via the arguments, it doesn't close over it. So for handleNameChange you can leave the dependencies blank by passing an empty array as the second argument to useCallback. (At some stage, there may be something that detects those dependencies automatically; for now, you declare them.)
The keen-eyed will note that even with useCallback, you're still creating a new function every time (the one you pass in as the first argument to useCallback). But useCallback will return the previous version of it instead if the previous version's dependencies match the new version's dependencies (which they always will in the handleNameChange case, because there aren't any). That means that the function you pass in as the first argument is immediately available for garbage collection. JavaScript engines are particularly efficient at garbage collecting objects (including functions) that are created during a function call (the call to Greeting) but aren't referenced anywhere when that call returns, which is part of why useCallback makes sense. (Contrary to popular belief, objects can and are created on the stack when possible by modern engines.) Also, reusing the same function in the props on the input may allow React to more efficiently render the tree (by minimizing differences).
The useCallback version of that code is:
import React, { useState, useCallback } from 'react' // ***
import Row from './Row'
export default function Greeting(props) {
const [name, setName] = useState('Mary');
const handleNameChange = useCallback(e => { // ***
setName(e.target.value) // ***
}, []) // *** empty dependencies array
return (
<section>
<Row label="Name">
<input
value={name}
onChange={handleNameChange}
/>
</Row>
</section>
)
}
Here's a similar example, but it also includes a second callback (incrementTicks) that does use something it closes over (ticks). Note when handleNameChange and incrementTicks actually change (which is flagged up by the code):
const { useState, useCallback } = React;
let lastNameChange = null;
let lastIncrementTicks = null;
function Greeting(props) {
const [name, setName] = useState(props.name || "");
const [ticks, setTicks] = useState(props.ticks || 0);
const handleNameChange = useCallback(e => {
setName(e.target.value)
}, []); // <=== No dependencies
if (lastNameChange !== handleNameChange) {
console.log(`handleNameChange ${lastNameChange === null ? "" : "re"}created`);
lastNameChange = handleNameChange;
}
const incrementTicks = useCallback(e => {
setTicks(ticks + 1);
}, [ticks]); // <=== Note the dependency on `ticks`
if (lastIncrementTicks !== incrementTicks) {
console.log(`incrementTicks ${lastIncrementTicks === null ? "" : "re"}created`);
lastIncrementTicks = incrementTicks;
}
return (
<div>
<div>
<label>
Name: <input value={name} onChange={handleNameChange} />
</label>
</div>
<div>
<label>
Ticks: {ticks} <button onClick={incrementTicks}>+</button>
</label>
</div>
</div>
)
}
ReactDOM.render(
<Greeting name="Mary Somerville" ticks={1} />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
When you run that, you see that handleNameChange and incrementTicks were both created. Now, change the name. Notice that nothing is recreated (well, okay, the new ones aren't used and are immediately GC'able). Now click the [+] button next to ticks. Notice that incrementTicks is recreated (because the ticks it closes over was stale, so useCallback returned the new function we created), but handleNameChange is still the same.

Looking strictly from a JavaScript perspective (ignoring React), defining a function inside a loop (or inside another function that's called regularly) is unlikely to become a performance bottleneck.
Take a look at these jsperf cases. When I run this test, the function declaration case runs at 797,792,833 ops/second. It's not necessarily a best practice either, but it's often a pattern that falls victim to premature optimization from programmers who assume that defining a function must be slow.
Now, from React's perspective. It can become a challenge for performance is when you are passing that function to child components who end up re-rendering because it's technically a new function each time. In that case it becomes sensible to reach for useCallback to preserve the identity of the function across multiple renders.
It's also worth mentioning that even with the useCallback hook, the function expression is still redeclared with each render, it's just that it's value is ignored unless the dependency array changes.

Related

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.

Missing dependencies in useEffect() really cause stale data?

I'm asking this question to confirm my understanding of some concept.
The React doc is emphatic about including all dependencies used in the useEffect() callback. As explained in the doc:
Otherwise, your code will reference stale values from previous renders.
I kind of understand where this explanation is coming from. But what concerns me is the "stale value" part. I don't see any possible way stale values can occur due to a missing dependency. My argument is also backed by what's in the doc:
Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render. This is intentional. In fact, this is what lets us read the count value from inside the effect without worrying about it getting stale.
As for my understanding, if we miss listing a dependency, the effect will not run after a render caused by that dependency change because React doesn't think the effect depends on it. If I take a guess, it may be the situation when the doc refers to as referencing stale data. Indeed, that data is stale in the effect code. However, the effect callback doesn't run in the first place. I won't notice that data being stale until the effect runs. If that matters, I will first figure out why the effect didn't run and resolve the issue. I'd be confounded not by the data being stale, but by the fact that the effect wouldn't run.
What's more, let's suppose the effect runs after a render caused by another dependency change. In this scenario, even if we missed a dependency, we wouldn't read stale data thanks to the aforementioned closure reason. I experimented a bit to confirm this:
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(10);
useEffect(() => {
console.log(count2);
}, [count1]);
return (
<div className="App">
<div>
Count1: {count1}
<button onClick={() => setCount1(count1 + 1)}>increase</button>
</div>
<div>
Count2: {count2}
<button onClick={() => setCount2(count2 + 1)}>increase</button>
</div>
</div>
);
}
We always get the latest count2, as long as the effect runs. So does my understanding holds?
I want to know why React recommends the inclusion of all dependencies so much. People typically use the dependency array with the intention to bypass some effect running. If they omit a dependency, it's likely what they want. If it's a slip, they will easily notice the effect not running and take action.
I've slightly modified your example to show stale values.
Effects are often used for async reasons, so this is not unusual.
Basically it's down to closures, a first render of useEffect will create a closure on count1 & count2, if the effect is not re-run on all dependencies then these closures will stay (stale).
Clicking on count1 then means the useEffect is called again, a new instance of the setInterval is created, with a fresh (none stale) copy of count1 & count2. Because count2 is not in the dependency array, Clicking on count2 will mean the a new setInterval is not created, and stale copies of count1 & count2 are kept in memory.
To be fair, this is probably one area of Hooks that can be tricky to understand. It's easy to think of Hook Components as if there classes with data been part of the Object. But in reality hook Components are only render functions, useState / useEffect etc, are kind of side loaded into the render function pipeline. In comparison a React Class Component has it's data stored with the object instance, so this.xyz is never stale.
const {useState, useEffect} = React;
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(10);
useEffect(() => {
const tm = setInterval(() => {
console.log(count1, count2);
}, 1000);
return () => clearInterval(tm);
}, [count1]);
return (
<div className="App">
<div>
Count1: {count1}
<button onClick={() => setCount1(count1 + 1)}>increase</button>
</div>
<div>
Count2: {count2}
<button onClick={() => setCount2(count2 + 1)}>increase</button>
</div>
</div>
);
}
ReactDOM.render(<App/>,document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
<p>Increase count2, see the console not update until you increase count1,.</p>
<p>Add count2 to the dependancy, and then everything will keep in sync</p>
Your example code is showing the simplest example, in an extremely simple scenario. By default, useEffect will run on every rerender of your component. Using the dependency array, the internal function only runs when one of those values changes. I have personally run into scenarios where I forgot a dependency, my effect fired, and a value in my function had stale data. This is likely because there were several bits of process going on at once, when the one change triggered the effect, and the other piece of data hadn't caught up yet. Switching to using useReducer for controlling multiple bits of state simultaneously, instead of multiple useState, helped in some of those situations, but ultimately the dependency array kept it in line. Also (and I haven't confirmed this), the framework code for useEffect probably makes heavy use of closures so, again, it's about making sure it's referencing the data points at the right juncture in process.

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.

React - use the exact same component instance many times in the DOM

I have a React component that's a div, some styling, and text. That exact component with the exact same value is used many, many times in the DOM. I am trying to figure out how to create just one instance of that component, and simply present the exact same instance everywhere that I need it. I tried use memoization, but that seems to be memoizing the instance itself.
Just to be explicitly clear, I'd like to memoize the component such that calling the function with the same arguments from anywhere will return the exact same instance every single time.
const Component = React.memo((value) => (<div>...</div>));
Calling this seems to memoize the particular instance, but it will still create a brand new instance every single time it's invoked from a different place in the Virtual DOM, even if the arguments are identical. As I understand it, React.Memo is just a wrapper around a particular instance.
I've thought of some hacky ways to do this, but wanted to ask before exploring that further.
You can use Context https://reactjs.org/docs/context.html
const AppContext = React.createContext(null);
const App = () => (
<AppContext.Provider value={...//your component} >
//...
</AppContext.Provider>)
)
const ChildComponent = () => {
const Component = useContext(AppContext);
return (
<div>
//...
{Component}
</div>
)
}

Function inside functional component in React hooks - Performance

Need suggestion on having function within a functional component in react Hooks.
As far as I researched, many are saying it is bad practice
because it creates nested/inner function every time we call re-render.
After doing some analysis,
I found we can use onClick={handleClick.bind(null, props)} on the element and place the function outside the functional component.
Example:
const HelloWorld = () => {
function handleClick = (event) => {
console.log(event.target.value);
}
return() {
<>
<input type="text" onChange={handleClick}/>
</>
}
}
Please advise if there is any alternative way.
Thanks in advance.
Don't worry about it
Don't worry about creating new functions on each render. Only in edge cases does that impede your performance.
Setting onClick handlers are not one of those, so just create a new function on each render.
However, when you need to make sure you use the same function every time, you can use useCallback
Why not use useCallback for onClick
Here is a reason why you shouldn't bother with useCallback for onClick handlers (and most other event handlers).
Consider the following code snippets, one without useCallback:
function Comp(props) {
return <button onClick={() => console.log("clicked", props.foo)}>Text</Button>
}
and one with useCallback:
function Comp(props) {
const onClick = useCallback(() => {
console.log("clicked", props.foo)
}, [props.foo])
return <button onClick={onClick}>Text</Button>
}
The only difference in the latter is that React doen's have
to change the onClick on your button if props.foo remains the same.
Changing the callback is a very cheap operation, and it's not at all
worth complicating your code for the theoretical performance improvement it gives.
Also, it's worth noting that a new function is still created on every render
even when you use useCallback, but useCallback will return the old one
as long as the dependencies passed as the second argument are unchanged.
Why ever use useCallback
The point of using useCallback is that if you compare two functions with reference
equality, fn === fn2 is true only if fn and fn2 point to the same function in memory.
It doesn't matter if the functions do the same.
Thus, if you have memoisation or otherwise only run code when the function changes,
it can be useful to use useCallback to use the same function again.
As an example, React hooks compare old and new dependencies, probably using Object.is.
Another example is React.PureComponent, which will only re-render when props or state have changed. This can be useful for components that use a lot of resources to render. Passing e.g. a new onClick to a PureComponent on each render will cause it to re-render every time.
many are saying it is bad practice because it creates nested/inner function every time we call re-render
No, inner functions / closures are so common, there is no problem with them. The engine can heavily optimize those.
The point here is that you pass the function as a prop to the child component. And as the function was "recreated", it does not equal the previous function passed, annd thus the child does rerender (and that's whats bad for performance).
You can resolve that with useCallback, which memoizes the function reference.
Interesting question, me and my coleagues had some worries about this, so I did a test.
I have created 1 Component with Hooks and 1 Component with Class, put some functions there and then render it 1000x times.
The Component with Class looks like this:
export class ComponentClass extends React.PureComponent {
click1 = () => {
return console.log("just a log");
};
render() {
return (
<>
<span onClick={this.click1}>1</span>
</>
);
}
}
The Component with Hooks looks like this:
export const ComponentHook = React.memo((props) => {
const click1 = () => {
return console.log("just a log");
};
return (
<>
<span onClick={click1}>1</span>
</>
);
});
I have added more click handlers to the components and then rendered them some 1000s times, the Class is faster as it does not define the functions each render, if you increase the number of functions defined, then the difference will be bigger:
Here it is a codesandbox so you can test the performance Class vs Hooks : https://codesandbox.io/s/hooks-vs-class-forked-erdpb
useCallback
You can use useCallback feature :
const HelloWorld = ({ dispatch }) => {
const handleClick = useCallback((event) => {
dispatch(() => {console.log(event.target.value)});
})
return() {
<>
<input type="name" onChange={handleClick}/>
</>
}
}
useCallback will return a memoized version of the callback that only
changes if one of the dependencies has changed. This is useful when
passing callbacks to optimized child components that rely on reference
equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
For further details visit react documentation reference: React useCallback
Old Solution
First solution:
To pass the your handleClick function to your functional component.
const HelloWorld = (props) => {
return() {
<>
<input type="name" onChange={props.handleClick}/>
</>
}
}
Second solution:
To define your function outside of your functional component.
Inspired by #tibbus 's benchmark, I made this one that tests the perfomance using or not the useCallback hook. After several executions, it seams that the use of useCallback can be very important for high frequency rendering.
Execution 1
Execution 2
Execution 3
https://codesandbox.io/s/usecallback-vs-raw-definition-xke9v?file=/src/App.js
As per React Documentation (ending part),
The problem with latter syntax is that a different callback is created
each time the LoggingButton renders. In most cases, this is fine.
However, if this callback is passed as a prop to lower components,
those components might do an extra re-rendering. We generally
recommend binding in the constructor or using the class fields syntax,
to avoid this sort of performance problem.
Class field syntax:
class LoggingButton extends React.Component {
// This syntax ensures `this` is bound within handleClick.
// Warning: this is *experimental* syntax.
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
arrow function in the callback syntax:
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
I would honestly just use a class component in these cases. I'm aware of premature optimization, but creating a new function each time does just seem like extravagant wastefulness without much of a maintainability upside. tibbus has demonstrated the perf hit, and inline functions are arguably less readable than class methods. All you're losing out is the slick feeling of writing a functional component.
Just useCallback
Why would you need to define a function inside a component and not anywhere else? Because you either have to pass it to another child compononent, or you have to use it in an effect, memo, or another callback. For any of those cases if you dont wrap your function in useCallback you will be passing a new function and causing the component to rerender, the memo to re-run, the effect to re-run, or the callback to re-define.
You can never avoid the performance hit of redefining the function itself, but you can avoid the performance hit of performing any computation that has that function as a dependency to know if it has to run or not (be it a component or hook).
So... just wrap every function in your component in useCallback and forget about it, never seen a single in case in which this would cause any harm. If you can define the function outside the component, thats always better.

Categories

Resources