There's no doubt that Hooks are one of the most exciting features of the last React updates. They let we work in a project without writing class-based components, allowing use of state and other features.
One important concern that we have to do when developing applications, in general, is performance.
React already has a "diffing" algorithm to avoid unnecessary DOM render, but in some cases, we want to avoid unnecessary executions of the component's render function to increase performance. In the case of functional components, render function is itself.
I created the following project to demonstrate how we can optimize React functional components with Hooks:
1. The application
This application is simple!
-
Homeis the root component; -
Component1displays the currentname; -
Component2displays the currentsurname; - The root component has a input field for
nameand another forsurname; - The root component stores the
nameandsurnamein a local state (usinguseStatehook); - The root component pass down the property
nametoComponent1andsurnametoComponent2;
Code:
// ./src/pages/index.tsx
import React, { useState } from 'react';
import { Component1, Component2 } from '../components';
export default function Home() {
const [name, setName] = useState('');
const [surname, setSurname] = useState('');
return (
<div className="container">
<label>Name: </label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<label>Surname: </label>
<input type="text" value={surname} onChange={(e) => setSurname(e.target.value)} />
<Component1 name={name} />
<Component2 surname={surname} />
</div>
);
}
// ./src/components/Component1.tsx
import React from 'react';
interface Props {
name: string;
}
export default function Component1({ name }: Props) {
console.log('Component1 :: render', { name });
return (
<div>
<label>Component1: </label>
<p>Name: {name}</p>
</div>
);
}
// ./src/components/Component2.tsx
import React from 'react';
interface Props {
surname: string;
}
export default function Component2({ surname }: Props) {
console.log('Component2 :: render', { surname });
return (
<div>
<label>Component2: </label>
<p>Surname: {surname}</p>
</div>
);
}
2. The first problem
I put a console.log in the Component1 and Component2 to print the properties on them.
So, after typing my name, see what happened!
Component2 prints the console.log message indicating that it was executed unnecessary. The surname property value is empty all the time.
2.1. Solution
To resolve this problem, we just need to use React.memo!
React.memo is a higher-order component and it allows a component to be rendered only if the properties are changed.
// ./src/components/Component2.tsx
...
function Component2({ surname }: Props) {
...
}
export default React.memo(Component2);
So, after the change...
3. The second problem
See what happened when I added a property data of the type object in the Component2.
// ./src/components/Component2.tsx
import React from 'react';
interface Props {
surname: string;
data: Record<string, unknown>;
}
function Component2({ surname, data }: Props) {
console.log('Component2 :: render', { surname, data });
return (
<div>
<label>Component2: </label>
<p>Surname: {surname}</p>
<p>Data: {JSON.stringify(data)}</p>
</div>
);
}
export default React.memo(Component2);
// ./src/pages/index.tsx
...
<Component2 surname={surname} data={{}} />
Component2 prints the console.log message indicating that it was executed unnecessary.
AGAIN !!!
Even if I declare the following way, same problem occurs...
// ./src/pages/index.tsx
...
const data = {};
...
<Component2 surname={surname} data={data} />
Why ???
How to resolve this?
3.1. Solution
One thing about React.memo is that, by default, it will only shallowly compare complex objects in the props object.
Well, every time that the root component renders because state changes, a new instance of object {} was created and pass down to Component2. The shallow comparison of the React.memo detects that the object is different and re-render the Component2.
To resolve this problem, React provides a hook called useMemo. This function receives two arguments, a "create" function and an array of dependencies. useMemo will only execute the "create" function to return a new instance of the data when one of the dependencies has changed.
Let's update the code...
// ./src/pages/index.tsx
import React, { useMemo, useState } from 'react';
...
const data = useMemo(() => ({ surname }), [surname]);
...
<Component2 surname={surname} data={data} />
It's all OK now!
4. The last problem
See what happened when I added a property func of the type function in the Component2.
// ./src/components/Component2.tsx
import React from 'react';
interface Props {
surname: string;
data: Record<string, unknown>;
func: () => void;
}
function Component2({ surname, data, func }: Props) {
console.log('Component2 :: render', { surname, data, func });
return (
<div>
<label>Component2: </label>
<p>Surname: {surname}</p>
<p>Data: {JSON.stringify(data)}</p>
</div>
);
}
export default React.memo(Component2);
// ./src/pages/index.tsx
...
<Component2 surname={surname} data={data} func={() => undefined} />
Component2 still prints the console.log message...
The reason is the same as the previous topic. A new instance of the passed function is created every time that the state changes.
4.1. Solution
To resolve this problem, React provides a hook called useCallback. This function receives two arguments, a function and an array of dependencies. The operation is similiar to useMemo. useCallback will only create a new instance of the function when one of the dependencies has changed.
The final code...
import React, { useCallback, useMemo, useState } from 'react';
import { Component1, Component2 } from '../components';
export default function Home() {
const [name, setName] = useState('');
const [surname, setSurname] = useState('');
const data = useMemo(() => ({ surname }), [surname]);
const func = useCallback(() => undefined, []);
return (
<div className="container">
<label>Name: </label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<label>Surname: </label>
<input type="text" value={surname} onChange={(e) => setSurname(e.target.value)} />
<Component1 name={name} />
<Component2 surname={surname} data={data} func={func} />
</div>
);
}
That's all folks!







Top comments (2)
Great Job dude!
Thank's for sharing!
Great, thanks for sharing!