useState — React Hook

AkashSDas
6 min readMar 27, 2024

--

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.

useState Hook

Following is the signature of useState:

useState signature
useState signature
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.

Examples of using useState
Examples of using useState

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 (by Object.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 use flushSync.
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>;
}
Using set function to re-render another component during rendering of some other component
Using set function to re-render another component during rendering of some other component

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>
);
}

--

--