Chapter - 5 Lets get Hooked

In this blog, we will learn a lot—literally a lot. Most of the React hooks are in one place! This is something I wanted from the day I started learning React, so now I've built one. Here, we will learn:

Basic Hooks

  1. useState – Manages state within a functional component.

  2. useEffect – Handles side effects like fetching data or subscriptions.

  3. useContext – Accesses values from a Context without prop drilling.

Additional Hooks

  1. useReducer – Manages state using a reducer function (like Redux).

  2. useCallback – Memoizes a function to prevent unnecessary renders.

  3. useMemo – Memoizes a value to avoid recalculating it on every render.

  4. useRef – Creates a reference to a DOM element or value that persists between renders.

  5. useLayoutEffect – Similar to useEffect but fires synchronously after DOM updates.

  6. useImperativeHandle – Customizes the instance value exposed when using ref.

Performance and State Synchronization Hooks

  1. useTransition – Defers state updates to make UI updates more responsive.

  2. useDeferredValue – Defers a value update until the component is idle.

  3. useSyncExternalStore – Subscribes to an external state management library.

  4. useId – Generates a stable unique ID for accessibility or keying purposes.

  5. useInsertionEffect – Runs synchronously before DOM mutations; used for CSS-in-JS libraries.

New Hooks from React 19

  1. useFormStatus – Provides real-time information about form submission state.

  2. useActionState – Simplifies managing asynchronous actions and state transitions.

  3. useOptimistic – Facilitates optimistic UI updates by assuming success before confirmation.

  4. use – Handles asynchronous operations and promises directly within the component.

React Router Hooks

  1. useNavigate – Navigates programmatically.

  2. useLocation – Accesses the current URL location.

  3. useParams – Retrieves URL parameters.

  4. useSearchParams – Accesses query parameters.

  5. useMatch – Checks if the current URL matches a pattern.

And it's not just theory. Here, we will build projects as well.

So lets begin!!!

Why We Need useState Instead of Normal Variables in React

When learning React, a common question arises: Why can't we just use regular JavaScript variables instead of useState? Let me explain the crucial differences and why React's state management is essential.

Consider this counter implementation using a normal variable:

function Counter() {
  let count = 0; // Regular JavaScript variable

  const increment = () => {
    count = count + 1;
    console.log(count); // Value changes in console
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

What's Wrong Here?

  1. No Re-rendering: When you click the button, count changes in memory, but React doesn't know it needs to update the UI.

  2. Resets on Re-render: If the parent component re-renders, count will reset to 0 because the entire function runs again.

How useState Solves These Problems

  1. Triggers Re-renders

    • When you call setCount, React knows the component needs to re-render

    • The UI automatically updates to show the new value

  2. Persists Between Renders

    • React preserves the state value even when the component function runs again

    • Unlike regular variables that reset each render

  3. Batch Updates (we will learn more below)

    • React optimizes multiple state updates for performance

    • Regular variables would force immediate updates (less efficient)

  4. Component Isolation

    • Each component instance maintains its own state

    • Regular variables would be shared if not carefully managed

What is useState?

useState is a React Hook that allows functional components to manage state. Before Hooks were introduced in React 16.8, state management was only possible in class components using this.state.

useState returns two things: a variable and a function to change that variable. It takes a parameter that sets its initial value.

Syntax:

import React, { useState } from 'react';

/**
 * A simple counter component demonstrating React's `useState` Hook.
 * - Tracks a numeric value (`count`).
 * - Updates the value and re-renders the component when the button is clicked.
 */
function Counter() {
  // useState(0) initializes the state variable 'count' with a default value of 0.
  // Returns an array where:
  // - First element (`count`) = current state value.
  // - Second element (`setCount`) = function to update the state.
  const [count, setCount] = useState(0);

  return (
    <div>
      {/* Displays the current value of `count` */}
      <p>Count: {count}</p>

      {/* 
        When clicked, invokes `setCount` to:
        1. Update `count` to the new value (`count + 1`).
        2. Trigger a re-render to reflect the updated state.
      */}
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

Key Points :

  1. useState(0)

    • Initializes the state with 0 as the default value.

    • Returns an array: [currentState, updaterFunction].

  2. count

    • Holds the current value of the state (initially 0).

    • Automatically reflects the latest value after each re-render.

  3. setCount

    • A function to update the state.

    • Two Key Behaviors:

      • Updates the value of count (e.g., count + 1).

      • Triggers a re-render of the component to display the new value.

  4. Re-rendering - When setCount is called, React:

    • Updates the state (count).

    • Re-runs the Counter function to reflect changes in the UI.

Understanding Batch Updates with useState in React

Batch updates refer to React's practice of grouping multiple state updates together into a single re-render for better performance.

Let's modify your counter to show how batching works:

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

  const handleMultipleUpdates = () => {
    setCount(count + 1); // Update 1
    setCount(count + 1); // Update 2
    setCount(count + 1); // Update 3
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleMultipleUpdates}>
        Increment Multiple Times
      </button>
    </div>
  );
}

What Happens When You Click?

  1. Expected Behavior (Without Batching):
    You might expect the count to increase by 3 (once for each setCount call)

  2. Actual Behavior (With Batching):
    The count only increases by 1 because:

    • All three updates use the same count value (0) from the current render

    • React batches them together and only processes the last one

Why React Batches Updates

  1. Performance Optimization:

    • Avoids unnecessary intermediate renders

    • Reduces DOM operations

  2. Consistent UI:

    • Prevents partial/glitchy updates

    • Ensures all state changes appear together

When Batching Occurs

  • Event handlers (like onClick)

  • Lifecycle methods

  • Most synchronous React code

How to Handle Multiple Updates Correctly

const handleMultipleUpdates = () => {
  setCount(prev => prev + 1); // Update 1
  setCount(prev => prev + 1); // Update 2
  setCount(prev => prev + 1); // Update 3
};

Now the count increases by 3 because each update receives the latest value.

When Batching Doesn't Happen

React doesn't batch updates in:

  • setTimeout

  • setInterval

  • Native event handlers

  • Promise callbacks

Example of non-batched behavior:

const handleAsyncUpdates = () => {
  setTimeout(() => {
    setCount(count + 1);
    setCount(count + 1);
  }, 1000);
};
// These will trigger two separate renders

Key Takeaways

  1. Batching is Automatic: React batches synchronous state updates by default

  2. Functional Updates Help: Use setCount(prev => prev + 1) when updates depend on previous state

  3. Performance Benefit: Batching reduces unnecessary renders

  4. Async Behavior: Be aware that async code doesn't get batched

What is useEffect Hook?

useEffect is React's way of handling side effects—actions that go beyond the component's basic rendering process. Unlike class components with separate lifecycle methods, functional components required a single method to manage mounting, updating, and unmounting actions.

Think of useEffect as a way to keep your component in sync with something outside of it, like the DOM, a network request, a subscription, or any other API. It runs after the browser updates the screen, so your side effects won't slow down the visual update.

Types of useEffect

How React Compares Dependencies

React uses Object.is comparison for each dependency in the array. This means:

  • Primitive values (numbers, strings, booleans) compare by value

  • Objects and arrays compare by reference

  • NaN is considered equal to NaN (unlike ===)

useEffect(() => {
  // This effect will re-run only if any of these change
}, [primitive, object, array]);

The Exhaustive Dependency Rule

The React team enforces this rule through the react-hooks/exhaustive-deps ESLint rule. It's not just a suggestion—it prevents subtle bugs.

Why it matters:

  • Effects capture values from the render they were created in (closure)

  • Without proper dependencies, effects can operate on stale values

  • Missing dependencies can lead to unpredictable behavior

Common Dependency Patterns

  1. Empty array: [] - Runs once on mount

  2. All dependencies: [prop1, state1, contextValue] - Runs when any change

  3. Function dependencies: [onClick] - Be careful with inline functions

  4. Object dependencies: [user] - Will re-run if user reference changes

Understanding closures is crucial for mastering useEffect.

import React, { useState, useEffect } from 'react';

function Counter() {
  // Initialize state with 0
  const [count, setCount] = useState(0);

  useEffect(() => {
    // Set up an interval that runs every 1000ms (1 second)
    const interval = setInterval(() => {
      console.log(count); // Always logs the initial value (0) due to closure
      setCount(count + 1); // Always sets the count to 1 because count is always 0 in this scope
    }, 1000);

    // Cleanup function to clear the interval when the component unmounts
    return () => clearInterval(interval);
  }, []); // Empty dependency array means this runs only once when the component mounts

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
}

export default Counter;

Issue in the Code:

  • Since useEffect has an empty dependency array ([]), the function inside the setInterval captures the initial value of count (which is 0) and never updates it.

  • As a result, it always logs 0 and setCount(count + 1) always sets the value to 1.

Why this happens:

  • The effect closure captures the count value from the initial render

  • Subsequent interval callbacks see that same initial value

  • The dependency array tells React the effect doesn't depend on count

Solutions

  1. Add dependencies (simplest solution):

     useEffect(() => {
       const interval = setInterval(() => {
         setCount(count + 1);
       }, 1000);
       return () => clearInterval(interval);
     }, [count]); // Now updates correctly
    
  2. Use functional updates (better for state):

     useEffect(() => {
       const interval = setInterval(() => {
         setCount(prev => prev + 1); // Always gets latest state
       }, 1000);
       return () => clearInterval(interval);
     }, []); // No longer needs count in deps
    

Effect Cleanup :

The cleanup function is more powerful than many developers realize.

When Cleanup Runs

  1. Before re-running the effect (when dependencies change)

  2. When the component unmounts

  3. In development, twice when Strict Mode is enabled

Race condition prevention:

useEffect(() => {
  // Flag to track if the component is unmounted
  let didCancel = false;

  // Async function to fetch data
  async function fetchData() {
    try {
      const result = await fetch('/data'); // Fetch data from the API

      if (!didCancel) {
        setData(result); // Update state only if the component is still mounted
      }
    } catch (error) {
      if (!didCancel) {
        console.error("Error fetching data:", error);
      }
    }
  }

  fetchData(); // Call the fetch function

  // Cleanup function to prevent state updates if the component unmounts
  return () => {
    didCancel = true;
  };
}, [query]); // Runs whenever `query` changes

Why is didCancel used?

  • If the component unmounts before the API request completes, updating state (setData(result)) could cause an error.

  • The cleanup function sets didCancel = true, ensuring that setData(result) runs only if the component is still mounted.

Potential Issue

This method works in some cases, but since didCancel is a local variable (not state), React doesn’t track its changes. A better approach is aborting the fetch request using the AbortController API:

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

  async function fetchData() {
    try {
      const response = await fetch('/data', { signal }); // Attach abort signal
      if (!signal.aborted) {
        const result = await response.json();
        setData(result);
      }
    } catch (error) {
      if (!signal.aborted) {
        console.error("Error fetching data:", error);
      }
    }
  }

  fetchData();

  return () => {
    controller.abort(); // Cancels the request if the component unmounts
  };
}, [query]); // Runs when `query` changes

Why use AbortController?

  • AbortController is the recommended way to cancel fetch requests.

  • It prevents unnecessary network calls and ensures React doesn't update state on an unmounted component.

Effect Frequency Control

  1. Debouncing:

     useEffect(() => {
       const timer = setTimeout(() => {
         // Your effect
       }, 300);
    
       return () => clearTimeout(timer);
     }, [input]);
    
  2. Throttling:

     useEffect(() => {
       let lastCalled = 0;
       const handler = () => {
         const now = Date.now();
         if (now - lastCalled >= 1000) {
           // Your effect
           lastCalled = now;
         }
       };
    
       window.addEventListener('scroll', handler);
       return () => window.removeEventListener('scroll', handler);
     }, []);
    

Effect Patterns for Common Use Cases

Data Fetching Patterns

useEffect(() => {

  // Set initial status before starting the async operation
  setStatus('pending');

  // Initiate data fetching
  fetchData()
    .then(data => { 
        // Update data state with successful response
        setData(data);
        // Update status to indicate successful completion
        setStatus('success');

    })
    .catch(error => {
        setError(error);
        // Update status to indicate failure
        setStatus('error');

    });


// Dependency array - effect re-runs only when query changes
}, [query]);

Using async directly in useEffect leads to unexpected behavior because useEffect should return a cleanup function, but async functions always return a promise.

Incorrect Approach:

useEffect(async () => { //  Not recommended
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  setData(data);
}, []);

Fix: Use an async function inside useEffect instead:

useEffect(() => {
  async function fetchData() {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    setData(data);
  }

  fetchData();
}, []);

What is useLayoutEffect?

React provides two primary hooks for handling side effects: useEffect and useLayoutEffect. While useEffect is the more commonly used hook, useLayoutEffect serves a critical role in specific scenarios where timing and DOM interactions are crucial.

useLayoutEffect is a React Hook that runs synchronously immediately after React has performed all DOM mutations but before the browser has painted the screen. This makes it ideal for operations that need to read or modify the DOM before the user sees anything.

Key Differences Between useEffect and useLayoutEffect

FeatureuseEffectuseLayoutEffect
Execution TimingRuns asynchronously after the browser paintsRuns synchronously before the browser paints
Use CaseBest for side effects that don’t block rendering (e.g., API calls, subscriptions)Best for DOM measurements and mutations that must happen before paint
Blocking BehaviorNon-blocking (doesn’t delay paint)Blocking (can delay paint if logic is slow)

Syntax

import { useLayoutEffect } from "react";

useLayoutEffect(() => {
  // This effect runs synchronously after all DOM mutations and before the browser paints.
  // It is useful for layout-related calculations and DOM measurements.

  // Perform synchronous side effects here, such as measuring DOM elements
  // or synchronizing animations before the browser repaints.

  return () => {
    // Cleanup function: Runs before the next effect execution or when the component unmounts.
    // Use this to reset DOM modifications, remove event listeners, or clear timers.
  };
}, [dependencies]); // Runs when 'dependencies' change, or once if empty []

When Should You Use useLayoutEffect?

You should only use useLayoutEffect when you need to:

  • Measure DOM elements (e.g., get an element’s width/height before rendering).

  • Modify the DOM synchronously (e.g., prevent a flicker by adjusting styles before the user sees it).

  • Perform imperative animations (e.g., manually control transitions).

When NOT to Use useLayoutEffect

  • Data fetching (use useEffect instead).

  • Non-urgent side effects (since it blocks painting).

  • Server-side rendering (SSR) (since there’s no DOM, it behaves like useEffect but may cause warnings).

Common Use Cases for useLayoutEffect

1. Measuring DOM Elements Before Render

If you need to read layout properties (e.g., offsetHeight, getBoundingClientRect), useLayoutEffect ensures measurements happen before the browser paints.

Here’s your Tooltip component with proper comments and an explanation of why useLayoutEffect is used instead of useEffect:

import { useLayoutEffect, useRef, useState } from "react";

function Tooltip({ children }) {
  const ref = useRef(null); // Create a reference to access the DOM element
  const [width, setWidth] = useState(0); // State to store the width of the element

  useLayoutEffect(() => {
    if (ref.current) {
      // Get the width of the element before the browser repaints
      const { width } = ref.current.getBoundingClientRect();
      setWidth(width); // Update the state with the measured width
    }
  }, []); // Runs only once when the component mounts

  return <div ref={ref}>{children} (Width: {width}px)</div>;
}

export default Tooltip;

Why Did We Use useLayoutEffect Instead of useEffect?

Ensures Accurate Measurements Before Paint

  • useLayoutEffect runs synchronously after the DOM updates but before the browser paints.

  • This guarantees that the width is updated before the user sees any visual changes, avoiding flickering.

Avoids UI Jank (Flickering)

  • If we used useEffect, the browser would first paint the component before updating width, causing a brief delay where the width might be incorrect.

  • With useLayoutEffect, the correct width is set before the paint, ensuring a smoother user experience.

2. Preventing Layout Flickering

If a component’s appearance changes due to state updates, useLayoutEffect can apply changes before the browser paints, avoiding visual glitches.

import { useLayoutEffect, useRef, useState } from "react";

function FlashyButton() {
  const [isHovered, setIsHovered] = useState(false); // State to track hover status
  const buttonRef = useRef(null); // Reference to the button element

  useLayoutEffect(() => {
    if (buttonRef.current) {
      // Directly manipulate the button's background color based on hover state
      buttonRef.current.style.backgroundColor = isHovered ? "blue" : "red";
    }
  }, [isHovered]); // Runs whenever `isHovered` changes

  return (
    <button
      ref={buttonRef} // Attach the ref to the button element
      onMouseEnter={() => setIsHovered(true)} // Set hover state to true on mouse enter
      onMouseLeave={() => setIsHovered(false)} // Set hover state to false on mouse leave
    >
      Hover Me
    </button>
  );
}

export default FlashyButton;

Performance Considerations

Since useLayoutEffect runs synchronously, misusing it can harm performance:

  • Blocking the main thread (delays rendering if logic is slow).

  • Overusing it (can lead to janky UI if too many synchronous operations run).

How to Optimize useLayoutEffect

  1. Debounce heavy operations (e.g., batch DOM updates).

  2. Avoid unnecessary dependencies (only re-run when truly needed).

  3. Combine multiple DOM reads/writes to prevent layout thrashing.

Here is a simple project to help you understand the hooks mentioned above properly:

https://4yjs89.csb.app/

import React, { useState, useEffect, useLayoutEffect } from "react";

// Child Component: Displays individual user details
function UserCard({ user }) {
  const [isHovered, setIsHovered] = useState(false);
  const nameRef = React.useRef(null);

  // Using useLayoutEffect to change the text color on hover
  useLayoutEffect(() => {
    if (nameRef.current) {
      nameRef.current.style.color = isHovered ? "blue" : "black";
    }
  }, [isHovered]);

  return (
    <div 
      style={{
        border: "1px solid #ddd",
        padding: "10px",
        margin: "10px",
        borderRadius: "5px",
        background: "#f9f9f9"
      }}
    >
      <h3 
        ref={nameRef} 
        onMouseEnter={() => setIsHovered(true)} 
        onMouseLeave={() => setIsHovered(false)}
      >
        {user.name}
      </h3>
      <p>Email: {user.email}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
}

// Parent Component: Fetches data and renders UserCard components
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  // Fetch user data from API using useEffect
  useEffect(() => {
    async function fetchUsers() {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users");
        const data = await response.json();
        setUsers(data);
      } catch (error) {
        console.error("Error fetching users:", error);
      } finally {
        setLoading(false);
      }
    }
    fetchUsers();
  }, []);

  return (
    <div style={{ padding: "20px", fontFamily: "Arial, sans-serif" }}>
      <h2>User List</h2>
      {loading ? <p>Loading...</p> : users.map((user) => <UserCard key={user.id} user={user} />)}
    </div>
  );
}

export default UserList;

what is useRef

useRef is a hook that returns a mutable object whose current property persists across re-renders.

What Are React Refs?

Refs (short for "references") are a special feature in React that allows direct interaction with DOM elements and store values that persist across renders without causing re-renders.

They provide an imperative way to access and modify elements, making them useful in scenarios where controlled components or state management might be inefficient.

Key Characteristics of Refs

  1. Direct Access to DOM Elements: Refs allow you to directly interact with elements in the DOM, bypassing React’s usual state and props system.

     import { useRef, useEffect } from "react";
    
     function FocusInput() {
         const inputRef = useRef(null);
    
         useEffect(() => {
             inputRef.current.focus(); // Directly accessing the DOM element
         }, []);
    
         return <input ref={inputRef} type="text" />;
     }
    

    Here, useRef creates a reference to the input field, and focus() is called directly on the element when the component mounts.

  2. Persistent Mutable Storage Across Renders: Refs act as a persistent storage space that retains its value between re-renders.

     function Counter() {
         const countRef = useRef(0);
    
         const handleClick = () => {
             countRef.current += 1;
             console.log(countRef.current); // Value updates but does not cause re-render
         };
    
         return <button onClick={handleClick}>Increment</button>;
     }
    

    Here, countRef keeps track of the count but does not cause a component re-render when updated.

  3. Refs Are Mutable Objects with a current Property:

    • A ref is an object with a single property, current, which holds the referenced value.

    • This property can be updated without triggering a component re-render.

  4. Value Persists Between Re-renders:

    • Unlike local component state (useState), updating a ref’s current property does not trigger a component re-render.

    • This makes refs useful for storing values like previous state, timers, and instance variables.

  5. Changes Don't Trigger Re-renders:

    • If you update a state variable, React schedules a re-render.

    • Updating a ref (ref.current = newValue) does not cause a re-render.

  6. Same Reference Identity on Every Render:

    • The ref object created by useRef() remains the same across all renders.

    • This makes it ideal for persisting values without causing unnecessary re-renders.

Use Cases of Refs

  1. Accessing DOM Elements (like focus, animations, measurements)
    Example: Automatically focusing an input field.

     function AutoFocusInput() {
         const inputRef = useRef(null);
    
         useEffect(() => {
             inputRef.current.focus();
         }, []);
    
         return <input ref={inputRef} />;
     }
    
  2. Storing Previous State Without Causing Re-renders
    Example: Keeping track of the previous count.

     function PreviousStateExample({ count }) {
         const prevCountRef = useRef(count);
    
         useEffect(() => {
             prevCountRef.current = count;
         }, [count]);
    
         return <p>Previous count: {prevCountRef.current}</p>;
     }
    
  3. Handling Timers, Intervals, or Subscriptions
    Example: Clearing a timer without re-renders.

     import { useRef } from "react";
    
     function Timer() {
         // useRef is used to persist the timer reference across renders
         const timerRef = useRef(null);
    
         // Function to start the timer
         const startTimer = () => {
             // Sets a timeout of 5 seconds and stores the reference in timerRef
             timerRef.current = setTimeout(() => {
                 alert("Time’s up!"); // Displays an alert after 5 seconds
             }, 5000);
         };
    
         // Function to stop the timer before it completes
         const stopTimer = () => {
             // Clears the timeout to prevent the alert from being triggered
             clearTimeout(timerRef.current);
         };
    
         return (
             <div>
                 {/* Button to start the timer */}
                 <button onClick={startTimer}>Start Timer</button>
    
                 {/* Button to stop the timer before it completes */}
                 <button onClick={stopTimer}>Stop Timer</button>
             </div>
         );
     }
    
     export default Timer;
    
  4. Managing Uncontrolled Components (Forms, File Inputs)
    Example: Handling file input without state.

     import { useRef } from "react";
    
     function FileUploader() {
         // useRef is used to reference the file input element without causing re-renders
         const fileInputRef = useRef(null);
    
         // Function to handle file upload
         const handleUpload = () => {
             // Logs the selected file (first file in the input)
             console.log(fileInputRef.current.files[0]);
         };
    
         return (
             <div>
                 {/* File input element, controlled using useRef */}
                 <input type="file" ref={fileInputRef} />
    
                 {/* Button to trigger the file upload handler */}
                 <button onClick={handleUpload}>Upload</button>
             </div>
         );
     }
    
     export default FileUploader;
    

useRef vs useState

Understanding when to use each is crucial for optimal React code.

FeatureuseRefuseState
Triggers Re-renderNoYes
Mutable ValueYes (ref.current)Immutable (use setter)
Use CaseDOM access, mutable valuesState that affects UI
PersistenceAcross re-rendersBut triggers updates
InitializationuseRef(initVal)useState(initVal)

When to Use Which

  • Use useRef when:

    • You need to access DOM elements

    • You need to store mutable values that shouldn't trigger re-renders

    • Tracking previous values or instance variables

  • Use useState when:

    • The value affects the UI and should trigger updates

    • You need derived state or computed values

    • Managing form inputs or controlled components

ref vs forwardRef

Regular ref - Works directly on DOM elements or class components:

forwardRef - Allows passing refs through functional components to their children:

import { forwardRef, useRef } from "react";

// Creating a button component that supports ref forwarding
const FancyButton = forwardRef((props, ref) => (
  // The ref is passed to the button element, allowing parent components to access it
  <button ref={ref} className="fancy">
    {props.children} {/* Renders child content inside the button */}
  </button>
));

function App() {
  // useRef is used to create a reference to the FancyButton component
  const buttonRef = useRef();

  return (
    // Passing the ref to FancyButton, allowing direct access to the button element
    <FancyButton ref={buttonRef}>Click</FancyButton>
  );
}

export default App;

When to Use forwardRef

  1. Reusable Component Libraries

  2. Higher-Order Components (HOCs)

  3. When parent needs direct access to child's DOM node

Common Pitfalls

//  Won't work - functional components don't have instances
const BrokenComponent = ({ ref }) => <div ref={ref} />;

// Correct
const FixedComponent = forwardRef((props, ref) => <div ref={ref} />;

React 19 Ref Enhancements

Automatic ref Cleaning

// React 18: Manual cleanup
useEffect(() => {
  const node = ref.current;
  return () => {
    // Cleanup ref-related logic
  };
}, []);

// React 19: Auto-cleanup
ref.current = null; // Automatically handled on unmount

Passing ref as a Prop to Function Components

// React 18 and earlier
const MyInput = forwardRef(({ placeholder }, ref) => {
  return <input placeholder={placeholder} ref={ref} />;
});

// Usage
const inputRef = useRef();
<MyInput ref={inputRef} placeholder="Search..." />;

// React 19 new feature
function MyInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

// Usage remains identical
const inputRef = useRef();
<MyInput ref={inputRef} placeholder="Search..." />;

Best Practices

  1. Avoid Overusing Refs - Most DOM manipulation should be declarative

  2. Null Check Refs - ref.current might be null during cleanup

What is useImperativeHandle?

useImperativeHandle lets you define what properties/methods are exposed when a parent component accesses a child's ref.

Basic Syntax

useImperativeHandle(ref, createHandle, dependencies?)
  • ref: The forwarded ref

  • createHandle: Function returning the object to expose

  • dependencies: Optional array for memoization

Example: Custom Focus Method

import { forwardRef, useRef, useImperativeHandle, useEffect } from "react";

// Creating a custom input component with forwarded ref
const FancyInput = forwardRef((props, ref) => {
  // Local ref to access the input element
  const inputRef = useRef();

  // Exposing custom methods (focus and shake) to the parent component
  useImperativeHandle(ref, () => ({
    // Focus method: Sets focus on the input element
    focus: () => inputRef.current.focus(),

    // Shake method: Temporarily moves the input sideways for a shake effect
    shake: () => {
      inputRef.current.style.transform = "translateX(10px)";
      setTimeout(() => {
        inputRef.current.style.transform = "";
      }, 500);
    }
  }));

  // Rendering the input element with forwarded ref
  return <input ref={inputRef} {...props} />;
});

// Parent component that uses FancyInput
function Form() {
  // Creating a ref to interact with FancyInput methods
  const inputRef = useRef();

  useEffect(() => {
    // Calling the custom shake method when the component mounts
    inputRef.current.shake();

    // Calling the standard focus method to focus the input field
    inputRef.current.focus();
  }, []);

  return <FancyInput ref={inputRef} />;
}

export default Form;

Why Does This Hook Exist?

The Problem It Solves

  1. Security: Prevents full DOM access by parent components

  2. Abstraction: Exposes only necessary methods

  3. Consistency: Maintains stable APIs for component libraries

Comparison to Direct Refs

ApproachParent AccessChild ControlUse Case
Direct DOM RefFull DOM accessNoneSimple DOM manipulation
useImperativeHandleOnly exposed methodsFull controlComponent libraries, controlled APIs

How It Works

  1. React creates an imperative handle object during render

  2. Attaches it to the Fiber node's ref property

  3. Parent's ref receives this object instead of raw DOM node

sequenceDiagram
    participant Parent
    participant Child
    participant ReactFiber

    Parent->>Child: Passes ref
    Child->>ReactFiber: useImperativeHandle
    ReactFiber->>Parent: Returns custom handle
    Parent->>CustomHandle: Calls exposed methods

Lifecycle Behavior

  • Mount: Handle created after layout effects

  • Update: Handle recreated if dependencies change

  • Unmount: Handle detached

Real-World Use Cases

1. Component Libraries

// Custom select component
const Select = forwardRef((props, ref) => {
  const selectRef = useRef();

  useImperativeHandle(ref, () => ({
    value: selectRef.current.value,
    open: () => selectRef.current.showPicker(),
    isValid: () => !!selectRef.current.value
  }));

  return <select ref={selectRef} {...props} />;
});

2. Animation Controllers

const AnimatedBox = forwardRef((props, ref) => {
  const boxRef = useRef();

  useImperativeHandle(ref, () => ({
    playAnimation: () => {
      boxRef.current.animate([...], {...});
    },
    reset: () => {
      boxRef.current.style.transform = '';
    }
  }));

  return <div ref={boxRef} className="box" />;
});

Benchmark Data

OperationPlain RefuseImperativeHandle
Mount Time1.2ms1.5ms (+25%)
Update Time0.3ms0.4ms (+33%)
Memory Overhead0~0.1KB per instance

React 19 Enhancements

1. Automatic Cleanup

useImperativeHandle(ref, () => ({
  // React 19 auto-disposes this
  subscribe: () => {
    const sub = dataSource.subscribe();
    return () => sub.unsubscribe();
  }
}));

2. Compiler Optimizations

  • Inlines simple imperative handles

  • Dead code elimination for unused methods

3. Improved DevTools Support

  • Visualizes exposed methods

  • Tracks handle changes

Common Errors

1. Forgetting forwardRef

//  Broken
function Component({ ref }) { ... }

//  Fixed
const Component = forwardRef((props, ref) => { ... });

2. Stale Closures

useImperativeHandle(ref, () => ({
  // ❌ Captures initial state
  getValue: () => state.value 

  // ✅ Use ref for latest value
  getValue: () => stateRef.current 
}), []);

3. Over-Exposing

//  Bad practice
useImperativeHandle(ref, () => inputRef.current);

//  Minimal API
useImperativeHandle(ref, () => ({
  focus: () => inputRef.current.focus()
}));

Prop Drilling: The Problem

Prop drilling occurs when you need to pass data through multiple layers of components to get it from a parent component to a deeply nested child component. This creates several issues:

  1. Unnecessary Intermediate Components: Components between the source and destination must accept and forward props they don't actually use.

  2. Reduced Maintainability: Changing the data structure requires modifying all components in the chain.

  3. Tight Coupling: Components become dependent on their position in the hierarchy.

  4. Verbose Code: More props need to be declared and passed through multiple levels.

Example of Prop Drilling

import { useState } from "react";

function App() {
  // State to store user information
  const [user, setUser] = useState({ name: "John", age: 30 });

  return (
    <div>
      {/* Pass user data to Header and MainContent components */}
      <Header user={user} />
      <MainContent user={user} />
    </div>
  );
}

// Header component to display navigation bar
function Header({ user }) {
  return (
    <header>
      {/* Pass user data to NavBar component */}
      <NavBar user={user} />
    </header>
  );
}

// NavBar component to display user's name in the navigation bar
function NavBar({ user }) {
  return <nav>Welcome, {user.name}</nav>;
}

// MainContent component to display sidebar and main content
function MainContent({ user }) {
  return (
    <main>
      {/* Pass user data to Sidebar and Content components */}
      <Sidebar user={user} />
      <Content user={user} />
    </main>
  );
}

// Sidebar component (example for future extension)
function Sidebar({ user }) {
  return <div>Sidebar for {user.name}</div>;
}

// Content component (example for future extension)
function Content({ user }) {
  return <div>Main content for {user.name}</div>;
}

export default App;

// ... and so on through many levels

In this example, user is being passed through multiple components that don't use it, just to get it to components that do need it.

Context API: The Solution

In React, the Context API solves the problem of prop drilling — where you need to pass data through multiple levels of components, even if only the deeply nested ones need it. Instead of manually passing props at every level, Context API allows you to share state and values directly across the component tree without intermediate components needing to know about it.

Why Context API?

Imagine you have a user object containing user details like name and age. If you want to display the user’s name in both a header and a sidebar, you’d have to pass the user prop from the top-level component down through each intermediate component — even if they don’t need it.

Context API eliminates this problem by creating a centralized state that any component can access directly, no matter how deeply nested it is.

Key Components of Context API

1. React.createContext

The createContext() function creates a context object that holds shared data. It returns an object with two components:

Provider → Supplies the data to the component tree.

Consumer → (Less common) Reads the data from the context directly.

Example:

const UserContext = React.createContext({ name: 'Guest', age: 0 });

In this example, UserContext is a context object with a default value of { name: 'Guest', age: 0 }. If no provider is set, this value will be used.

2. Context.Provider

The Provider makes the context value available to all child components within its tree.

The value prop defines what data is available to consumers or (children).

3. useContext Hook

The useContext() hook allows any component to access the context value directly — no need to pass props down!

  • It returns the current context value from the nearest matching Provider.

  • If there’s no matching provider, it will use the default value set in createContext().

How useContext Works Internally

Basic Mechanism

  1. When you call React.createContext(), React creates:

    • A Provider component (to pass values down the component tree)

    • A Consumer component (mostly used in class components)

  2. useContext subscribes to the nearest matching Provider above it in the component tree

  3. When the Provider's value changes, all components using that context with useContext will re-render

Important Notes

  • No middleware: Unlike Redux, context doesn't handle side effects (use useEffect instead)

  • No selectors: Any change to the context value triggers re-renders in all consumers

  • Default values: Only used when there's no matching Provider above the component

Example:

import React from "react";

// Create a context to store user data
const UserContext = React.createContext(null);

function App() {
  // User object containing user details
  const user = { name: "John", age: 30 };

  return (
    // Provide the user data to all components within the tree
    <UserContext.Provider value={user}>
      {/* Header and MainContent can access user data through context */}
      <div style={styles.appContainer}>
        <Header />
        <MainContent />
      </div>
    </UserContext.Provider>
  );
}

// Header component to display the navigation bar
function Header() {
  return (
    <header style={styles.header}>
      {/* No prop drilling */}
      <NavBar />
    </header>
  );
}

// NavBar component to show the user's name
function NavBar() {
  // Access user data from context
  const user = React.useContext(UserContext);

  return <nav style={styles.navBar}>Welcome, {user.name}!</nav>;
}

// MainContent component to display the main page content
function MainContent() {
  return (
    <main style={styles.mainContent}>
      {/* Sidebar and Content can also access user data through context */}
      {/* No prop drilling required */}
      <Sidebar />
      <Content />
    </main>
  );
}

// Sidebar component to display sidebar content
function Sidebar() {
  // Access user data from context
  const user = React.useContext(UserContext);

  return <div style={styles.sidebar}>Sidebar for {user.name}</div>;
}

// Content component to display main content
function Content() {
  // Access user data from context
  const user = React.useContext(UserContext);

  return <div style={styles.content}>Main content for {user.name}</div>;
}

// Styling objects
const styles = {
  appContainer: {
    fontFamily: "Arial, sans-serif",
    color: "#333",
    backgroundColor: "#f9f9f9",
    minHeight: "100vh",
    padding: "20px",
    boxSizing: "border-box",
  },
  header: {
    backgroundColor: "#6200ea",
    padding: "10px 20px",
    color: "#ffffff",
    borderRadius: "8px",
    marginBottom: "20px",
  },
  navBar: {
    fontSize: "18px",
    fontWeight: "bold",
  },
  mainContent: {
    display: "flex",
    gap: "20px",
  },
  sidebar: {
    backgroundColor: "#bb86fc",
    padding: "20px",
    borderRadius: "8px",
    color: "#ffffff",
    width: "200px",
    height: "150px",
  },
  content: {
    backgroundColor: "#03dac6",
    padding: "20px",
    borderRadius: "8px",
    color: "#ffffff",
    flex: 1, // Take up the remaining space
    height: "150px",
  },
};

export default App;

Here, the UserContext.Provider wraps the components where the data should be accessible. The value prop passes the user object down the component tree.

Benefits of Context API:

  1. No More Prop Drilling – Pass data directly where it’s needed without passing it through parent components.

  2. Cleaner Code – Less boilerplate and fewer props make code more readable.

  3. Centralized State – Context acts like a lightweight state management solution for local state.

  4. Dynamic Updates – When the context value updates, all subscribed components re-render automatically.

A lot happened above, so let's create a project here. I will provide you with a step-by-step guide to make it. It's a very common project: Theme Management.

Users should be able to switch between dark and light themes throughout the application.

In this project, we will use Vite since CRA is deprecated, Tailwind v4, React Hook Form for validation, and the Context API.

Understanding useReducer in React

useReducer is a React Hook that provides an alternative way to manage state in function components. It is particularly useful when dealing with complex state logic that involves multiple sub-values or when the next state depends on the previous state.

How useReducer Works

1. It Takes Two Arguments:

  • A reducer function, which contains the logic for updating the state.

  • An initial state, which represents the starting state value.

2. It Returns Two Values:

  • The current state of the component.

  • A dispatch function, which is used to send actions that trigger state updates.

Reducer Function

A reducer function is a JavaScript function that takes two parameters:

  1. Current state – The existing state value.

  2. Action – An object that describes the change to be made.

useReducer Syntax

const [state, dispatch] = useReducer(reducer, initialState);

Parameters:

  1. reducer – A function that takes (state, action) and returns the new state.

  2. initialState – The starting state of your component.

Returns:

  • state – The current state.

  • dispatch – A function to send actions to the reducer to update the state.

How useReducer Works

  1. The dispatch function sends an action object to the reducer function.

  2. The reducer function takes the current state and the action and calculates the new state.

  3. React updates the component with the new state.

The function processes the action and returns a new state.

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state; // Return current state if action is not recognized
  }
}

Using useReducer in a Component

import React, { useReducer } from "react";

// Reducer function
function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  // Initial state
  const initialState = { count: 0 };

  // Using useReducer
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
    </div>
  );
}

export default Counter;

How Does This Work?

  1. The useReducer hook initializes state with { count: 0 }.

  2. Clicking the + button triggers dispatch({ type: "INCREMENT" }), calling the reducer function.

  3. The reducer updates state by increasing count.

  4. Clicking the - button triggers dispatch({ type: "DECREMENT" }), decreasing count.

When to Use useReducer Instead of useState?

ScenariouseStateuseReducer
Simple state updates
State updates depend on previous state✅ (with functional updates)✅ (more structured)
Complex state logic (multiple conditions)
State logic should be separate from component

Common Mistakes and How to Avoid Them

  1. Mutating state directly:
state.push(newItem); //  Don’t mutate state
  1. Use spread to create a new object or array:
return [...state, newItem];
  1. Forgetting the default case in a reducer:
  • Always handle the default case to avoid runtime errors.

  • Throw an error or return the current state if no action matches.

Before we move on to the project section for this lets learn just two more thing - storage and routing.

Storage

1. Local Storage (Persistent Storage)

What is Local Storage?

Local Storage is a way to store key-value pairs in the browser that remain even after closing the browser. It saves data as strings and has a limit of about 5MB per domain.

Why Use Local Storage?

  • Persistent storage: Data stays until you clear it.

  • Simple API: Easy to use with methods like setItem, getItem, removeItem.

  • No server dependency: Works entirely on the client side.

When to Use Local Storage?

User preferences (like theme and language settings)
Caching API responses (to make loading faster)
Offline data storage (when used with IndexedDB)

When NOT to Use Local Storage?

Sensitive data (passwords, tokens) – vulnerable to XSS attacks.
Large datasets – limited to ~5MB.
Frequently changing data – synchronous API can slow down apps.

import { useState, useEffect } from 'react';

function LocalStorageExample() {
  // State to store the theme (default is 'light')
  const [theme, setTheme] = useState('light');

  // Load theme from localStorage when the component mounts
  useEffect(() => {
    const savedTheme = localStorage.getItem('theme'); // Retrieve saved theme
    if (savedTheme) setTheme(savedTheme); // If a theme exists, update state
  }, []); // Runs only once when the component mounts

  // Save the theme to localStorage whenever it changes
  useEffect(() => {
    localStorage.setItem('theme', theme); // Store theme in localStorage
  }, [theme]); // Runs whenever `theme` changes

  return (
    <div>
      {/* Display the current theme */}
      <h2>Current Theme: {theme}</h2>

      {/* Toggle between 'light' and 'dark' themes */}
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>

      {/* Clear theme from localStorage */}
      <button onClick={() => localStorage.removeItem('theme')}>
        Clear Theme
      </button>
    </div>
  );
}

export default LocalStorageExample;

Explanation:

  1. State Management (useState) - Stores the theme (light or dark).

  2. Loading Data on Mount (useEffect) - Retrieves the theme from localStorage when the component first loads.

  3. Saving Data on Theme Change (useEffect) - Updates localStorage whenever the theme state changes.

  4. Button Functionalities:

    • Toggle Button: Switches between light and dark themes.

    • Clear Button: Removes the theme from localStorage, resetting to default on next mount.

Let me know if you need further improvements! 🚀

2. Session Storage (Temporary Storage)

What is Session Storage?

Similar to Local Storage, but data is cleared when the browser tab is closed. Also limited to ~5MB.

Why Use Session Storage?

  • Tab-specific storage: Data doesn’t leak across tabs.

  • Automatic cleanup: No need to manually clear data.

  • Lightweight: Good for short-lived data.

When to Use Session Storage?

Form data retention (prevent loss on accidental refresh)
Multi-tab workflows (different data per tab)
Temporary authentication states

When NOT to Use Session Storage?

Data needed across sessions – use Local Storage instead.
Large data storage – limited capacity.

import { useState, useEffect } from 'react';

function SessionStorageExample() {
  // State to store the draft text
  const [draft, setDraft] = useState('');

  // Load draft from sessionStorage when the component mounts
  useEffect(() => {
    const savedDraft = sessionStorage.getItem('draft'); // Retrieve the saved draft
    if (savedDraft) setDraft(savedDraft); // If a draft exists, update state
  }, []); // Runs only once when the component mounts

  // Save the draft to sessionStorage whenever it changes
  useEffect(() => {
    sessionStorage.setItem('draft', draft); // Store draft in sessionStorage
  }, [draft]); // Runs whenever `draft` state changes

  return (
    <div>
      {/* Display draft heading */}
      <h2>Your Draft:</h2>

      {/* Textarea for writing draft */}
      <textarea 
        value={draft} // Controlled component with draft state
        onChange={(e) => setDraft(e.target.value)} // Update state on change
        placeholder="Type something..."
      />

      {/* Button to clear draft from sessionStorage */}
      <button onClick={() => {
        sessionStorage.removeItem('draft'); // Remove draft from sessionStorage
        setDraft(''); // Reset the draft state
      }}>
        Clear Draft
      </button>
    </div>
  );
}

export default SessionStorageExample;

Explanation of Key Features:

  1. State Management (useState) - Stores the draft text typed by the user.

  2. Loading Data on Mount (useEffect) - Retrieves the saved draft from sessionStorage when the component first loads.

  3. Auto-Saving Draft (useEffect) - Updates sessionStorage whenever the draft changes.

  4. Button Functionalities: - Clears the draft from sessionStorage and resets the textarea.

Session Storage Behavior

  • Data persists while the tab is open.

  • Data is lost when the tab is closed.

3. Cookies (HTTP-Attached Storage)

What are Cookies?

Small (~4KB) pieces of data sent with every HTTP request to the server. Can have expiration dates.

Why Use Cookies?

  • Server-readable: Automatically sent in HTTP headers.

  • Expiration control: Can be session-only or persistent.

  • Widely supported: Works across all browsers.

When to Use Cookies?

  1. Authentication tokens (JWT, session IDs)

  2. Tracking user behavior (analytics, GDPR-compliant)

  3. CSRF protection (secure tokens for forms)

When NOT to Use Cookies?

Large data storage – 4KB limit per cookie.
Client-only data – unnecessary overhead if not needed by the server.

npm install js-cookie
import { useState, useEffect } from 'react';
import Cookies from 'js-cookie'; // Import js-cookie library

function CookieExample() {
  // State to store user's cookie consent
  const [userConsent, setUserConsent] = useState(false);

  // Check if a cookie for consent already exists when the component mounts
  useEffect(() => {
    const consent = Cookies.get('userConsent'); // Retrieve the 'userConsent' cookie
    if (consent) setUserConsent(consent === 'true'); // Convert string to boolean
  }, []); // Runs only once when the component mounts

  // Function to handle user consent selection
  const handleConsent = (granted) => {
    setUserConsent(granted); // Update state with user's choice
    Cookies.set('userConsent', granted, { expires: 365 }); // Store consent in cookies for 1 year
  };

  return (
    <div>
      {/* Display a message if user has accepted cookies */}
      {userConsent ? (
        <p>Thank you for accepting cookies!</p>
      ) : (
        // Show the cookie banner if user hasn't given consent yet
        <div className="cookie-banner">
          <p>We use cookies to improve your experience.</p>
          <button onClick={() => handleConsent(true)}>Accept</button>
          <button onClick={() => handleConsent(false)}>Decline</button>
        </div>
      )}
    </div>
  );
}

export default CookieExample;

Explanation of Key Features:

  1. State Management (useState) - Stores the user's cookie consent status (true or false).

  2. Checking Existing Cookies (useEffect) - When the component mounts, it checks for a saved cookie and updates the state.

  3. Handling Consent (handleConsent) - When the user clicks "Accept" or "Decline", the choice is:

    1. Saved in cookies using js-cookie

    2. Stored for 1 year using { expires: 365 }

    3. Updated in the component state for immediate effect.

  4. Conditional Rendering:

    • If the user accepts cookies, a thank-you message is shown.

    • If they haven’t given consent, the cookie banner is displayed.

  • Cookies persist even after page reloads and browser restarts.

  • Useful for remembering user preferences over long periods.

4. IndexedDB (Client-Side Database)

What is IndexedDB?

A low-level, NoSQL database for storing large amounts of structured data (~50MB+). Works asynchronously.

Why Use IndexedDB?

  • Large storage capacity: Great for offline apps.

  • Advanced querying: Indexes, transactions, and cursors.

  • Non-blocking: Doesn’t freeze the UI.

When to Use IndexedDB?

  1. Offline-first apps (Progressive Web Apps)

  2. Complex client-side data (e.g., cached API responses)

  3. File storage (blobs, images)

When NOT to Use IndexedDB?

Simple state management – overkill for small data.
Beginner projects – complex API compared to Local Storage.

npm install idb
import { useState, useEffect } from 'react';
import { openDB } from 'idb'; // Import IndexedDB library

function IndexedDBExample() {
  // State to store notes
  const [notes, setNotes] = useState([]);
  const [newNote, setNewNote] = useState('');

  // Initialize IndexedDB when the component mounts
  useEffect(() => {
    const initDB = async () => {
      // Open or create the IndexedDB database
      const db = await openDB('NotesDB', 1, {
        upgrade(db) {
          // Create an object store (table) for notes if it doesn't exist
          if (!db.objectStoreNames.contains('notes')) {
            db.createObjectStore('notes', { keyPath: 'id' });
          }
        },
      });

      // Load existing notes from the database
      const savedNotes = await db.getAll('notes');
      setNotes(savedNotes); // Update state with the retrieved notes
    };

    initDB();
  }, []); // Runs only once when the component mounts

  // Function to add a new note
  const addNote = async () => {
    if (!newNote.trim()) return; // Prevent adding empty notes

    const db = await openDB('NotesDB', 1); // Open the database
    const note = { id: Date.now(), text: newNote }; // Create a new note object
    await db.add('notes', note); // Add the note to IndexedDB

    setNotes([...notes, note]); // Update the state with the new note
    setNewNote(''); // Clear the input field
  };

  return (
    <div>
      {/* Heading */}
      <h2>My Notes</h2>

      {/* Input field to enter a new note */}
      <input
        value={newNote} // Controlled component linked to state
        onChange={(e) => setNewNote(e.target.value)} // Update state on change
        placeholder="Add a new note"
      />

      {/* Button to save the note */}
      <button onClick={addNote}>Save</button>

      {/* Display saved notes */}
      <ul>
        {notes.map(note => (
          <li key={note.id}>{note.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default IndexedDBExample;

Explanation :

  1. State Management (useState) - Stores notes and new note input.

  2. Database Initialization (useEffect)

    • Opens or creates the IndexedDB database.

    • Creates an object store (notes) if it doesn't exist.

    • Retrieves saved notes from the database.

  3. Adding a New Note (addNote)

    • Creates a new note object with a unique id.

    • Saves the note to IndexedDB.

    • Updates the state to reflect the changes.

  4. Rendering Notes - Displays all saved notes using a <ul> list.

Why Use IndexedDB?

  • Persistent storage: Data remains even after page reloads.

  • Larger storage: Unlike localStorage or sessionStorage, it can store more data.

  • Useful for offline apps.

Comparison Table: When to Use What?

Storage MethodPersistenceCapacityBest Use CaseAvoid When
Local StorageForever (until cleared)~5MBUser preferences, cachingSensitive data, large datasets
Session StorageTab session~5MBForm data, per-tab stateCross-session storage
CookiesConfigurable (session/expiry)~4KBAuth tokens, server-needed dataLarge data, client-only needs
IndexedDBForever (until cleared)~50MB+Offline apps, large datasetsSimple state, beginners
React State/ContextIn-memory (resets on refresh)N/AUI state, real-time updatesPersistent storage needed

Choosing the Right Storage

  • Need persistence? → Local Storage / IndexedDB

  • Tab-specific data? → Session Storage

  • Server-readable data? → Cookies

  • Large structured data? → IndexedDB

  • Temporary UI state? → React State/Context

Security Considerations

  • Never store passwords/tokens in Local/Session Storage (XSS risk).

  • Use HttpOnly cookies for authentication.

  • Sanitize stored data to prevent injection attacks.

Now Routing!!!

What is React Router DOM?

React Router DOM is a package that allows developers to implement dynamic routing in a React application. Unlike traditional multi-page applications, React Router enables client-side routing, allowing users to navigate between different views without a full page reload.

Key Features:

  • Declarative Routing: Define routes using JSX components.

  • Dynamic Route Matching: Supports parameters, query strings, and nested routes.

  • Programmatic Navigation: Use hooks like useNavigate and useParams for navigation.

  • Lazy Loading Support: Optimize performance by loading components only when required.

Installation

To use React Router DOM, install the package via npm or yarn:

npm install react-router-dom
# or
yarn add react-router-dom

Setting Up React Router

1. Basic Routing Setup

To start using React Router, wrap your application with the BrowserRouter component and define routes inside Routes and Route components:

// Importing required modules from React and React Router DOM
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// Importing page components
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';

/**
 * Main App Component
 * 
 * This component sets up the routing for the entire application using React Router DOM.
 * It defines different routes and associates them with their respective components.
 */
function App() {
  return (
    // Wrapping the application with <Router> to enable client-side routing
    <Router>
      {/* 
        <Routes> component acts as a container for all individual routes.
        It replaces the older <Switch> component (v5) and is more efficient.
      */}
      <Routes>
        {/*
          Route Definitions:
          - Each <Route> maps a URL path to a React component.
          - When the URL matches the specified path, the corresponding component renders.
        */}

        {/* 
          Home Route (Root path "/")
          - Renders the Home component when the URL is exactly "/"
        */}
        <Route path="/" element={<Home />} />

        {/* 
          About Route ("/about")
          - Renders the About component when the URL matches "/about"
        */}
        <Route path="/about" element={<About />} />

        {/* 
          Contact Route ("/contact")
          - Renders the Contact component when the URL matches "/contact"
        */}
        <Route path="/contact" element={<Contact />} />

        {/* To handle unknown routes, add a catch-all * route at the end: */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}

// Export the App component as the default export
export default App;

Explanation:

  1. <Router>

    • The root component that enables client-side routing.

    • BrowserRouter (aliased as Router) uses HTML5 history API for navigation.

  2. <Routes>

    • Replaces <Switch> from v5.

    • Automatically picks the best matching route (more efficient than Switch).

  3. <Route>

    • Defines a mapping between a URL path and a component.

    • path: The URL path to match (e.g., /about).

    • element: The component to render when the path matches.

  4. Order of Routes

    • Routes are matched from top to bottom.

    • The path="/" is the default route (matches the root URL).

Dynamic Route

How to Set Up a Dynamic Route

In React Router DOM, you define dynamic segments in a route path using a colon (:). For example:

<Route path="/user/:id" element={<UserProfile />} />

Explanation:

  • :id is a dynamic parameter that can match any value in the URL.

  • Examples:

    • /user/123id = "123"

    • /user/johnid = "john"

Full Example in App.js

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import UserProfile from './pages/UserProfile';

function App() {
  return (
    <Router>
      <Routes>
        {/* Dynamic route for user profiles */}
        <Route path="/user/:id" element={<UserProfile />} />
      </Routes>
    </Router>
  );
}

export default App;

Multiple Dynamic Parameters

Example: Blog Post with User ID and Post Slug

<Route path="/blog/:userId/:postSlug" element={<BlogPost />} />

// URL: /blog/123/react-router-guide

Optional Parameters

Example: Optional Tab Parameter

<Route path="/settings/:tab?" element={<SettingsPage />} />

Matches:

  • /settingstab = undefined

  • /settings/profiletab = "profile"

Best Practices

  1. Use Descriptive Parameter Names

    • Prefer /:userId over /:id for clarity.
  2. Handle Missing/Invalid Parameters

    • Check if the parameter exists before using it:

        const { id } = useParams();
        if (!id) return <p>User not found!</p>;
      
  3. Combine with useEffect for Data Fetching

    • Always re-fetch data when the parameter changes:

        useEffect(() => {
          fetchUser(id);
        }, [id]);
      
  4. Use Nested Routes for Complex UIs

    • Example:

        <Route path="/users/:userId" element={<UserLayout />}>
          <Route index element={<UserProfile />} />
          <Route path="posts" element={<UserPosts />} />
        </Route>
      

Nested Routes: Hierarchical Navigation

Nested routes allow you to create a parent-child relationship between routes, enabling shared layouts while rendering different sub-pages.

Why Use Nested Routes?

  • Avoid repeating layouts (e.g., a dashboard with a sidebar).

  • Better code organization.

  • Maintain consistent UI structure.

How to Define Nested Routes

{/*
  * Nested Route Configuration for Dashboard
  *
  * This sets up a parent route ("/dashboard") with two child routes:
  * - "/dashboard/stats"
  * - "/dashboard/settings"
  *
  * The <Dashboard> component serves as the layout container,
  * while <Stats> and <Settings> render inside it via <Outlet>.
  */}
<Route 
  path="/dashboard"  // Parent route path
  element={<Dashboard />}  // Layout component for all nested routes
>
  {/*
    * Child Route: Statistics Dashboard
    * - Path is relative to parent ("stats" = "/dashboard/stats")
    * - Renders inside Dashboard's <Outlet> position
    */}
  <Route 
    path="stats"  // Relative path (no leading slash)
    element={<Stats />}  // Component to render for /dashboard/stats
  />

  {/*
    * Child Route: Settings Panel
    * - Path is relative to parent ("settings" = "/dashboard/settings")
    * - Renders inside Dashboard's <Outlet> position
    */}
  <Route 
    path="settings"  // Relative path
    element={<Settings />}  // Component to render for /dashboard/settings
  />
</Route>
  • Parent Route (/dashboard)

    • Contains the shared layout (<Dashboard />).

    • Uses <Outlet /> to render child routes.

  • Child Routes (stats, settings)

    • Relative paths (stats instead of /dashboard/stats).

    • Automatically rendered inside the parent's <Outlet />.

Implementation in Dashboard Component

import { Outlet, Link } from 'react-router-dom';

function Dashboard() {
  return (
    <div className="dashboard-layout">
      {/* Shared Dashboard Header */}
      <header>
        <h1>Admin Dashboard</h1>
        <nav>
          {/* Relative links automatically resolve */}
          <Link to="stats">Statistics</Link>
          <Link to="settings">Settings</Link>
        </nav>
      </header>

      {/* 
        * Dynamic Content Area 
        * Child routes render here based on URL:
        * - /dashboard/stats → <Stats />
        * - /dashboard/settings → <Settings />
        */}
      <div className="content">
        <Outlet />
      </div>
    </div>
  );
}

Key Points:

  • <Outlet /> acts as a placeholder for child routes.

  • Navigation uses relative paths (to="stats" instead of to="/dashboard/stats").

Redirects: Programmatic Navigation

Why Use Redirects?

  • Handle 404 pages.

  • Redirect users after actions (e.g., login, logout).

Using <Navigate /> for Redirects

import { Navigate } from 'react-router-dom';

function NotFound() {
  // Redirect to home if page doesn't exist
  return <Navigate to="/" replace />;
}
  • replace prevents the current URL from being stored in history.

Dynamic Redirects (e.g., After Login)

function Login() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  if (isLoggedIn) {
    return <Navigate to="/dashboard" />;
  }

  return <button onClick={() => setIsLoggedIn(true)}>Login</button>;
}

Protected Routes: Authentication Control

Why Use Protected Routes?

  • Restrict access to authenticated users.

  • Redirect unauthorized users to a login page.

Implementation

import { Navigate } from 'react-router-dom';

function ProtectedRoute({ children, isAuthenticated }) {
  return isAuthenticated ? children : <Navigate to="/login" />;
}

Usage in Routes

{/*
  * Protected Dashboard Route
  *
  * This route implements authentication guarding:
  * - Only accessible when `user` is authenticated
  * - Redirects to login page if unauthorized
  * - Maintains clean route structure while adding security
  */}
<Route
  path="/dashboard"  // The route path to protect
  element={
    {/*
      * Authentication Wrapper
      * - Takes `isAuthenticated` prop (boolean)
      * - Conditionally renders either:
      *   a) The protected content (<Dashboard />) when authenticated, OR
      *   b) Redirects to login when unauthenticated
      */}
    <ProtectedRoute isAuthenticated={!!user}>
      {/*
        * Protected Content
        * Only renders when user is authenticated
        * Contains the actual dashboard component
        */}
      <Dashboard />
    </ProtectedRoute>
  }
/>

{/*
  * Implementation Notes:
  * 1. The `!!user` converts any truthy user object to true
  * 2. The ProtectedRoute handles redirection logic internally
  * 3. All child routes of /dashboard will inherit this protection
  */}

Complete ProtectedRoute Component Implementation:

import { Navigate } from 'react-router-dom';

/**
 * Authentication Guard Component
 * @param {Object} props - Component properties
 * @param {boolean} props.isAuthenticated - Authentication status
 * @param {ReactNode} props.children - Content to protect
 * @returns {ReactNode} Either protected content or redirect
 */
function ProtectedRoute({ isAuthenticated, children }) {
  return isAuthenticated ? (
    // Render protected content if authenticated
    children
  ) : (
    {/*
      * Unauthorized Handling
      * - Redirects to login page
      * - replace=true prevents back-button issues
      * - state preserves intended destination
      */}
    <Navigate 
      to="/login" 
      replace 
      state={{ from: location.pathname }} 
    />
  );
}

Key Features Explained:

  1. Authentication Check

    • Uses isAuthenticated prop to determine access

    • Typically receives user object from context/store

  2. Redirection Logic

    • replace prevents login page from being in navigation history

    • state preserves original destination for post-login redirect

  3. Route Protection

    • Wraps around any sensitive routes

    • Can be reused for multiple protected routes

    • Works with nested routes (children inherit protection)

  4. Security Best Practices

    • Never stores sensitive data in routes

    • Always checks authentication on client AND server

    • Consider adding role-based permissions

Usage Example with Context:

// In your main App component
const { user } = useAuth(); // From your auth context

// In your route configuration
<Route
  path="/admin"
  element={
    <ProtectedRoute isAuthenticated={user?.isAdmin}>
      <AdminPanel />
    </ProtectedRoute>
  }
/>

Lazy Loading Routes for Performance

Why Use Lazy Loading?

  • Reduces initial bundle size.

  • Loads components only when needed.

Using React.lazy and Suspense

// Import React's lazy and Suspense for code splitting
import { lazy, Suspense } from 'react';

{/*
  * Lazy Component Imports
  * - Dynamically imports components only when needed
  * - Each import() returns a Promise that resolves to the module
  * - Webpack automatically creates separate chunks for these
  */}
const Home = lazy(() => import('./pages/Home'));  // Lazy-loaded Home component
const About = lazy(() => import('./pages/About')); // Lazy-loaded About component

function App() {
  return (
    <Router>
      {/*
        * Suspense Boundary
        * - Shows fallback content while lazy components load
        * - Wraps all routes that might use lazy loading
        * - Prevents "pop-in" of unloaded components
        */}
      <Suspense fallback={
        {/*
          * Loading Indicator
          * - Shown during component loading
          * - Can be a spinner, skeleton UI, etc.
          * - Should be lightweight (shows immediately)
          */}
        <div className="loading-spinner">Loading...</div>
      }>
        {/*
          * Route Configuration
          * - Normal route definitions
          * - Lazy components work exactly like regular ones
          */}
        <Routes>
          {/*
            * Home Route
            * - Only loads Home component code when first visited
            * - Subsequent visits use cached version
            */}
          <Route path="/" element={<Home />} />

          {/*
            * About Route
            * - Only loads About component code when first visited
            * - Automatically code-split from main bundle
            */}
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

Key Benefits Explained:

  1. Performance Optimization

     // Without lazy loading (all in main bundle)
     import Home from './pages/Home';
    
     // With lazy loading (separate chunk)
     const Home = lazy(() => import('./pages/Home'));
    
    • Reduces initial bundle size by ~30-50%

    • Faster Time-To-Interactive (TTI) metric

  2. Suspense Behavior

     <Suspense fallback={<LoadingSpinner />}>
       {/* Routes that might trigger lazy loading */}
     </Suspense>
    
    • Shows fallback during component fetch

    • Prevents layout shifts (CLS metric)

  3. Production Build Impact

    • Webpack creates separate files like:

      • home.chunk.js

      • about.chunk.js

    • Loaded automatically when routes are visited

Performance Comparison:

MetricStandard ImportLazy Loading
Initial JS500KB300KB
Home Page Load500KB310KB
About Page Load500KB320KB
TTI2.5s1.8s

Summary

FeaturePurposeImplementation
Nested RoutesHierarchical layouts<Outlet /> in parent
RedirectsNavigate programmatically<Navigate to="..." />
Protected RoutesRestrict accessConditional <Navigate />
Catch-All RouteHandle 404s<Route path="*" element={...} />
Lazy LoadingOptimize performanceReact.lazy + Suspense

Lets understand hooks related to router -

Here are few hooks used by react-router-dom

  1. useNavigate – Programmatically navigate between routes.

  2. useLocation – Access the current URL location.

  3. useParams – Retrieve dynamic URL parameters.

  4. useSearchParams – Access and manipulate query parameters.

  5. useMatch – Check if the current URL matches a pattern.

1. useNavigate – Programmatic Navigation

What is useNavigate?

The useNavigate hook replaces the older useHistory hook (v5) and allows you to navigate programmatically to different routes in your application.

Why is it Needed?

  • Redirect users after form submission.

  • Navigate in response to events (e.g., button clicks, API success).

  • Replace the current route instead of pushing to history.

How to Use useNavigate

import { useNavigate } from 'react-router-dom';

function Login() {
  const navigate = useNavigate();

  const handleLogin = () => {
    // Navigate to the dashboard after login
    navigate('/dashboard');
  };

  const goBack = () => {
    // Go back to the previous page
    navigate(-1);
  };

  return (
    <div>
      <button onClick={handleLogin}>Login</button>
      <button onClick={goBack}>Go Back</button>
    </div>
  );
}

Key Features

  • navigate(path, options) can take:

    • A string path ('/dashboard').

    • A number (-1 for going back, 1 for going forward).

    • An object with replace: true to avoid adding a new history entry.

2. useLocation – Access Current URL

This hook returns the current location object, which contains information about the current URL.

Why is it Needed?

  • Extract pathnames, search queries, or hash values.

  • Perform side effects based on URL changes.

  • Track analytics based on route changes.

How to Use useLocation

import { useLocation } from 'react-router-dom';

function CurrentRoute() {
  const location = useLocation();

  return (
    <div>
      <p>Current Path: {location.pathname}</p>
      <p>Search Params: {location.search}</p>
      <p>Hash: {location.hash}</p>
    </div>
  );
}

Key Properties

  • pathname – The current path (/dashboard).

  • search – Query string (?id=123).

  • hash – Hash fragment (#section).

  • state – Optional state passed via navigate.

3. useParams – Retrieve Dynamic URL Parameters

This hook extracts dynamic route parameters from the URL.

Why is it Needed?

  • Fetch data based on URL parameters (e.g., /users/:id).

  • Build dynamic pages (e.g., blog posts, product details).

How to Use useParams

import { useParams } from 'react-router-dom';

function UserProfile() {
  const { userId } = useParams(); // Extracts from `/users/:userId`

  return <h1>User ID: {userId}</h1>;
}

Example Route Setup

<Route path="/users/:userId" element={<UserProfile />} />

Key Notes

  • Returns an object with key-value pairs of URL parameters.

  • Useful for dynamic data fetching (e.g., useEffect with userId).

4. useSearchParams – Manage Query Parameters

This hook allows reading and updating query parameters (e.g., ?search=react).

Why is it Needed?

  • Implement search filters.

  • Sync state with the URL (e.g., pagination, sorting).

How to Use useSearchParams

import { useSearchParams } from 'react-router-dom';

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q');

  const updateQuery = (newQuery) => {
    setSearchParams({ q: newQuery });
  };

  return (
    <div>
      <input 
        value={query || ''} 
        onChange={(e) => updateQuery(e.target.value)} 
      />
      <p>Current Search: {query}</p>
    </div>
  );
}

Key Features

  • searchParams – Works like URLSearchParams (supports .get(), .getAll()).

  • setSearchParams – Updates the query string and triggers a re-render.

5. useMatch – Check URL Match

This hook checks if the current URL matches a given pattern.

Why is it Needed?

  • Conditionally render components based on the route.

  • Highlight active navigation links.

How to Use useMatch

import { useMatch } from 'react-router-dom';

function NavLink({ to, children }) {
  const match = useMatch(to);

  return (
    <li style={{ fontWeight: match ? 'bold' : 'normal' }}>
      {children}
    </li>
  );
}

Key Notes

  • Returns null if no match, or an object with match details.

  • Useful for styling active links in navigation.

Conclusion

React Router DOM hooks provide a clean and efficient way to manage routing in React applications. Here’s a quick recap:

HookPurposeExample Use Case
useNavigateProgrammatic navigationRedirect after login
useLocationAccess current URLAnalytics tracking
useParamsExtract URL parametersDynamic user profiles
useSearchParamsRead/update query paramsSearch filters
useMatchCheck URL matchActive link styling

Project Time!!!

We will create a simple project, focusing on using useReducer and context.

https://github.com/vaishdwivedi1/reducer

Introduction to Memoization in React

Memoization is a way to make functions run faster by saving their results. If the function gets the same inputs again, it returns the saved result instead of doing the work all over. In React, you can use useMemo and useCallback to do this in functional components.

  • useMemo: Memoizes a computed value.

  • useCallback: Memoizes a function itself.

Both hooks help in optimizing performance by reducing unnecessary computations and re-renders.

What is useMemo?

useMemo is a hook that memoizes a computed value and only recalculates it when one of its dependencies changes. This is useful for expensive calculations that shouldn’t run on every render.

Syntax

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • First argument: A function that computes the value.

  • Second argument: An array of dependencies. The memoized value is recalculated only when these dependencies change.

How JavaScript Compares Values:

  • JavaScript compares objects and functions by reference, not by value.

  • Even if two objects or functions look the same, they are treated as different if they are created separately.

Example:

const a = { name: "John" };
const b = { name: "John" };

console.log(a === b); // false (because they are different objects in memory)

Even though a and b have the same content, they are stored at different memory locations — so they are not equal.

What Happens During a Re-Render

  1. When a parent component re-renders, all child components re-render by default.

  2. During the re-render:

    • Any objects or functions defined inside the parent are created again.

    • Since objects/functions are compared by reference, React sees them as new props — even if their values haven't changed.

    • This causes child components to re-render unnecessarily.

Example:

const MyComponent = () => {
  const handleClick = () => {
    console.log("Clicked!");
  };

  return <ChildComponent onClick={handleClick} />;
};
  • Every time MyComponent re-renders:

    • handleClick is recreated.

    • React sees handleClick as a new prop.

    • This causes ChildComponent to re-render.

How useMemo helps

useMemo — remembers the result of a calculation and only recalculates it if dependencies change.

Example:

const value = useMemo(() => expensiveCalculation(x), [x]);
  • If x hasn’t changed, React reuses the previous result without recalculating it.How React.memo Works with Memoization

React.memo wraps a component and only allows re-renders if the props actually change (using a shallow comparison).

Example:

const ChildComponent = React.memo(({ onClick }) => {
  console.log("Rendering Child");
  return <button onClick={onClick}>Click Me</button>;
});
  • If onClick is stable, React.memo will skip re-rendering because the props haven’t changed.

When to Use useMemo?

  1. Expensive Calculations
    When a function takes significant time to compute (e.g., sorting large arrays, complex math operations), useMemo prevents it from running unnecessarily.

     const sortedList = useMemo(() => {
       return largeArray.sort((a, b) => a - b);
     }, [largeArray]);
    
  2. Referential Equality in Rendering
    If a computed object or array is passed as a prop to a child component, useMemo ensures the reference remains the same unless dependencies change, preventing unnecessary re-renders.

     const userData = useMemo(() => ({ name, age }), [name, age]);
     return <UserProfile data={userData} />;
    

When NOT to Use useMemo?

  • For simple calculations where memoization overhead outweighs benefits.

  • When dependencies change frequently, making memoization ineffective.

What is useCallback?

useCallback memoizes a function itself, ensuring that the function reference remains the same between re-renders unless dependencies change. This is particularly useful when passing callbacks to optimized child components (e.g., React.memo).

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
  • First argument: The function to memoize.

  • Second argument: Dependency array. The function is recreated only when dependencies change.

When to Use useCallback?

  1. Preventing Unnecessary Re-renders in Child Components
    If a parent component passes a callback to a child wrapped in React.memo, useCallback ensures the child doesn’t re-render unnecessarily.

     const handleClick = useCallback(() => {
       console.log('Button clicked!');
     }, []);
    
     return <MemoizedButton onClick={handleClick} />;
    
  2. Optimizing Event Handlers
    When an event handler depends on props or state, useCallback prevents recreating the function on every render.

     const handleSubmit = useCallback(() => {
       submitForm(data);
     }, [data]);
    
  3. Dependencies in Effects
    If a function is used inside useEffect, useCallback ensures stability.

     const fetchData = useCallback(async () => {
       const res = await fetch(url);
       setData(res.json());
     }, [url]);
    
     useEffect(() => {
       fetchData();
     }, [fetchData]);
    

When NOT to Use useCallback?

  • For simple functions that don’t cause performance issues.

  • If the function is not passed down to memoized components.

Stable Callbacks for Child Components

const Child = React.memo(({ onClick }) => {
  console.log("Child re-rendered");
  return <button onClick={onClick}>Click</button>;
});

const Parent = () => {
  const [count, setCount] = useState(0);

  // Without useCallback, Child re-renders every time
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // Stable reference

  return <Child onClick={handleClick} />;
};

Why? Prevents Child from re-rendering unnecessarily.

Dynamic Callbacks with Dependencies

const [userId, setUserId] = useState(1);
const fetchUser = useCallback(async () => {
  const res = await fetch(`/api/users/${userId}`);
  return res.json();
}, [userId]); // Recreates only when `userId` changes

Pitfall: Forgetting userId in dependencies causes stale closures.

Combining with useEffect

const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
  const res = await fetch('/api/data');
  setData(await res.json());
}, []);

useEffect(() => {
  fetchData();
}, [fetchData]); // Safe because fetchData is memoized

Without useCallback, fetchData changes every render → useEffect runs infinitely.

Common Edge Cases

  1. Stale Closures in useCallback
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
  console.log(count); // Always logs initial value (0)
}, []); // Missing `count` dependency

Fix:

const logCount = useCallback(() => {
  console.log(count);
}, [count]); // Correct

// Or use the functional update pattern:

const increment = useCallback(() => {
  setCount(prev => prev + 1); // Always gets latest state
}, []);
  1. Over-Memoization (Performance Harm)
// Unnecessary memoization (simple function)
const sayHello = useCallback(() => {
  console.log("Hello");
}, []);

// Better: Just define it normally
const sayHello = () => console.log("Hello");

Rule of thumb: Only memoize if:

  • Passing to React.memo components.

  • Used in useEffect/useMemo dependencies.

Real-World Examples

  1. Optimizing a Data Grid
const DataGrid = ({ rows, sortKey }) => {
  const sortedRows = useMemo(() => {
    return [...rows].sort((a, b) => a[sortKey] - b[sortKey]);
  }, [rows, sortKey]);

  return (
    <table>
      {sortedRows.map(row => (
        <Row key={row.id} data={row} />
      ))}
    </table>
  );
};

Why? Avoids re-sorting thousands of rows on every render.

  1. Preventing Re-renders in a Form
const Form = () => {
  const [values, setValues] = useState({ name: "", email: "" });

  // Memoize onChange to prevent Input re-renders
  const handleChange = useCallback((e) => {
    setValues(v => ({ ...v, [e.target.name]: e.target.value }));
  }, []);

  return (
    <form>
      <Input name="name" value={values.name} onChange={handleChange} />
      <Input name="email" value={values.email} onChange={handleChange} />
    </form>
  );
};

const Input = React.memo(({ name, value, onChange }) => {
  console.log(`Input ${name} rendered`);
  return <input name={name} value={value} onChange={onChange} />;
});

Result: Each Input only re-renders when its own value changes.

When Memoization Helps

ScenarioImprovement
Large lists (~1k+ items)2-5x faster renders
Frequent re-renders (e.g., animations)Smointer UI
Heavy computations (e.g., data sorting)Avoids UI freezes

When Memoization Hurts

ScenarioPerformance Cost
Overusing on simple componentsSlower initial render
Memoizing tiny functionsMemory overhead
Unnecessary dependency trackingComplex code

Rule: Measure first! Use React DevTools’ profiler to identify bottlenecks.

Key Takeaways

  1. useMemo → Cache values (objects, arrays, computations).

  2. useCallback → Cache functions (event handlers, callbacks).

  3. Always include dependencies to avoid stale closures.

  4. Avoid premature optimization—profile before memoizing.

  5. Combine with React.memo for maximum re-render prevention.

Key Differences Between useMemo and useCallback

FeatureuseMemouseCallback
PurposeMemoizes a computed valueMemoizes a function
ReturnsA value (number, array, object)A function
Use CaseOptimizing expensive calculationsPreventing unnecessary re-renders in child components
Exampleconst result = useMemo(() => compute(a, b), [a, b])const fn = useCallback(() => doSomething(a, b), [a, b])

Why Do We Need useId?

Before useId, developers often used:

  • Math.random() → Unreliable, changes on re-renders.

  • uuid/nanoid → Works but causes hydration mismatches in SSR.

  • Manual counters (let id = 0) → Breaks with React's concurrent rendering.

Hydration Mismatches in SSR

When React hydrates server-rendered HTML, mismatched client/server IDs cause:

  • Accessibility issues (e.g., broken aria-labelledby).

  • Console warnings (hydration errors).

  • UI inconsistencies (e.g., form labels not linked correctly).

How useId Solves This

  • Generates stable, unique IDs that match between server and client.

  • Works with React 18+ concurrent features (no race conditions).

  • Avoids manual ID management.

How useId Works

import { useId } from 'react';

function PasswordField() {
  const id = useId(); // Generates a unique ID like ":r1:"

  return (
    <>
      <label htmlFor={id}>Password</label>
      <input id={id} type="password" />
    </>
  );
}

// o/p

<label for=":r1:">Password</label>
<input id=":r1:" type="password" />

Key Features

FeatureExplanation
Stable across re-rendersSame ID on every render.
SSR-compatibleMatches server/client IDs.
Prefix-basedUses : to avoid conflicts with manual IDs.
Works with concurrent renderingSafe in <Suspense> and transitions.

Use Cases

1. Form Labels & Inputs

function EmailForm() {
  const emailId = useId();
  const passwordId = useId();

  return (
    <form>
      <label htmlFor={emailId}>Email</label>
      <input id={emailId} type="email" />

      <label htmlFor={passwordId}>Password</label>
      <input id={passwordId} type="password" />
    </form>
  );
}

2. ARIA Attributes for Accessibility

function Alert() {
  const headingId = useId();
  const contentId = useId();

  return (
    <div role="alert" aria-labelledby={headingId} aria-describedby={contentId}>
      <h2 id={headingId}>Error</h2>
      <p id={contentId}>Invalid password.</p>
    </div>
  );
}

3. Dynamic Lists with Unique Keys

function CheckboxList({ items }) {
  return (
    <ul>
      {items.map((item) => {
        const id = useId(); // ❌ DON'T DO THIS (violates rules of hooks)
        return (
          <li key={item.id}>
            <input id={id} type="checkbox" />
            <label htmlFor={id}>{item.label}</label>
          </li>
        );
      })}
    </ul>
  );
}

⚠️ Warning: Never call useId inside loops/maps (breaks React's rules). Instead:

function CheckboxList({ items }) {
  const baseId = useId(); // ✅ Single ID generation

  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id}>
          <input id={`${baseId}-${index}`} type="checkbox" />
          <label htmlFor={`${baseId}-${index}`}>{item.label}</label>
        </li>
      ))}
    </ul>
  );
}

Key Points

1. Don’t Use useId for key Props

//  Wrong (keys should be derived from data, not hooks)
{items.map((item) => <Item key={useId()} />)}

// Correct
{items.map((item) => <Item key={item.id} />)}

2. Avoid Using useId Inside Loops

//Wrong (calls hook dynamically)
{items.map(() => {
  const id = useId(); // Breaks rules of hooks
  return <div id={id} />;
})}

// Correct (generate one base ID)
const baseId = useId();
{items.map((_, index) => <div id={`${baseId}-${index}`} />)}

3. Don’t Override useId for Custom Logic

//  Avoid (unless absolutely necessary)
const id = useId();
const customId = `user-${id.replace(/:/g, "")}`;

4. Use for Accessibility, Not Styling

//  Avoid (IDs should not drive CSS)
<div id={useId()} className="fancy-box" />

Comparison with Other ID Solutions

MethodSSR-SafeStableConcurrent-Mode Safe
useIdYesYesYes
Math.random()NoNoNo
uuid/nanoidNoYesNo
Manual counter (let id=0)NoNoNo

Why Does useInsertionEffect Exist?

The Problem: Flash of Unstyled Content (FOUC)

When using CSS-in-JS solutions, styles are often injected at runtime. This can cause:

  • FOUC: A brief flicker where unstyled content appears before styles load.

  • Layout shifts: Sudden style changes disrupt the UI.

  • Performance bottlenecks: Injecting styles too late can slow rendering.

How useInsertionEffect Solves This

  • Runs before React makes DOM changes (even before useLayoutEffect).

  • Ensures styles are injected before the browser paints.

  • Optimized for high-performance CSS injection.

How useInsertionEffect Works

useInsertionEffect(() => {
  // Inject styles here
  const style = document.createElement('style');
  style.textContent = `button { color: red; }`;
  document.head.appendChild(style);

  return () => style.remove(); // Cleanup
}, []);

Key Differences from Other Effects

HookTimingUse Case
useInsertionEffectBefore DOM mutationsInjecting styles
useLayoutEffectAfter DOM mutations, before paintMeasuring layout
useEffectAfter render & paintSide effects

Execution Order

  1. useInsertionEffect → Style injection.

  2. DOM updates → React applies changes.

  3. useLayoutEffect → Layout measurements.

  4. Browser paint → Final render.

Use Cases

1. CSS-in-JS Libraries (styled-components, Emotion)

function useCSS(rule) {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.textContent = rule;
    document.head.appendChild(style);
    return () => style.remove();
  }, [rule]);
}

function Button() {
  useCSS(`button { background: red; }`);
  return <button>Click</button>;
}

2. Critical CSS for SSR

function CriticalStyles() {
  useInsertionEffect(() => {
    if (typeof window === 'undefined') return; // Skip SSR
    injectStyles('...');
  }, []);
}

3. Dynamic Theme Switching

function ThemeProvider({ theme }) {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.textContent = `:root { --color: ${theme.color}; }`;
    document.head.appendChild(style);
    return () => style.remove();
  }, [theme]);
}

Common Errors

1. Avoid Using for Non-Style Logic

//  Wrong (use `useLayoutEffect` instead)
useInsertionEffect(() => {
  const rect = ref.current.getBoundingClientRect(); // Too late!
}, []);

2. Always Clean Up Styles

useInsertionEffect(() => {
  const style = document.createElement('style');
  document.head.appendChild(style);
  return () => style.remove(); //  Essential!
}, []);

3. Don’t Use in Server-Side Rendering (SSR)

useInsertionEffect(() => {
  //  Fails in SSR (no `document`)
  document.head.appendChild(style);
}, []);

4. Prefer Libraries Over Manual Injection

  • styled-components and Emotion already optimize this.

  • Only use useInsertionEffect if building a custom CSS-in-JS solution.

Key Takeaways

  1. Purpose: Optimizes CSS-in-JS style injection.

  2. Timing: Runs before DOM mutations (earlier than useLayoutEffect).

  3. Best for: Libraries like styled-components, not app code.

  4. Cleanup: Always remove injected styles.

  5. Alternatives: Use useEffect/useLayoutEffect for non-style logic.

What is useTransition?

useTransition is a React Hook that allows you to mark state updates as transitions. Unlike normal state updates (which are synchronous and blocking), state updates wrapped in useTransition are treated as non-blocking. This means that React will allow the user interface (UI) to remain interactive while the transition state is being computed.

Syntax

const [isPending, startTransition] = useTransition();

Parameters

  • isPending → A boolean value indicating whether the transition is still in progress.

  • startTransition → A function that wraps the state update you want to treat as a transition.

How It Works

  • React will prioritize immediate updates (such as clicks or typing) over transitions.

  • If the transition update takes time, React will show the existing UI and keep it responsive until the update is complete.

Why Do You Need useTransition?

In a standard React state update, when you change a state, React will:

  1. Re-render the entire component

  2. Block the UI until the new state is computed

This can cause performance issues, especially when dealing with:
1. Large data sets (like filtering or sorting)
2. Complex calculations
3. Expensive DOM manipulations

With useTransition, you can:
1. Keep the UI responsive while updates are computed
2. Allow low-priority updates to be processed in the background
3. Prevent UI freezing

How useTransition Works Internally

React classifies state updates into two categories:

  1. Urgent Updates → High-priority updates like typing, clicking, or pressing a button.

  2. Transition Updates → Low-priority updates like filtering or sorting large datasets.

Without useTransition

  • State updates are synchronous

  • The UI freezes during processing

  • The user experience is degraded

With useTransition

  • State updates are marked as non-urgent

  • React allows high-priority updates to go through first

  • The UI remains responsive

Example

In this example, updating the state directly causes the UI to freeze when filtering a large list:

const LargeList = ({ items }) => {
  // State to store the search query
  const [query, setQuery] = useState('');

  // Filter items based on the query (case-sensitive)
  const filteredItems = items.filter(item => item.includes(query));

  // Handle input change and update query state
  const handleChange = (e) => {
    setQuery(e.target.value); // Direct state update with the input value
  };

  return (
    <div>
      {/* Input field for searching items */}
      <input 
        type="text" 
        value={query} 
        onChange={handleChange} 
        placeholder="Search items..." 
      />

      {/* Render the filtered list */}
      <ul>
        {filteredItems.map(item => (
          <li key={item}>{item}</li> // Use item as key (assuming items are unique)
        ))}
      </ul>
    </div>
  );
};

With useTransition – Keeps UI Responsive During Update

Now, let's wrap the state update in startTransition to prevent UI freezing:

const LargeList = ({ items }) => {
  // State to store the search query
  const [query, setQuery] = useState('');

  // State to manage transition status
  const [isPending, startTransition] = useTransition();

  // Filter items based on the query (case-sensitive)
  const filteredItems = items.filter(item => item.includes(query));

  // Handle input change and update query state within a transition
  const handleChange = (e) => {
    // Start a transition to update state without blocking the UI
    startTransition(() => {
      setQuery(e.target.value);
    });
  };

  return (
    <div>
      {/* Input field for searching items */}
      <input 
        type="text" 
        value={query} 
        onChange={handleChange} 
        placeholder="Search items..." 
      />

      {/* Show loading indicator while state is updating */}
      {isPending && <div>Updating...</div>}

      {/* Render the filtered list */}
      <ul>
        {filteredItems.map(item => (
          <li key={item}>{item}</li> // Use item as key (assuming items are unique)
        ))}
      </ul>
    </div>
  );
};

Explanation

  • startTransition marks the setQuery state update as non-blocking.

  • While the update is in progress, the UI remains interactive.

  • isPending is true while the transition is processing, so you can display a loading state or message.

When to Use useTransition

1. Filtering or Sorting Large Lists

  • When filtering or sorting a large list, the operation can be computationally expensive, causing UI lag.

  • useTransition allows React to update the state for filtering or sorting in the background, keeping the UI responsive.

Example:

  • Filtering a list of 10,000+ items based on user input.

  • useTransition will ensure that the input field remains responsive while the list is being updated.

const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
  startTransition(() => {
    setQuery(e.target.value);
  });
};

2. Paginating Data

  • When loading paginated data, switching between pages can cause noticeable lag.

  • useTransition helps keep the UI responsive while React loads the next set of data in the background.

Example:

  • Loading the next page of search results while keeping the search bar responsive.
const handlePageChange = (page) => {
  startTransition(() => {
    setCurrentPage(page);
  });
};

3. Lazy Loading Components

  • When rendering components dynamically (lazy loading), there may be a slight delay that can freeze the UI.

  • useTransition allows React to prioritize the user interaction and render the component after it’s available.

Example:

  • Loading a chart or table component after a user clicks a button.
const handleLoadComponent = () => {
  startTransition(() => {
    setShowChart(true);
  });
};

4. Updating Complex State Without UI Blocking

  • When updating complex state structures or large objects, direct state updates can block the UI.

  • useTransition lets React process these updates in the background while keeping the UI responsive.

Example:

  • Updating a nested object with multiple state changes.
const handleComplexUpdate = (newData) => {
  startTransition(() => {
    setComplexState((prevState) => ({
      ...prevState,
      ...newData,
    }));
  });
};

5. Rendering Expensive or Nested Components

  • Components that require expensive calculations or deep nested rendering may cause lag.

  • useTransition allows React to split the rendering work into smaller units, keeping the UI responsive.

Example:

  • Rendering a large, complex data tree.
const handleRenderTree = () => {
  startTransition(() => {
    setTreeData(generateLargeTree());
  });
};

Why useTransition Works

  • React marks state updates inside startTransition as low priority.

  • High-priority updates (like user input) are processed first, while background updates (like filtering) happen afterward.

  • This ensures that the UI remains smooth and responsive even during heavy computation.

When NOT to Use useTransition

  • For simple state updates (like toggling a boolean).

  • When the update is critical and needs to happen immediately (e.g., form submission).

  • For animations or micro-interactions where immediate feedback is expected.

How React Prioritizes Updates with useTransition

React uses an event loop to handle state updates:

  1. Immediate state updates (like clicks) are processed with high priority.

  2. Transition updates (like filtering) are processed with low priority.

  3. If a new high-priority event comes in (like a user click), React will pause the transition update and resume it later.

Example:

  • A user types into an input → React processes it immediately.

  • Filtering the list → React treats this as low-priority and processes it only when the UI is idle.

useTransition vs. useDeferredValue

Both useTransition and useDeferredValue handle state transitions, but they work differently:

FeatureuseTransitionuseDeferredValue
TypeFunction that marks state updates as transitionsValue that React defers updating
Use CaseFor handling expensive state updatesFor deferring low-priority values
ExampleFiltering a large listPassing props to a child component
UI StateShows pending stateKeeps stale value until updated

What is useDeferredValue?

useDeferredValue is a React hook introduced in React 18 that allows you to defer the value of a state update until after the more urgent updates (like user input) are handled.

It helps prevent UI lag by telling React: "This value is not urgent — update it when you have time."

Syntax

const deferredValue = useDeferredValue(value);
  • value – The state or value you want to defer.

  • deferredValue – A deferred version of the value that React will update after handling more urgent state updates.

How useDeferredValue Works

  1. When you pass a value to useDeferredValue, React creates a deferred copy of the value.

  2. React will try to keep the deferred value in sync with the original value.

  3. If React is busy handling high-priority updates (like user input), it will postpone updating the deferred value until the UI becomes idle.

Example:

Let’s say you have a search bar filtering a large list of items. Without useDeferredValue, the list would rerender with every keystroke, causing UI lag.

With useDeferredValue, React will:
1. Prioritize updating the input field first (high priority)
2. Delay updating the filtered list until the user stops typing or the UI is idle

import { useState, useDeferredValue } from 'react';

const LargeList = ({ items }) => {
  // State to store the search query entered by the user
  const [query, setQuery] = useState('');

  // Deferred version of the search query
  // React will delay updating `deferredQuery` until more urgent updates (like typing) are handled
  const deferredQuery = useDeferredValue(query);

  // Filter items based on the deferred query value
  // This prevents filtering from happening on every keystroke, improving performance
  const filteredItems = items.filter(item =>
    item.includes(deferredQuery) 
  );

  return (
    <div>
      {/* Input field for user to type search query */}
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)} // Update the state immediately on change
        placeholder="Search items..."
      />

      {/* Render the filtered list based on the deferred value */}
      <ul>
        {filteredItems.map(item => (
          <li key={item}>{item}</li> // Use item as key (assuming items are unique)
        ))}
      </ul>
    </div>
  );
};

export default LargeList;

Explanation:

1. query is updated immediately with each keystroke.
2. deferredQuery lags behind slightly — it updates after the input field has been updated.
3. This prevents UI blocking and lag because React focuses on updating the input field first (high priority) and processes the list filtering when the UI becomes idle.

Why useDeferredValue is Important

Without useDeferredValue:

  • Every state change (each keystroke) causes an immediate re-render of the entire list.

  • The UI becomes unresponsive because React is busy rendering the list.

With useDeferredValue:

  • React first updates the input field.

  • React defers the list update until the input event is processed.

  • The UI remains smooth and responsive.

When to Use useDeferredValue

useDeferredValue is especially useful when:

1. Filtering or Sorting Large Lists

  • For large datasets, updating the list in real-time while typing causes UI lag.

  • useDeferredValue allows the input field to update immediately while the list updates in the background.

2. Rendering Expensive Components

  • Rendering complex components (like charts, graphs, or tables) on every state change can block the UI.

  • useDeferredValue delays the rendering to prevent UI blocking.

3. Debouncing-Like Behavior Without Delays

  • useDeferredValue behaves like debouncing but without the need for setTimeout().

  • The value updates as soon as React is ready — no manual timing needed.

4. Avoiding Flickering in Real-Time Applications

  • In real-time apps (e.g., chat, live updates), constant re-rendering can cause flickering.

  • useDeferredValue ensures smooth updates by prioritizing high-priority state changes first.

useDeferredValue vs useTransition

Both useDeferredValue and useTransition help with performance, but they handle state differently:

FeatureuseDeferredValueuseTransition
TypeHook to delay value updatesHook to delay state updates
ScopeWorks at the value levelWorks at the state level
PurposeUsed to defer state propagationUsed to mark a state update as low-priority
Use CaseFiltering, sorting, expensive renderingData fetching, state updates, complex state changes
Priority HandlingKeeps UI responsive while updating deferred valuesPrioritizes high-priority state changes first

Rule of Thumb:

  • Use useDeferredValue when you need to delay a single value update.

  • Use useTransition when you need to mark an entire state update as low priority.

Example with useTransition + useDeferredValue

You can combine both useTransition and useDeferredValue for maximum control over state and value updates.

Example:

  • useTransition to handle state changes (e.g., loading).

  • useDeferredValue to handle value propagation.

import { useState, useDeferredValue, useTransition } from 'react';

const LargeList = ({ items }) => {
  // State to store the search query
  const [query, setQuery] = useState('');

  // useTransition to manage state updates without blocking the UI
  const [isPending, startTransition] = useTransition();

  // useDeferredValue delays the value update to reduce rendering load
  const deferredQuery = useDeferredValue(query);

  // Filter the list based on the deferred query
  const filteredItems = items.filter(item =>
    item.includes(deferredQuery)
  );

  // Handle input change using transition to avoid blocking UI rendering
  const handleChange = (e) => {
    // Wrap state update in a transition to allow React to prioritize rendering
    startTransition(() => {
      setQuery(e.target.value);
    });
  };

  return (
    <div>
      {/* Input field for search query */}
      <input type="text" value={query} onChange={handleChange} />

      {/* Show loading indicator while transition is pending */}
      {isPending && <div>Loading...</div>}

      {/* Render the filtered list */}
      <ul>
        {filteredItems.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default LargeList;

What is useImperativeHandle?

useImperativeHandle is a React Hook that lets you decide what properties and methods are available on a component when you use ref with forwardRef. In simple words, it helps you control what you can do with a component when you refer to it using ref.

Basic Syntax

useImperativeHandle(ref, createHandle, [deps])
  • ref: The ref passed down from the parent component via forwardRef

  • createHandle: A function that returns an object with the properties and methods you want to expose

  • deps (optional): Dependency array similar to other React hooks

Why Do We Need useImperativeHandle?

The Problem with Direct Refs

When you use ref with a DOM element or a class component, you get direct access to the underlying DOM node or component instance. This can lead to several issues:

  1. Breaking Encapsulation: Parent components can access and modify anything in the child component

  2. Implementation Coupling: Parents depend on child implementation details

  3. Maintenance Issues: Changes in child components might break parent components

The Solution

useImperativeHandle provides a controlled way to expose only specific methods or values to parent components, maintaining proper encapsulation while still allowing imperative interactions when absolutely necessary.

How to Use useImperativeHandle

Step 1: Forward the Ref

First, you need to wrap your component with forwardRef:

const ChildComponent = forwardRef((props, ref) => {
  // Component implementation
});

Step 2: Define Exposed Methods

Inside your component, use useImperativeHandle to define what should be exposed:

const ChildComponent = forwardRef((props, ref) => {
  const internalState = useState(0);
  const internalMethod = () => {
    console.log('Internal method called');
  };

  useImperativeHandle(ref, () => ({
    // Only expose these methods/values
    increment: () => {
      internalState[1](prev => prev + 1);
    },
    getCount: () => internalState[0],
    // Note: internalMethod is not exposed
  }));

  return <div>{internalState[0]}</div>;
});

Step 3: Use in Parent Component

The parent can now access only the exposed methods:

function ParentComponent() {
  const childRef = useRef();

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={() => childRef.current.increment()}>
        Increment from Parent
      </button>
      <button onClick={() => console.log(childRef.current.getCount())}>
        Log Count
      </button>
    </div>
  );
}

Real-World Use Cases

1. Form Management

Expose form validation and submission methods:

const Form = forwardRef((props, ref) => {
  const [formData, setFormData] = useState({});

  useImperativeHandle(ref, () => ({
    validate: () => {
      // Validation logic
      return Object.values(formData).every(Boolean);
    },
    submit: () => {
      // Submit logic
    },
    getFormData: () => formData
  }));

  // Form fields implementation
});

2. Media Players

Control media playback from parent:

const VideoPlayer = forwardRef((props, ref) => {
  const videoRef = useRef();

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current.play(),
    pause: () => videoRef.current.pause(),
    seek: (time) => {
      videoRef.current.currentTime = time;
    }
  }));

  return <video ref={videoRef} src={props.src} />;
});

Before we explore our next hook, we need to understand what hydration is in React. So, let's dive into that!

Hydration in React

Hydration is when React makes a static HTML page, created on the server, interactive on the client side by adding event handlers.

Simple Explanation

Imagine you order a pizza (your webpage) from a restaurant (the server):

  1. Server Rendering: The restaurant prepares your pizza and delivers it fully cooked (static HTML)

  2. Hydration: When it arrives, you add your own toppings and heat it up (making it interactive)

Without hydration, you'd just get a cold pizza that looks good but you can't eat (a page that looks right but has no interactivity).

How Hydration Works

  1. Server: Renders components to HTML (just the visual structure)

  2. Client: Takes that HTML and "hydrates" it by:

    • Attaching event handlers (onClick, etc.)

    • Connecting state management

    • Enabling interactivity

// Server renders this component as static HTML:
function Button() {
  return <button onClick={() => console.log('Clicked!')}>Click Me</button>;
}

// After hydration on client:
// The button now actually works when clicked!

Why Hydration Matters

  1. Faster initial load: Users see content immediately (from server HTML)

  2. Better SEO: Search engines can crawl the static HTML

  3. Smoother experience: Interactivity loads after the initial render

Common Hydration Problems

  1. Mismatch Errors:

     Warning: Text content did not match. Server: "Hello" Client: "Hi"
    

    Happens when server and client render different content

  2. Missing Hydration:

    • Elements are visible but don't respond to clicks

    • Interactive features don't work

  3. Double Rendering:

    • Client re-renders everything after hydration

Hydration Example

Before Hydration (static HTML):

<div id="root">
  <button>Click Me</button> <!-- Looks like a button but does nothing -->
</div>

After Hydration:

<div id="root">
  <button onclick="handleClick()">Click Me</button> <!-- Now interactive -->
</div>

How to Ensure Proper Hydration

  1. Match server/client renders:

     // Bad - might differ between server/client
     const time = new Date().toLocaleTimeString();
    
     // Good - use effects for client-only values
     const [time, setTime] = useState('');
     useEffect(() => {
       setTime(new Date().toLocaleTimeString());
     }, []);
    
  2. Use hydration-friendly libraries:

     // Instead of direct DOM manipulation
     useEffect(() => {
       // Client-side only code
     }, []);
    
  3. Proper SSR setup:

     // Server
     const html = ReactDOMServer.renderToString(<App />);
    
     // Client
     ReactDOM.hydrateRoot(<App />, document.getElementById('root'));
    

Hydration is what transforms your static webpage into a fully interactive React application while maintaining the performance benefits of server-side rendering.

So time for our next hook…

What is useSyncExternalStore?

useSyncExternalStore is a React Hook that lets components safely and efficiently read from external sources that can change. It's mainly used with state management libraries that need to work with React's concurrent rendering features.

Basic Syntax

const state = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot?
);
  • subscribe: Function that registers a callback to be called whenever the store changes

  • getSnapshot: Function that returns the current value of the store

  • getServerSnapshot (optional): Function that returns the initial snapshot for server-side rendering

Why Do We Need useSyncExternalStore?

1. Tearing: Inconsistent State Rendering

When different parts of your UI show different versions of the same state during updates. Imagine a scoreboard at a basketball game where one part shows 50-48 and another part shows 51-48 at the same time. That's tearing!

Example:

// With an external store
let score = { home: 0, away: 0 };

function updateScore() {
  score = { home: score.home + 1, away: score.away + 1 };
}

// In React 17 and earlier, during concurrent rendering:
<>
  <ScoreDisplay team="home" />  {/* Might show 1 */}
  <ScoreDisplay team="away" />  {/* Might show 0 */}
</>

Why it happens: React's concurrent rendering might interrupt the render process between components, showing inconsistent state.

2. Hydration Mismatches

When the server-rendered HTML doesn't match what React shows on the client after hydration. It's like ordering a blue shirt online (server), but when it arrives (client hydration), it's red!

Example:

// Server-side (Node.js):
const initialData = fetchData(); // Returns "Hello"
renderToString(<App data={initialData} />);

// Client-side:
const initialData = window.__INITIAL_DATA__; // Should be "Hello"
// But if the store changes before hydration:
const storeData = "Hola"; // Different from server!

hydrateRoot(<App data={storeData} />);

// Now React sees mismatch between server "Hello" and client "Hola"

Why it's bad: React will warn about hydration mismatches and may have to re-render everything.

3. Subscription Management

Forgetting to clean up event listeners when components unmount. It's like leaving your TV on when you leave the house - wasting energy (memory)!

Example of bad code:

function Component() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Subscribe to store
    store.subscribe((newData) => setData(newData));

    // Oops! Forgot to return cleanup function
  }, []);

  return <div>{data}</div>;
}

What happens: If this component unmounts and mounts many times, you'll have many subscriptions piling up, causing memory leaks.

4. Concurrent Mode Safety

External stores might not work properly with React's new concurrent features. Imagine a traffic light (React) trying to manage cars (components) that don't follow the signals (store updates).

Example problem:

function useBadStore(store) {
  const [state, setState] = useState(store.getState());

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      // This might interrupt React's rendering work
      setState(store.getState());
    });
    return unsubscribe;
  }, [store]);

  return state;
}

Why it's bad: In Concurrent Mode, React might pause, abort, or restart renders. External stores updating at the wrong time can cause crashes or visual glitches.

How useSyncExternalStore Solves These

  1. Tearing: Ensures all components see the same store version during a render

  2. Hydration: Provides getServerSnapshot to match server and client

  3. Subscriptions: Automatically handles cleanup

  4. Concurrent Safety: Integrates properly with React's scheduler

Fixed Example:

function useGoodStore(store) {
  return useSyncExternalStore(
    store.subscribe,          // How to subscribe
    store.getState,           // How to get current state
    store.getServerState      // How to get state for SSR
  );
}

Now all components will stay in sync, clean up properly, and work with concurrent rendering!

The Solution

useSyncExternalStore provides:

  • A standardized way to subscribe to external stores

  • Protection against tearing during concurrent renders

  • Proper cleanup of subscriptions

  • Support for server-side rendering

How to Use useSyncExternalStore

// Import the useSyncExternalStore hook from React
// This hook allows components to safely read from external data stores
import { useSyncExternalStore } from 'react';

/**
 * Custom hook to subscribe to an external store
 * @param {object} store - The external store object containing subscribe and getSnapshot methods
 * @returns {any} The current state from the store
 */
function useCustomStore(store) {
  // Use useSyncExternalStore to safely read from the external store
  // Parameters:
  // 1. store.subscribe - Function to subscribe to store changes
  // 2. store.getSnapshot - Function to get current store state
  const state = useSyncExternalStore(
    store.subscribe,
    store.getSnapshot
  );
  return state;
}

/**
 * Creates a simple store with state management capabilities
 * @param {any} initialState - The initial state of the store
 * @returns {object} Store object with subscribe, getSnapshot, and setState methods
 */
const createStore = (initialState) => {
  // Internal state variable
  let state = initialState;

  // Set to keep track of all subscribed listeners
  const listeners = new Set();

  /**
   * Subscribes a listener function to store changes
   * @param {function} listener - Callback function to be called on state changes
   * @returns {function} Unsubscribe function to remove the listener
   */
  const subscribe = (listener) => {
    // Add the listener to our set
    listeners.add(listener);
    // Return an unsubscribe function that removes the listener
    return () => listeners.delete(listener);
  };

  /**
   * Returns the current state snapshot
   * @returns {any} Current state
   */
  const getSnapshot = () => state;

  /**
   * Updates the store state and notifies all subscribers
   * @param {any} newState - The new state value
   */
  const setState = (newState) => {
    // Update the state
    state = newState;
    // Notify all subscribed listeners that the state changed
    listeners.forEach(listener => listener());
  };

  // Return the store API
  return { subscribe, getSnapshot, setState };
};

// Create a store instance with initial state { count: 0 }
const store = createStore({ count: 0 });

/**
 * Counter component that displays and updates the count from the store
 */
function Counter() {
  // Use our custom hook to get the current store state
  // We destructure the count property from the state
  const { count } = useCustomStore(store);

  return (
    <div>
      {/* Button that increments the count when clicked */}
      {/* Calls store.setState to update the state */}
      <button onClick={() => store.setState({ count: count + 1 })}>
        Increment: {count}
      </button>
    </div>
  );
}

Explanation:

  1. useCustomStore Hook:

    • Documents the purpose and parameters of the custom hook

    • Explains the use of useSyncExternalStore with its two required parameters

  2. createStore Function:

    • Documents the factory function that creates stores

    • Explains the internal state management mechanism

    • Describes each returned method (subscribe, getSnapshot, setState)

  3. Store Implementation Details:

    • Explains the listeners Set that tracks subscriptions

    • Documents the subscription/unsubscription mechanism

    • Shows how state updates trigger listener notifications

  4. Counter Component:

    • Shows how components consume the store

    • Documents the state access pattern

    • Explains the state update mechanism via setState

  5. Type Information:

    • Uses JSDoc style comments to indicate parameter and return types

    • Documents the shape of expected objects

What is useFormStatus?

useFormStatus is a React Hook that provides real-time status updates for form submissions, including:

  • Whether the form is currently submitting (pending state)

  • The submitted form data

  • Success/error states after submission

Basic Syntax

const { pending, data, action, method } = useFormStatus();
  • pending: A boolean (true when the form is submitting, false otherwise).

  • data: A FormData object containing the submitted form values.

  • action: The form’s action URL (if specified).

  • method: The HTTP method (GET/POST) used in submission.

Why Do We Need useFormStatus?

The Problem Without It

Before useFormStatus, developers had to manually manage form states:

  1. Boilerplate Code – Needed useState for loading, error, and success states.

  2. Race Conditions – Multiple submissions could cause inconsistent UI updates.

  3. Complex Context Management – Required useContext or Redux for shared form state.

How useFormStatus Solves This

  • Automatic State Tracking – No need for manual useState for pending states.

  • Optimized Re-renders – Only re-renders components that consume the status.

  • Simplified Error Handling – Integrates seamlessly with <form> actions.

How to Use useFormStatus

// Import the useFormStatus hook from React
// This hook provides real-time status of form submissions
import { useFormStatus } from 'react';

/**
 * SubmitButton component - A smart button that responds to form submission status
 * Uses useFormStatus to automatically handle pending state
 */
function SubmitButton() {
  // Destructure the pending status from useFormStatus
  // pending = true when form is submitting, false otherwise
  const { pending } = useFormStatus();

  return (
    // The button is disabled when form is submitting to prevent duplicate submissions
    // Text changes to indicate loading state
    <button 
      type="submit" 
      disabled={pending}
      aria-busy={pending} // Accessibility improvement for loading state
      aria-disabled={pending} // Accessibility attribute
    >
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

/**
 * MyForm component - A form that demonstrates useFormStatus integration
 * Contains a text input and our smart SubmitButton
 */
function MyForm() {
  return (
    // Basic form with an action endpoint
    // The action would typically point to a server endpoint or form handler
    <form action="/submit" method="POST">
      {/* 
        Text input field for username 
        The 'name' attribute is important as it identifies this field in formData
      */}
      <input 
        name="username" 
        type="text"
        required // Basic HTML validation
        aria-label="Username" // Accessibility label
      />

      {/* 
        Our smart submit button that automatically handles submission state
        This component must be rendered INSIDE the form element to work properly
      */}
      <SubmitButton />
    </form>
  );
}

Explaination :

  1. Hook Import: Clearly states what useFormStatus is and its purpose

  2. SubmitButton Component:

    • Explains the component's role as a smart submission button

    • Documents the pending state behavior

    • Includes accessibility attributes with explanations

  3. Button Implementation:

    • Explains the conditional rendering logic

    • Mentions duplicate submission prevention

    • Notes the accessibility improvements

  4. MyForm Component:

    • Describes the form's basic structure

    • Explains the action attribute's purpose

    • Documents the input field requirements

  5. Accessibility Notes:

    • Includes ARIA attributes for better screen reader support

    • Explains the importance of the name attribute for form data

  6. Structural Comments:

    • Uses consistent comment formatting

    • Separates logical sections clearly

    • Explains the parent-child relationship requirement (button must be in form)

These comments make the code:

  • More maintainable by explaining the why behind implementation choices

  • More accessible by documenting ARIA attributes

  • Easier to debug by clarifying component relationships

  • More team-friendly by providing clear documentation

How useFormStatus Works

  1. Listens to Form Events – Automatically hooks into the parent <form>’s submit event.

  2. Tracks Pending State – Sets pending: true when submission starts, false on completion.

  3. Optimized Updates – Uses React’s internal scheduler to prevent unnecessary re-renders.

Key Requirements

  • Must be used inside a <form> (throws an error otherwise).

  • Works best with React Server Components (RSC) and Server Actions.Comparison with Alternatives

FeatureuseFormStatususeStateFormik/React Hook Form
BoilerplateMinimalHighModerate
Pending StateBuilt-inManualManual
Error HandlingBasicManualAdvanced
PerformanceOptimizedManualOptimized
Server ActionsYesNoNo

When to Use useFormStatus?
1. Simple forms needing pending state.
2. Apps using React Server Components.
3. Avoiding external libraries.

When to Avoid?
1. Complex validation (use React Hook Form instead).
2. Forms needing advanced error handling.

What is useActionState?

useActionState is a React Hook that manages the state and side effects of form submissions or any asynchronous action. It provides:

  1. Submission state tracking (pending, success, error)

  2. Automatic state updates after action completion

  3. Optimized re-renders without manual dependency management

const [state, action, pending] = useActionState(fn, initialState);
  • fn: The async function to run (typically a form submission)

  • initialState: Starting value before first submission

  • Returns:

    • state: Current result (data or error)

    • action: Wrapped function to trigger

    • pending: Boolean indicating if action is in progress

Why Do We Need useActionState?

The Problem Without It - Traditional approaches require:

  • Manual useState for loading/error states

  • useEffect for side effects

  • Complex error handling

  • Boilerplate for optimistic updates

How useActionState Solves This

  1. Automatic Pending State – No manual isLoading tracking

  2. Built-in Error Handling – Catches errors and updates state

  3. Simpler Optimistic UI – Easy rollback on failure

  4. Seamless Server Actions – Works with Next.js/RSC actions

How to Use useActionState

// Import the useActionState hook from React
// This hook manages state and side effects for form submissions or async actions
import { useActionState } from 'react';

/**
 * Async form submission handler
 * @param {any} prevState - The previous state before submission
 * @param {FormData} formData - Data from the submitted form
 * @returns {Promise<object>} Submission result object
 */
async function submitForm(prevState, formData) {
  // Simulate API call with 1 second delay
  await new Promise(resolve => setTimeout(resolve, 1000));

  // Return success state with parsed form data
  return { 
    success: true, 
    data: Object.fromEntries(formData) // Convert FormData to plain object
  };
}

/**
 * Form component demonstrating useActionState
 * Handles form submission with loading state and result display
 */
function MyForm() {
  // Initialize useActionState hook:
  // - submitForm: The async action handler
  // - null: Initial state (before first submission)
  // Returns:
  // - state: Current submission result (null initially)
  // - submitAction: Action to bind to form
  // - pending: Boolean indicating submission in progress
  const [state, submitAction, pending] = useActionState(submitForm, null);

  return (
    // Form element with action bound to our submitAction
    // The form will automatically pass FormData to our submitForm function
    <form action={submitAction}>
      {/* Email input field - name attribute is required for FormData */}
      <input 
        name="email" 
        type="email" 
        required // HTML5 validation
        aria-label="Email address" // Accessibility
      />

      {/* Submit button with dynamic state:
          - Disabled during submission to prevent duplicate submits
          - Changes text to indicate loading state */}
      <button 
        type="submit" 
        disabled={pending}
        aria-busy={pending} // Accessibility loading indicator
      >
        {pending ? "Sending..." : "Submit"}
      </button>

      {/* Success message - only shown after successful submission */}
      {state?.success && (
        <p aria-live="polite">Submitted: {state.data.email}</p>
      )}
    </form>
  );
}

Explaination:

  1. Hook Import:

    • Clearly explains what useActionState is used for

    • Mentions its role in managing async actions

  2. submitForm Function:

    • Documents parameters with JSDoc

    • Explains the mock API simulation

    • Describes the return value structure

  3. useActionState Initialization:

    • Details each parameter's purpose

    • Explains the return tuple structure

    • Notes the initial state value

  4. Form Component:

    • Explains the form's action binding

    • Documents automatic FormData handling

  5. Input Field:

    • Notes the required name attribute

    • Includes accessibility and validation attributes

  6. Submit Button:

    • Explains the dynamic disabled state

    • Documents the loading state text change

    • Includes ARIA attributes for accessibility

  7. Success Message:

    • Uses aria-live for screen reader announcements

    • Only renders when submission succeeds

Additional Best Practices Included:

  1. Accessibility:

    • aria-busy for loading states

    • aria-live for dynamic updates

    • Proper labeling for form inputs

  2. Validation:

    • HTML5 required attribute

    • Proper input type="email"

  3. Type Safety:

    • JSDoc for function parameters

    • Clear state structure documentation

  4. Performance:

    • Conditional rendering of success message

    • Proper form submission handling

How useActionState Works Internally

1. Action Trigger Phase

When the action is invoked (e.g., form submission):

<form action={submitAction}>  // ← Triggers here

Technical Process:

  1. Pending State Activation

    • Immediately sets pending = true

    • Marks the action as "in progress" in React's scheduler

  2. Transition Start

    • Wraps the action in startTransition (React's concurrent feature)

    • Allows React to interrupt the action if more urgent updates occur

// Simplified React internals
function triggerAction() {
  startTransition(() => {
    setPending(true);
    const result = await actionFunction();
    // ...handle result
  });
}

2. Execution Phase

async function submitForm(prevState, formData) {
  await apiCall(formData);  // ← Your custom logic
  return { success: true };
}

Key Behaviors:

  1. FormData Handling

    • Automatically collects form inputs into FormData

    • Converts to a usable object via Object.fromEntries()

  2. Error Boundaries

    • If the async function throws:

        try { await fn(); } 
        catch (e) { /* Preserves last valid state */ }
      
  3. Suspense Integration

    • Can suspend rendering while waiting (compatible with React Suspense)

3. State Update Phase

Success Case:

BeforeDuringAfter
state = nullpending = truestate = {success: true}
pending = falsestate = nullpending = false

Error Case:

BeforeDuringAfter
state = {data: 1}pending = truestate = {data: 1} (unchanged)
pending = falseError occurspending = false

Optimistic Updates (Advanced):

const [state, action] = useActionState(fn, initialState);

// Preview update before confirmation
action(newData); 
// → Immediately shows newData while running fn() in background
// → Rolls back if fn() fails

4. Re-render Optimization

  1. Non-Urgent Updates

    • Actions are marked as "transition" updates

    • Allows high-priority UI interactions (e.g., typing) to interrupt

  2. Batched Updates

    • Combines multiple state changes into a single re-render

    • Prevents intermediate "janky" states

Performance Benefits

Traditional ApproachuseActionState
Manual useState updatesAtomic state transitions
Possible duplicate rendersSingle render per lifecycle
Race condition risksQueue management by React

Real-World Example Flow

  1. User clicks submit

     sequenceDiagram
       User->>React: Clicks submit
       React->>useActionState: setPending(true)
       useActionState->>API: submitForm()
       API-->>useActionState: {success: true}
       useActionState->>React: setState(result), setPending(false)
    
  2. Network error occurs

     sequenceDiagram
       User->>React: Clicks submit
       React->>useActionState: setPending(true)
       useActionState->>API: submitForm()
       API--X useActionState: Error!
       useActionState->>React: setPending(false) // Keeps old state
    

Why This Matters

  1. Predictable State - No stale data or intermediate "half-loaded" states

  2. Built-in Resilience - Automatic rollback on failures maintains UI consistency

  3. Seamless UX - Pending states integrate with Suspense for smooth loading

This system empowers developers to handle complex async flows while maintaining React's declarative programming model.

Comparison with Alternatives

FeatureuseActionStateuseState + useEffectRedux Thunk
BoilerplateMinimalHighHigh
Pending StateBuilt-inManualManual
Error HandlingAutomaticManualManual
Optimistic UIEasyComplexModerate
Server ActionsNativeNoNo

What is useOptimistic?

useOptimistic is a React Hook that lets you optimistically update the UI while an asynchronous action is processing. If the action ultimately fails, the UI automatically rolls back to the correct state.

Key Features

  • Instant UI updates

  • Automatic rollback on failure

  • Simple integration with async operations

  • Built-in loading states

Syntax

const [optimisticState, addOptimistic] = useOptimistic(
  state,
  (currentState, optimisticValue) => {
    // Return new state based on current + optimistic update
  }
);

Parameters

  1. state: The current "source of truth" state

  2. Update function: How to merge optimistic updates

Returns

  1. optimisticState: The state including pending updates

  2. addOptimistic: Function to trigger optimistic updates

Why Use Optimistic Updates?

Traditional Approach Problems

// Without optimistic updates
const [messages, setMessages] = useState([]);

async function sendMessage(newMessage) {
  setLoading(true);
  try {
    await api.sendMessage(newMessage);
    setMessages([...messages, newMessage]); // Only updates after delay
  } catch {
    // Show error
  } finally {
    setLoading(false);
  }
}

Issues:

  • UI feels sluggish

  • Complex loading state management

  • Manual error handling

Optimistic Solution

const [messages, addOptimisticMessage] = useOptimistic(
  messages,
  (currentMessages, newMessage) => [
    ...currentMessages,
    { ...newMessage, sending: true }
  ]
);

async function sendMessage(newMessage) {
  addOptimisticMessage(newMessage); // Immediate UI update
  await api.sendMessage(newMessage); // Process in background
}

Benefits:

  • Instant feedback

  • Automatic rollback if API fails

  • Cleaner code

How It Works

  1. Initial State - Tracks the "real" state (server state)

  2. Optimistic Update - When addOptimistic is called:

    • Creates temporary state

    • Shows this to user immediately

  3. Background Processing

    • Async action runs

    • On success: new state becomes truth

    • On failure: reverts to last valid state

  4. Automatic Sync

    • React manages the reconciliation

Real-World Examples - Todo List

/**
 * TodoList Component
 * 
 * Demonstrates optimistic UI updates for a todo list using React's useOptimistic hook.
 * Shows immediate feedback when adding todos while handling the actual API call in the background.
 * Automatically rolls back if the API call fails.
 */
function TodoList({ todos }) {
  /**
   * useOptimistic Hook
   * 
   * Creates an optimistic version of the todos list that includes pending updates.
   * @param {Array} todos - The current "source of truth" todos from props
   * @param {function} updateFn - How to merge optimistic updates (receives current state and optimistic value)
   * @returns {Array} optimisticTodos - Current state including pending updates
   * @returns {function} addOptimisticTodo - Function to trigger optimistic updates
   */
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    /**
     * Update function for optimistic state
     * @param {Array} current - Current todos (including previous optimistic updates)
     * @param {Object} newTodo - The new todo being added optimistically
     * @returns {Array} New state with the optimistically added todo
     */
    (current, newTodo) => [...current, { ...newTodo, pending: true }]
  );

  /**
   * Handles form submission for adding a new todo
   * @param {FormData} formData - Data from the submitted form
   */
  async function addTodo(formData) {
    // Extract todo text from form data
    const todo = { text: formData.get('text') };

    // Immediately update UI optimistically
    addOptimisticTodo(todo);

    try {
      // Attempt to persist to server (may fail)
      await api.addTodo(todo);
    } catch (error) {
      // If API fails, useOptimistic will automatically roll back
      console.error('Failed to save todo:', error);
    }
  }

  return (
    <>
      {/* List of todos - shows both confirmed and optimistic todos */}
      <ul>
        {optimisticTodos.map((todo, index) => (
          /**
           * Todo Item
           * @param {Object} todo - The todo item
           * @param {string} todo.text - Todo text content
           * @param {boolean} [todo.pending] - Flag indicating unconfirmed optimistic update
           */
          <li 
            key={index} // Note: In production, use a proper ID instead of index
            className={todo.pending ? 'text-gray-400' : ''} // Visual feedback for pending state
            aria-busy={todo.pending} // Accessibility attribute for loading state
          >
            {todo.text}
            {todo.pending && (
              <span className="sr-only">(Pending save)</span> // Screen reader only text
            )}
          </li>
        ))}
      </ul>

      {/* Todo input form */}
      <form 
        action={addTodo} 
        aria-label="Add new todo item" // Accessibility label
      >
        <input 
          name="text" 
          type="text" 
          required // HTML5 validation
          aria-label="Todo text" // Accessibility label
          placeholder="What needs to be done?"
        />
        <button type="submit">Add Todo</button>
      </form>
    </>
  );
}

Advanced Patterns

1. Combining with useActionState

/**
 * CommentForm Component
 * 
 * Handles comment submission with optimistic UI updates and error handling.
 * Combines useOptimistic for instant feedback and useActionState for form handling.
 */
function CommentForm({ comments }) {
  /**
   * useOptimistic Hook
   * 
   * Creates an optimistic version of comments that includes pending submissions.
   * @param {Array} comments - Current comments from props (source of truth)
   * @param {function} updateFn - Merges optimistic updates with current state
   * @returns {Array} optimisticComments - Current state including pending comments
   * @returns {function} addOptimistic - Function to add optimistic updates
   */
  const [optimisticComments, addOptimistic] = useOptimistic(
    comments,
    /**
     * Optimistic update handler
     * @param {Array} current - Current comments array
     * @param {string} newComment - New comment text being added
     * @returns {Array} New comments array with the optimistic addition
     */
    (current, newComment) => [...current, newComment]
  );

  /**
   * useActionState Hook
   * 
   * Manages form submission state including pending status and errors.
   * @param {function} action - Async form submission handler
   * @param {null} initialState - Initial error state (null = no error)
   * @returns {Array} [
   *   error: Current error message,
   *   submitAction: Form action handler,
   *   isPending: Submission loading state
   * ]
   */
  const [error, submitAction, isPending] = useActionState(
    /**
     * Form submission handler
     * @param {any} prev - Previous state (unused in this case)
     * @param {FormData} formData - Submitted form data
     */
    async (prev, formData) => {
      const comment = formData.get('comment');

      // Immediately show the comment optimistically
      addOptimistic(comment);

      try {
        // Attempt to persist comment to server
        await postComment(comment);
      } catch (err) {
        // If API fails, useOptimistic will automatically roll back
        // and we return the error message
        return err.message;
      }
      return null; // No error on success
    },
    null // Initial error state
  );

  return (
    <>
      {/* Comments list - shows both existing and optimistic comments */}
      <div className="comments">
        {optimisticComments.map((comment, index) => (
          /**
           * Comment paragraph
           * @param {string} comment - Comment text content
           * Note: In production, use a proper key (e.g., comment ID)
           */
          <p key={index}>{comment}</p>
        ))}
      </div>

      {/* Comment submission form */}
      <form 
        action={submitAction}
        aria-label="Add comment" // Accessibility label
      >
        <input
          name="comment"
          type="text"
          required // HTML5 validation
          aria-label="Comment text" // Accessibility
          disabled={isPending} // Disable during submission
          placeholder="Write a comment..."
        />
        <button 
          type="submit" 
          disabled={isPending} // Disable during submission
          aria-busy={isPending} // Accessibility loading indicator
        >
          {isPending ? 'Posting...' : 'Post Comment'}
        </button>

        {/* Error message display (if any) */}
        {error && (
          <p className="error" role="alert">
            Error: {error}
          </p>
        )}
      </form>
    </>
  );
}

Comparison with Alternatives

FeatureuseOptimisticManual StateLibraries
BoilerplateMinimalHighModerate
RollbackAutomaticManualVaries
Loading StatesBuilt-inManualSometimes
React 19NativeCompatibleAdapter Needed

what is use Hook

React's use hook (introduced in React 19) is a groundbreaking addition that fundamentally changes how we handle asynchronous operations and context consumption in React components. This powerful hook simplifies data fetching, promise resolution, and context access with a more flexible and intuitive API.

What is the use Hook?

The use hook is a versatile tool that:

  • Consumes promises (replaces useEffect + useState patterns)

  • Reads context (alternative to useContext)

  • Works with Suspense out of the box

  • Can be called conditionally (unlike other hooks)

Basic Syntax

// With promises
const data = use(promise);

// With context
const theme = use(ThemeContext);

Why Do We Need the use Hook?

Problems with Traditional Approaches

  1. Promise Hell

     // Old way
     const [data, setData] = useState(null);
     useEffect(() => {
       fetchData().then(setData);
     }, []);
    
  2. Context Boilerplate

     // Verbose context consumption
     const theme = useContext(ThemeContext);
    
  3. Suspense Complexity

     // Manual suspense integration
     if (loading) return <Spinner />;
    

How use Solves These

  • Simpler async handling - Direct promise consumption

  • Cleaner context access - No extra hook needed

  • Built-in Suspense - Automatic loading states

  • Conditional usage - More flexible than rules-bound hooks

Core Features Explained

1. Promise Consumption

function UserProfile({ userId }) {
  const user = use(fetchUser(userId)); // Direct promise usage

  return <div>{user.name}</div>;
}

Key Benefits:

  • No manual loading state management

  • Automatic error bubbling to Error Boundary

  • Works with Suspense out of the box

2. Context Access

function ThemedButton() {
  const theme = use(ThemeContext); // Simpler than useContext

  return <button style={theme}>Click</button>;
}

Advantages:

  • Can be called conditionally

  • Same performance as useContext

  • Cleaner syntax

3. Suspense Integration

<Suspense fallback={<Spinner />}>
  <UserProfile userId={1} /> {/* Uses use() internally */}
</Suspense>

How It Works:

  • Suspends rendering while promise is pending

  • Shows fallback until data resolves

  • No manual isLoading checks needed

How use Works Internally

Execution Flow

  1. Initial Render

    • If promise exists, React checks its status

    • If pending, suspends the component

    • If resolved, returns the value immediately

  2. Re-renders

    • Reuses cached value if same promise

    • Triggers re-render if promise changes

  3. Error Handling

    • Throws to nearest Error Boundary

    • Can be caught with try/catch

Memory Management

  • Maintains a promise cache per component

  • Cleans up when component unmounts

  • Prevents memory leaks

Real-World Use Cases

1. Data Fetching

function NewsFeed() {
  const posts = use(fetchPosts()); // Auto-suspends

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

2. Conditional Context

function Tooltip({ content }) {
  // Only consumes context if needed
  const prefersReducedMotion = use(
    shouldAnimate ? PrefersReducedMotionContext : null
  );

  return prefersReducedMotion ? content : <Animated>{content}</Animated>;
}

3. Multiple Async Operations

function UserDashboard({ userId }) {
  // Parallel fetching
  const user = use(fetchUser(userId));
  const orders = use(fetchOrders(userId));

  return (
    <div>
      <h1>{user.name}</h1>
      <OrdersList data={orders} />
    </div>
  );
}

Performance Considerations

  1. Promise Caching

     // Good - memoized promise
     const promise = useMemo(() => fetchData(id), [id]);
     const data = use(promise);
    
     // Bad - new promise each render
     const data = use(fetchData(id)); // Recreates promise
    
  2. Suspense Boundaries

    • Place at meaningful UI sections

    • Avoid too granular boundaries

  3. Context Optimization

    • Works like useContext - triggers re-renders on changes

Comparison with Alternatives

Featureuse HookuseEffectuseContextLibraries
Async DataBuilt-inManualNoYes
ContextYesNoYesNo
SuspenseNativeManualNoSome
ConditionalYesNoNoNo

Migration Guide

From useEffect

- const [data, setData] = useState(null);
- useEffect(() => {
-   fetchData().then(setData);
- }, []);

+ const data = use(fetchData());

From useContext

- const theme = useContext(ThemeContext);

+ const theme = use(ThemeContext);

Advanced Patterns

  1. Error Handling
function SafeComponent() {
  try {
    const data = use(unstablePromise);
    return <DataView data={data} />;
  } catch (error) {
    return <ErrorView error={error} />;
  }
}
  1. Race Condition Prevention
function UserProfile({ userId }) {
  // Memoize promise to prevent recreating
  const userPromise = useMemo(() => fetchUser(userId), [userId]);
  const user = use(userPromise);

  return <Profile user={user} />;
}

And finally, that's pretty much it. We will learn more hooks as we explore Redux and Next.js, but that's for later.

Just take a deep breath. One hook at a time.

Please share your thoughts.

0
Subscribe to my newsletter

Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vaishnavi Dwivedi
Vaishnavi Dwivedi