Skip to main content

State

State is component specific "memory" and it triggers component re-render when its value is changed.

Creating a state

State is created using the useState hook function. In React, hook functions are named using the convention of starting the function name with use followed by a descriptive term that explains what the hook does (in this case "State").

To use useState, you need to import it from the react library:

import { useState } from 'react';

The following example creates the state variable named firstName and the function setFirstName is used to update state's value. The useState hook function takes one argument that is an initial value of the defined state.

const [firstName, setFirstName] = useState("John");

Type inference works with the useState function. If you provide an initial value, TypeScript will automatically infer the type of the state. However, you can also explicitly define the type by passing a generic type argument to useState:

 const [firstName, setFirstName] = useState<string>("John");

In TypeScript, when creating a state with React's useState and the initial value is unknown, you can define the state using a union type that includes undefined or null. This allows you to initialize the state without a value while still maintaining type safety:

 const [firstName, setFirstName] = useState<string | undefined>();

Updating state

State value is always updated by using the function that you define in your useState hook. Now, in this case the function is setFirstName.

// Update value of the state
setFirstName("Jim");

State can be accessed by using name of a state (in this case firstName).

Do Not update state directly!

danger
firstName = "John";

Always use a function that is defined in the useState hook, then React knows when a state value has changed and re-rendering is needed.

If you try to update the state with a value of an incorrect type, you will get development time TypeScript error. This is good because fixing errors in the development phase is much "cheaper" than fixing them in production:

setFirstName(20);

Error

Counter example

Next, we will create counter example that the Vite starter project's App component already contains. But we will create it from scratch to learn React state.

Counter

Open the Vite project's App.tsx file in the editor and remove all unnecessary code and your code will look like the following:

import './App.css'

function App() {
return (
<>
</>
)
}

export default App

First, we import the useState function from the react package. Then, we call the useState function to create a state. We will name our state to count and initialize its value to zero (0).

import { useState } from 'react'
import './App.css'

function App() {
const [count, setCount] = useState(0)

return (
<>
</>
)
}

export default App

Next, the button is displayed in the return statement and the button text displays the count state value:

import { useState } from 'react'
import './App.css'

function App() {
const [count, setCount] = useState(0)

return (
<>
<button>
count is {count}
</button>
</>
)
}

export default App

Button's onClick event attribute invokes setCount function and increments the count state value by one. The component is re-rendered when the state updates and therefore you can see the new value in the screen.

import { useState } from 'react'
import './App.css'

function App() {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>+</button>
count is {count}
</button>
</>
)
}

export default App
info

When using setCount in the onClick handler, you need to pass a function to the onClick handler.

If you are calling setCount(count + 1) directly within the onClick, it immediately tries to update the state. This causes the component to re-render, and during the re-render, the onClick is executed again, leading to an infinite loop. In this case, you will see the following error in the console:

Infinite loop error

State updates

State updates are asynchronous and batched. In the counter example, the new state depends on the previous value of the state. To make sure that the state is always updated correctly, you should pass a function that updates the state like shown in the following code:

return (
<>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
count is {count}
</button>
</>
);

This is special case when state depends on the current value. Most of the time you just pass a new value to state update function.

Question

Now that our App component includes the counter functionality, open the main.tsx file and render the App component three times. When you run the app, you will see three buttons displayed in the browser. What happens to the count values when you click any of the buttons, and why does this behavior occur?

main.tsx
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<App />
<App />
</StrictMode>,
)
Answer

Each button increase its own count value. In this case we create three Counter component instaces. State is local to a component instance on the screen. If you render the same component multpile times each copy will have completely isolated state.

State batching

State updates are batched to minimize re-renders for better performance. Before React 18 only state updates in the event handlers are batched. In React 18+ all updates are batched.

// State batching
function App() {
const [count, setCount] = React.useState(0);
const [msg, setMsg] = React.useState("");

function handleClick() {
setCount((prevCount) => prevCount + 1); // Does not re-render yet
setMsg("Counter: " + count); // Does not re-render yet
// Now, React re-render
}

return (
<div>
<p>You clicked {count} times</p>
<p>{msg}</p>
<button onClick={handleClick}>+</button>
</div>
);
}

If you don't want to batch state updates, you can use flushSync() from react-dom package.

import { flushSync } from 'react-dom';
...

function handleClick() {
flushSync(() => {
setCounter(prevCount => c + 1);
});
// DOM is updated
flushSync(() => {
setMsg('Counter: ' + count);
});
// DOM is updated
}
...
warning

flushSync can have negative effect to performance. Use only when necessary.

Conditional rendering

Conditional rendering allows you to show or hide elements and components based on certain conditions.

For example:

if (iconurl) {
return <img alt="icon" src={iconurl} />;
} else {
return <div>No image found</div>;
}

The example above renders the image element if the iconurl state is defined, otherwise it renders No image found text.

You can also use ternary operator (condition ? exprIfTrue : exprIfFalse) and now, we can have only one return statement:

return (
iconurl ?
<img alt="icon" src={iconurl} /> :
<div>No image found</div>
);

Complex states

objects

If you need to have multiple state variables, you can just call useState hook function multiple times.

const [firstName, setFirstName] = useState("John");
const [lastName, setLastName] = useState("Johnson");

Or you can use an object state. That is recommended if the values are related to each others. For example, you have to store information about user (first name, last name, address, email, etc.).

const [user, setUser] = useState({
firstName: "John",
lastName: "Johnson",
});

TypeScript can infer the object state type and it looks the following;

{
firstName: string;
lastName: string;
}

If you use an object state, you can access state values by using the following syntax:

state_name.property;

// for example
user.firstName;

When you try to access property that doesn't exist in the type, you will get TypeScript error:

Object state typing

It is recommended to define explicit types for complex states. Explicit types make it easier to understand the structure and expected data types of your state objects. This improves code readability and maintainability.

type User = {
firstName: string;
lastName: string;
}

And then you can use type when declaring a state, In the following example user state can have value of either null or User type object:

const [user, setUser] = useState<User | null>(null);

Or you can intialize state to empty object using the following syntax:

const [user, setUser] = useState<User>({} as User);

You can also have optional properties in your object state when defining the type:

type User = {
firstName: string;
lastName: string;
email?: string;
}

In our example, you can update values by using the setUser function and passing new object as parameter.

setUser({ firstName: "Jim", lastName: "Doe" });

When updating a single property within an object stored in React state, you must use the object spread syntax. React does not automatically merge updates to object state. The spread syntax (...) allows you to copy the existing properties of the object and then update only the property you want to change.

setUser({ ...name, lastName: "Doe" });
// New value is now firstName: John, lastName: Doe

The following code demonstrates how to manage object state in a React component using TypeScript. It defines a User type with firstName and lastName properties. The useState hook initializes the user state with a default object. The component displays a greeting based on the firstName and includes a button that updates the firstName property of the user state to 'John' using the object spread syntax.

import { useState } from 'react';
import './App.css';

type User = {
firstName: string;
lastName: string;
}

export default function App() {
const [user, setUser] = useState<User>({
firstName: "",
lastName: "Doe",
});

return (
<>
<p>
{ user?.firstName ? `Hello ${user.firstName}` : "Hello Guest" }
</p>
<button onClick={() => setUser({...user, firstName: "John"})}>
Update name
</button>
</>
)
}

You can test the example in: https://codesandbox.io/p/sandbox/object-state-example-dtqdks

TypeScript types can represent nested objects, such as a user's address. You can define the nested object directly within the User type:

type User = {
firstName: string;
lastName: string;
address: {
streetAddress: string;
houseNumber: number;
postalCode: number;
}
}

In this case, the better way is to create separate type for the address. It improves readability and allows you to reuse the Address type elsewhere.

type Address = {
streetAddress: string;
houseNumber: number;
postalCode: number;
}

type User = {
firstName: string;
lastName: string;
address: Address;
};

arrays

When initializing state with an array, TypeScript can often infer the array's type. F

// TypeScript infers number[]
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);

In this case, TypeScript infers that numbers is of type number[]. However, with more complex types it's often good to explicitly define the type using the useState hook's generic type parameter:

// Explicitly typed as number[]
const [numbers, setNumbers] = useState<number[]>([1, 2, 3, 4, 5]);

Array of user objects

type User = {
firstName: string;
lastName: string;
email?: string;
}

type UserList = {
users: User[];
}

const [users, setUsers] = useState<UserList | undefined>();

The intial value of users state is undefined because we don't set any intial value when calling useState hook function.

unions

In some cases, your state should be restricted to a specific set of values. You can use a union type to ensure that the state only accepts these predefined values:

type Status = "idle" | "pending" | "success" | "error";

const [status, setStatus] = useState<Status>("idle");

When to use state?

There are some differencies between states and variables that is good to understand. You should avoid unnecessary states. The more states a component has, the more complex its code becomes. It might also affect to performance.

Here are some key differences:

State

  • The value of state persists between re-renders
  • Component is re-rendered if state value changes.

Variable

  • The value of variable doesn't persist between re-renders.
  • Component is not re-rendered if variable value changes.

Variables can be used to data that doesn't impact component's output directly.

ref

If you need variable that persists between re-renders but it is not needed for rendering, you can use React ref. You can create a ref by calling the useRef hook function and pass initial value as an argument.

For example,

// Call useRef hook in the top level of your component
const myRef = useRef<number>(0)

The useRef hook function returns an object with a single property: current. That property can be used to access and set a value.

myRef.current = 10;

See the example in React documentation: https://react.dev/reference/react/useRef#examples-value

Extra material

The useReducer hook function is alternative to useState It is recommended to use, when the next state depends on the previous state (like in the case of counter example) or you have complex state.

Read more about useReducer in https://react.dev/reference/react/useReducer.


Further reading