Read the original article here
According to the official React documentation, Hooks are functions that let you "hook into" React state and lifecycle features from function components. Which means you now have full control of your functional components, just like the other class-based components.
So, if you have a good understanding of what React Hooks are, check out this simple React application.
Getting started
I have published a similar project on my GitHub, you can clone it right here.
Let's get started by initializing a React application with Create React App.
$ npx create-react-app app-name
Then, edit the ./src/App.js
file.
// ./src/App.js
import React, { useState } from "react"
import Counter from "./Counter"
export default function App() {
const [value, setValue] = useState("")
return (
<div>
<input
type="text"
onChange={e => setValue(e.target.value)}
value={value}
/>
<Counter />
</div>
)
}
In the App
component, we are using the Counter
component imported from ./src/Counter.js
file, which we have not created yet. Let's fix it by creating it.
// ./src/Counter.js
import React, { useState, useRef } from "react"
export default function Counter() {
const [counter, setCounter] = useState(0)
const renders = useRef(0)
return (
<div>
<div>Counter: {counter}</div>
<div>Renders: {renders.current++}</div>
<button onClick={() => setCounter(counter + 1)}>Increase Counter</button>
</div>
)
}
In this example, there are two functional components. First, is the App
component which contains useState
hook to control the input value.
Second, is the Counter
component which contains useState
hook to hold the counter state and useRef
hook to count how many times this component updated or re-rendered.
Try to run the app, and play around with it. You shouldn't see a performance issue at the moment. So, let's find out if there is any problem with it.
One big problem
When you press the 'Increase Counter' button several times, the renders counter shows the exact same number as the counter state. Which means the Counter
component updated whenever our counter state changed.
But when you type in the App
component text input, you will see that the renders counter also increased. Which means that our Counter
component rerendered whenever our text input state changed.
So, how can we fix it?
Memoizing components
React 16.6 (and higher) comes with higher order component called React.memo
. Which is very similar to React.PureComponent
but for functional component instead of classes.
Basically, it helps us control when our components rerender.
"In computing, memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again" ― Wikipedia
Let's memoize our Counter
component to prevent unnecessary rerender.
// ./src/Counter.js
import React, { useState, useRef } from "react"
export default React.memo(() => {
const [counter, setCounter] = useState(0)
const renders = useRef(0)
return (
<div>
<div>Counter: {counter}</div>
<div>Renders: {renders.current++}</div>
<button onClick={() => setCounter(counter + 1)}>Increase Counter</button>
</div>
)
})
Easy right? Let's checkout out our new app and you'll see that the Counter
component isn't re-rendered when we type in the text input.
The problem persists
React.memo
is great. But, the problem is not solved yet.
There is one thing to always remember when memoizing React component. When the parent component passed props to a memoized React components, things get a little bit weird.
When passing numbers or strings as props, memoized components will check whether the props are changed or not. The component will only rerender when the numbers of strings are changed.
But when passing functions or objects, memoized components will always rerender when the parent component rerender. This happens because whenever the parent component passes that kind of data, memoized components couldn't check whether that function or object are changed or not.
To prove this, let's try to pass a prop to Counter
component.
// ./src/App.js
import React, { useState } from "react"
import Counter from "./Counter"
export default function App() {
const [value, setValue] = useState("")
return (
<div>
<input
type="text"
onChange={e => setValue(e.target.value)}
value={value}
/>
<Counter greeting="Hello world!" />
</div>
)
}
In this case, we pass greeting
prop which contains a string. Then, try to run the app and you'll see that our app will run as we expected. Now, try to pass a function or object.
// ./src/App.js
import React, { useState } from "react"
import Counter from "./Counter"
export default function App() {
const [value, setValue] = useState("")
return (
<div>
<input
type="text"
onChange={e => setValue(e.target.value)}
value={value}
/>
<Counter
addHello={() => setValue(value + "Hello!")}
myObject={{ key: "value" }}
/>
</div>
)
}
You will notice that your Counter
rerender whenever you type something in the text field. So, how can we fix this... Again?
Memoizing functions
We can use useCallback
hook to memoize our callback that we pass through the props.
useCallback
hook returns a memoized version of our function that only changes if one of the dependencies have changed. In other words, our function will never be recreated unless the state value has changed. Let's implement this in our app.
// ./src/App.js
import React, { useState, useCallback } from "react"
import Counter from "./Counter"
export default function App() {
const [value, setValue] = useState("")
const addHello = useCallback(() => setValue(value + "Hello!"), [value])
return (
<div>
<input
type="text"
onChange={e => setValue(e.target.value)}
value={value}
/>
<Counter addHello={addHello} myObject={{ key: "value" }} />
</div>
)
}
This method is very useful when you have more than one state hook. The memoized functions are updated only when the chosen state changed. To prove this, let's add another input field.
// ./src/App.js
import React, { useState, useCallback } from "react"
import Counter from "./Counter"
export default function App() {
const [value, setValue] = useState("")
const [newValue, setNewValue] = useState("")
const addHello = useCallback(() => setValue(value + "Hello!"), [value])
return (
<div>
<input
type="text"
onChange={e => setValue(e.target.value)}
value={value}
/>
<input
type="text"
onChange={e => setNewValue(e.target.value)}
value={newValue}
/>
<Counter addHello={addHello} myObject={{ key: "value" }} />
</div>
)
}
Now, when we type in the new text field, the Counter
component doesn't rerender. Because our memoized function only being updated whenever the value
state has changed.
We are successfully memoized our function with the useCallback
hook. But, the problem still persists though...
Memoizing objects
Now we know how to memoize our function, but there is one last thing you should know about memoizing.
Currently, our Counter
component is still rerendered whenever the state has changed. Its because the myObject
props are still not memoized yet. So, how can we memoize that kind of stuff?
useMemo
hook let you memoize a value (including objects) by passing a "create" function and an array of dependencies. The value will only recompute when one of the dependencies has changed (just like useCallback
hook).
Let's apply that and see what happened.
// ./src/App.js
import React, { useState, useCallback } from "react"
import Counter from "./Counter"
export default function App() {
const [value, setValue] = useState("")
const [newValue, setNewValue] = useState("")
const addHello = useCallback(() => setValue(value + "Hello!"), [value])
const myObject = useMemo(() => ({ key: "value" }), [])
return (
<div>
<input
type="text"
onChange={e => setValue(e.target.value)}
value={value}
/>
<input
type="text"
onChange={e => setNewValue(e.target.value)}
value={newValue}
/>
<Counter addHello={addHello} myObject={myObject} />
</div>
)
}
By adding these changes, you're now able to pass props to a memoized component without losing good performance.
Top comments (5)
your last exmaple or example with useCallback for prevent re-rendering is not working, when every input changed, counter component is re-rendered.
If you want prevent re-rendering counter compnent when input value changed, changed the useCallback's sencond array paramter [value] to []
Input changes means value is changed, so useCallback always return new addHello, counter component receive new referenced function so it always re-rendered, that is why your last example is not working.
Hmm, I don't think so...
We don't use useCallback to prevent re-rendering counter component. We use it to memoize our function (the one that add "Hello!" to the "value" state). When you memoize your function with useCallback, the component is still being re-rendered but the returned value of the memoized function is cached. This is very helpful if you want to do expensive task that requires a lot of time.
We put [value] because we always want to update our function when the "value" state changes, because we need "value" state inside our function. So, our function is still being memoized unless the "value" state has changed.
But thanks for the feedback anyway 😃
Thanks
After memoising the Counter component with useCallback and useMemo is there still a need to use React.memo?
Yes, if you want to pass data to the child component via props.