The useState
hook is one of the most fundamental and widely used hooks in React, enabling developers to manage state in functional components. Introduced in React 16.8, it eliminates the need for class components when working with local component state, making functional components more powerful and versatile.
At its core, useState
provides a way to declare a state variable and update its value. With this hook, you can dynamically track and respond to user interactions, API responses, or any data changes within your application. It allows React to efficiently re-render components whenever the state changes, ensuring your UI stays in sync with your application logic.
Adding state to a component.
Updating state based on the previous state.
Understanding Complex Data Types with
useState
.Optimizing React State Initialization with Lazy Initialization.
Resetting state with a key.
Syntax
The useState
hook is used to declare a state variable in a functional component. Here's the basic syntax:
const [state, setState] = useState(initialValue);
state
: This is the current state value.setState
: This is the function used to update the state.initialValue
: This is the initial value of the state when the component is first rendered.
You can use setState
to update the state value, which will trigger a re-render of the component with the new state.
Let's explore all the use cases for the useState
hook one by one.
Adding state to a component:
import React, { useState } from "react"; function CounterWithHooks() { const initialState = 0; const [count, setCount] = useState(initialState); const handleClick = () => { setCount(count + 1); }; return ( <div> <button onClick={handleClick}>Clicked {count} times</button> </div> ); } export default CounterWithHooks;
Compare this with the component below that does not use hooks.
import React, { Component } from "react"; export class CounterWithoutHooks extends Component { constructor(props) { super(props); this.state = { count: 0, }; } handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <div> <button onClick={this.handleClick}> Clicked {this.state.count} times </button> </div> ); } } export default CounterWithoutHooks;
We will see how to use the useState hook for different data types.
// 1. Boolean const [isSelected, setSelected] = useState(true); // we can set 'true' or 'false' here. // 2. String const [name, setName] = useState("ReactJS"); // It will accept the string values // 3. Object const [name, setName] = useState({ firstname: "React", lastname: "JS" }); // this will accept the object as datatype.
As we can see, hooks simplify the code and make it easier to understand. However, the code above has an issue when updating the state, so let's address that.
const handleClick = () => { setCount(count + 1); console.log(count); // count= 0+1 console.log(count); // count= 0+1 console.log(count); // count= 0+1 };
Updating state based on the previous state:
When updating state based on the previous state using the
useState
hook, it's important to use a function inside thesetState
call. This function receives the current state as an argument, allowing you to compute the new state based on the previous one. This approach is crucial when the new state depends on the current state, ensuring that you always have the most up-to-date state value.const handleClick = () => { setCount((prev) => prev + 1); // count= 0+1 setCount((prev) => prev + 1); // count= 1+1 setCount((prev) => prev + 1); // count= 2+1 };
Understanding Complex Data Types with
useState
:Objects with UseState:
When using
useState
with objects, you can manage complex data structures in your components. Here's how you can handle objects withuseState
:import React, { useState } from "react"; function Form() { const [userDetails, setUserDetails] = useState({ firstname: "", lastname: "", email: "", }); const handleSubmit = (e) => { e.preventDefault(); window.alert( `Firstname : ${userDetails.firstname} Lastname: ${userDetails.lastname} email: ${userDetails.email}` ); }; return ( <div> <form> <input type="text" value={userDetails.firstname} onChange={(e) => { setUserDetails({ ...userDetails, firstname: e.target.value }); }} /> <input type="text" value={userDetails.lastname} onChange={(e) => { setUserDetails({ ...userDetails, lastname: e.target.value }); }} /> <input type="email" value={userDetails.email} onChange={(e) => { setUserDetails({ ...userDetails, email: e.target.value }); }} /> <button onClick={handleSubmit} type="submit"> Submit </button> </form> </div> ); } export default Form;
Using useState with Nested Objects:
import React, { useState } from "react"; function Form() { const [userDetails, setUserDetails] = useState({ name: { firstname: "", lastname: "", }, email: "", }); const handleSubmit = (e) => { e.preventDefault(); window.alert( `Firstname : ${userDetails.name.firstname} Lastname: ${userDetails.name.lastname} email: ${userDetails.email}` ); }; return ( <div> <form> <input type="text" value={userDetails.name.firstname} onChange={(e) => { setUserDetails({ ...userDetails, name: { ...userDetails.name, firstname: e.target.value }, }); }} /> <input type="text" value={userDetails.name.lastname} onChange={(e) => { setUserDetails({ ...userDetails, name: { ...userDetails.name, lastname: e.target.value }, }); }} /> <input type="email" value={userDetails.email} onChange={(e) => { setUserDetails({ ...userDetails, email: e.target.value }); }} /> <button onClick={handleSubmit} type="submit"> Submit </button> </form> </div> ); } export default Form;
Using
useState
with Arrays:import React, { useState } from "react"; import AddTodo from "./AddTodo"; import TodoList from "./TodoList"; function TodoApp() { const initialTodos = [ { id: 0, title: "Buy milk", done: true }, { id: 1, title: "Eat tacos", done: false }, { id: 2, title: "Brew tea", done: false }, ]; const [todos, setTodos] = useState(initialTodos); // Refer the below methods to update the arrays. const handleAddTodos = (title) => { setTodos([...todos, { id: Date.now(), title: title, done: false }]); }; const handleDeleteTodos = (id) => { setTodos(todos.filter((todo) => todo.id !== id)); }; const handleEditTodos = (id, newTitle) => { setTodos( todos.map((todo) => todo.id === id ? { ...todo, title: newTitle } : todo ) ); }; const updateTodos = (id) => { setTodos( todos.map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo ) ); }; return ( <div> <AddTodo handleAddTodos={handleAddTodos} /> <TodoList todos={todos} updateTodos={updateTodos} handleDeleteTodos={handleDeleteTodos} handleEditTodos={handleEditTodos} /> </div> ); } export default TodoApp;
Using
Immer
to manage complex data types inuseState
:
Immer is a JavaScript library that simplifies working with immutable data structures by allowing you to work with them as if they were mutable. It uses a concept called "drafts," which lets you make changes to an object directly, but behind the scenes, Immer ensures the original object remains unmodified and returns a new updated object.In React, Immer is often used to simplify state updates, especially when the state is complex or deeply nested.
const initialTodos = [ { id: 0, title: "Buy milk", done: true }, { id: 1, title: "Eat tacos", done: false }, { id: 2, title: "Brew tea", done: false }, ]; const [todos, setTodos] = useImmer(initialTodos);
// Compare this example with the previous TodoApp. const handleAddTodos = (title) => { setTodos((draft) => { draft.push({ id: Date.now(), title: title, done: false }); }); }; const handleDeleteTodos = (id) => { setTodos((draft) => { const index = draft.findIndex((todo) => todo.id === id); if (index !== -1) draft.splice(index, 1); }); }; const handleEditTodos = (id, newTitle) => { setTodos((draft) => { const todo = draft.find((todo) => todo.id === id); if (todo) todo.title = newTitle; }); }; const updateTodos = (id) => { setTodos((draft) => { const todo = draft.find((todo) => todo.id === id); if (todo) todo.done = !todo.done; }); };
Draft State: Immer provides a "draft" version of your state, which you can modify directly. This draft is a proxy of your original state.
Immutable Updates: Even though you modify the draft, Immer produces a new immutable state by applying those changes.
Ease of Use: Immer reduces boilerplate code when updating deeply nested structures in state.
Optimizing React State Initialization with Lazy Initialization:
Managing state is a core part of building React applications. Often, initializing state involves computations or generating data. A common pitfall for developers is inadvertently calling expensive functions unnecessarily during renders. Enter lazy initialization with
useState
, a technique to ensure efficiency and performance.What is Lazy Initialization?
When you initialize state using
useState
, you can provide:A value: Directly sets the initial state.
A function reference: React will only execute this function once, during the component's initial render, to generate the initial state.
Why Does This Matter?
If the initialization logic is computationally heavy (e.g., creating a large dataset or performing calculations), you want to run it only when needed—at the time of initial rendering. Lazy initialization prevents repeated execution of such logic during every render.
Let's understand this with an example
// Using Lazy Initialization
import { useState } from 'react';
function createInitialList() {
console.log('Generating list...');
const list = [];
for (let i = 0; i < 100; i++) {
list.push(`Item ${i + 1}`);
}
return list;
}
export default function LazyExample() {
const [items, setItems] = useState(createInitialList); // Function reference
return (
<div>
<h1>Lazy Initialization</h1>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
//Output:
Generating list...
// Using Immediate Initialization
import { useState } from 'react';
function createInitialList() {
console.log('Generating list...');
const list = [];
for (let i = 0; i < 100; i++) {
list.push(`Item ${i + 1}`);
}
return list;
}
export default function ImmediateExample() {
const [items, setItems] = useState(createInitialList()); // Immediate execution
return (
<div>
<h1>Immediate Initialization</h1>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
// Output
Generating list...
Generating list...
Generating list...
Resetting state with a key:
The
key
attribute in React is primarily used when rendering lists, ensuring React can efficiently update the DOM. However, it has a secondary purpose: it can force React to recreate a component from scratch when thekey
changes.import { useState } from 'react'; export default function ResettableForm() { const [version, setVersion] = useState(1); function handleReset() { setVersion((v) => v + 1); // Increment the version to reset the form } return ( <div> <h1>Form Reset Example</h1> <Form key={version} /> <button onClick={handleReset}>Reset Form</button> // This will reset the form to default values </div> ); } function Form() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); return ( <form> <label> Name: <input value={name} onChange={(e) => setName(e.target.value)} type="text" /> </label> <br /> <label> Email: <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" /> </label> </form> ); }
Thank you for taking the time to read my blog post! I truly appreciate your support and interest in the useState
in ReactJS. Your engagement and feedback mean a lot to me, and I hope you found the content valuable and insightful. If you have any thoughts or questions, feel free to leave a comment or reach out. Stay tuned for more updates, and thank you once again for being a part of this journey!