useState
is by far the mostly used (sometimes overused) hook among other hooks. It allow us to create reactive values and ability to update them.
Following is the signature of useState
:
import { useState } from "react";
// Simple usage of useState
export default function App(): JSX.Element {
const [count, setCount] = useState(0);
function increment(): void {
setCount(count + 1);
}
return (
<section>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</section>
);
}
It takes a single argument which can either be a value (initial state) or a function (initializer function) that returns the initial value. useState
returns the reactive value with current state, and a set
function which can be used to update the value and trigger a re-render.
Initializer function should be a pure function and shouldn’t take any arguments. 🚨 In Strict Mode (development), React will call the initializer function twice.
In the set function, we can either pass a value (next state), or a function (updater function) which will be a pure function that takes previous state as argument and returns the next state.
React will put our updater function in a queue and re-render our component. During the next render, React will calculate the next state by applying all of the queued updaters to the previous state. 🚨 In Strict Mode (development), React will call our updater function twice.
Key details to remember while working with set
functions:
- It only updates the state variable for the next render
import { useState } from "react";
// Multiple useState calls but count value is same in same execution
export default function App(): JSX.Element {
const [count, setCount] = useState(0);
function increment(): void {
setCount(count + 1); // 0+1=1
setCount(count + 1); // 0+1=1
setCount(count + 1); // 0+1=1
setCount(count + 1); // 0+1=1
setCount((prev) => prev + 1); // 0+1=1
setCount((prev) => prev + 1); // 1+1=2
setCount((prev) => prev + 1); // 2+1=3
setCount((prev) => prev + 1); // 3+1=4
// React puts your updater functions in a queue. Then,
// during the next render, it will call them in the same order:
//
// prev => prev + 1 will receive 0 as the pending state and return 1 as the next state.
// prev => prev + 1 will receive 1 as the pending state and return 2 as the next state.
// prev => prev + 1 will receive 2 as the pending state and return 3 as the next state.
// prev => prev + 1 will receive 3 as the pending state and return 4 as the next state.
//
// There are no other queued updates, so React will store 4 as the current state in the end.
}
return (
<section>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</section>
);
}
- If the new value provided is identical to the current
state
(byObject.is
) then React will skip re-rendering the component and its children. Although in some cases React may still need to call your component before skipping the children, it shouldn’t affect our code. - React batches state updates. It updates the screen after all the event handlers have ran, and have called their
set
functions. This prevents multiple re-renders during a single event. In rare case we would want to force React to update the screen earlier (eg: access DOM node), and in such a case we’ve useflushSync
.
import { useState } from "react";
import { flushSync } from "react-dom";
// Use flushSync for emergency re-render
export default function BasicUsage(): JSX.Element {
const [count, setCount] = useState(0);
function increment(): void {
console.log(`Count: ${count}`);
setCount(count + 1);
console.log(`Count: ${count}`);
flushSync(function () {
setCount(count + 1);
console.log(`Count [flushSync]: ${count}`);
});
console.log(`Count: ${count}`);
setCount(count + 1);
console.log(`Count: ${count}`);
}
console.log("Render");
return (
<section>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</section>
);
}
// Output:
// Render
// Render
// Count: 0
// Count [flushSync]: 0
// Render
// Render
// Count: 0
// Render
// Render
- Calling the
set
function during rendering is only allowed from within the currently rendering component. React will discard its output and immediately attempt to render it again with the new state. This pattern is rarely needed, but we can use it to store information from the previous renders. Avoid this pattern.
import { Dispatch, SetStateAction, useState } from "react";
// Cannot call setter of other component during rendering
// @ref - https://react.dev/reference/react/useState#storing-information-from-previous-renders
export default function App(): JSX.Element {
const [count, setCount] = useState(0);
function increment(): void {
setCount((prevCount) => prevCount + 1);
}
return (
<section>
<button onClick={increment}>Increment</button>
<CountLabel count={count} updateCount={setCount} />
</section>
);
}
type CountLabelProps = {
count: number;
updateCount: Dispatch<SetStateAction<number>>;
};
function CountLabel(props: CountLabelProps): JSX.Element {
const { count, updateCount } = props;
const [previousCount, setPreviousCount] = useState(count);
if (count !== previousCount) {
setPreviousCount(count);
updateCount((prev) => prev + 1);
}
return <p>Count: {count}</p>;
}
Updating states
State should always be updated using the set
function. In case of objects and array, we’ve to pass the new object/array.
import { useState } from "react";
import { faker } from "@faker-js/faker";
// Updating different types of state
type Info = {
username: string;
age: number;
isMarried: boolean;
};
function createInitialState(): Info {
return {
username: faker.person.firstName(),
age: faker.number.int(),
isMarried: faker.datatype.boolean(),
};
}
export default function App(): JSX.Element {
// This is a bad practice since it will create a new object every time the component re-renders.
// const [info, setInfo] = useState<Info>(createInitialState());
//
// Instead, we should pass the initializer function which will be called only once
// during the initial render.
const [info, setInfo] = useState<Info>(() => createInitialState());
const [friends, setFriends] = useState<string[]>([]);
// Mutate object
function updateUsername(): void {
setInfo((prev) => ({
...prev,
username: faker.person.firstName(),
}));
}
// Mutate array
function addFriend(): void {
setFriends((prev) => [...prev, faker.person.firstName()]);
}
return (
<section>
<p>Username: {info.username}</p>
<p>Age: {info.age}</p>
<p>Is Married: {info.isMarried ? "Yes" : "No"}</p>
<button onClick={updateUsername}>Update Username</button>
<button onClick={addFriend}>Add Friend</button>
<ul>
{friends.map((friend, index) => (
<li key={index}>{friend}</li>
))}
</ul>
</section>
);
}
Updating these states manually for objects and array can become cumbersome, especially if they’re nested. Using use-immer
can greatly reduce this complexity and allow us to update objects/arrays as we normally do.
pnpm add use-immer
import { faker } from "@faker-js/faker";
import { useImmer } from "use-immer";
// Updating different types of state
type Info = {
username: string;
age: number;
isMarried: boolean;
};
function createInitialState(): Info {
return {
username: faker.person.firstName(),
age: faker.number.int(),
isMarried: faker.datatype.boolean(),
};
}
export default function App(): JSX.Element {
const [info, setInfo] = useImmer<Info>(() => createInitialState());
const [friends, setFriends] = useImmer<string[]>([]);
// Mutate object
function updateUsername(): void {
setInfo((prev) => {
prev.username = faker.person.firstName();
});
}
// Mutate array
function addFriend(): void {
setFriends((prev) => {
prev.push(faker.person.firstName());
});
}
return (
<section>
<p>Username: {info.username}</p>
<p>Age: {info.age}</p>
<p>Is Married: {info.isMarried ? "Yes" : "No"}</p>
<button onClick={updateUsername}>Update Username</button>
<button onClick={addFriend}>Add Friend</button>
<ul>
{friends.map((friend, index) => (
<li key={index}>{friend}</li>
))}
</ul>
</section>
);
}