Understanding React's useState: A Complete Walkthrough

Understanding React's useState: A Complete Walkthrough

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.

  1. 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
       };
    
  2. 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 the setState 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
       };
    
  3. Understanding Complex Data Types with useState:

    1. Objects with UseState:

      When using useState with objects, you can manage complex data structures in your components. Here's how you can handle objects with useState:

       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;
      
    2. 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;
      
    3. 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;
      
    4. Using Immer to manage complex data types in useState:
      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.

  4. 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:

    1. A value: Directly sets the initial state.

    2. 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...
  1. 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 the key 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!