One of the most important aspects about performance in React applications is how your components react to changes. After introducing hooks in 2019, the definition of components using functions became the new norm.
They came with an interesting side effect: the entire function is executed any time React detects a potential change in your component. Before, components defined with classes only executed certain methods like the lifecycle ones (componentDidMount
, etc) and the well known render
method.
To manage it, React added the amazing useEffect
hook. However, it's important to keep in mind that functions executes all the code inside when they are called.
Initialize a state in React
You can initialize a state in React using the useState
hook:
import { useState } from "react";
const MyComponent = () => {
const [counter, setCounter] = useState(0);
// Increment the given counter
const incrementCounter = () => setCounter(counter + 1);
return (
<section aria-label="Counter">
<button onClick={incrementCounter}>Increment</button>
<output>{counter}</output>
</section>
);
};
MyComponent
defines a new state to manage the current counter value. Following the previous statement, any time React detects a potential change, it calls MyComponent
function and compares the result of the execution with the previous state of the application.
Now, taking a deep look to this function, there are multiple calls and defintions:
- Call to
useState
- Define the
incrementCounter
function - Call JSX method under the hood
Apart from that, there's a tiny detail that is usually forgotten. 0
is also evaluated. So, what happens if you need to call a function to calculate the initial state value?
Lazy initial state
Now, let's check the following code:
import { useState } from "react";
import { initState } from "./utils";
const MyComponent = () => {
const [value, setValue] = useState(initState());
// ...
};
In this case, useState
doesn't receive a static value but a function result as parameter. Note that the initState
function is called any time React calls MyComponent
. However, useState
only use the the result once. After its mounted, next executions of the component will discard the initState
result.
Depending on the complexity of initState
, it may cause some performance issues in MyComponent
even after the first initialization. To avoid it, React allows you to pass a function that will be executed just once:
import { useState } from "react";
import { initState } from "./utils";
const MyComponent = () => {
const [value, setValue] = useState(() => initState());
// ...
};
This trick is called lazy state initialization.
You don't need to be lazy by default
Let's be fair. Fortunately, states are initialized with static values most of the times. Not all applications will benefit from this useState
feature. However, this is one of those difficult performance issues to detect and the solution is quite simple.
Just keep it in mind when you need to call a function to initialize a state. And think it twice if it's a requirement because your component will still need to wait for the result when it's mounted.