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
useState
– Manages state within a functional component.useEffect
– Handles side effects like fetching data or subscriptions.useContext
– Accesses values from aContext
without prop drilling.
Additional Hooks
useReducer
– Manages state using a reducer function (likeRedux
).useCallback
– Memoizes a function to prevent unnecessary renders.useMemo
– Memoizes a value to avoid recalculating it on every render.useRef
– Creates a reference to a DOM element or value that persists between renders.useLayoutEffect
– Similar touseEffect
but fires synchronously after DOM updates.useImperativeHandle
– Customizes the instance value exposed when usingref
.
Performance and State Synchronization Hooks
useTransition
– Defers state updates to make UI updates more responsive.useDeferredValue
– Defers a value update until the component is idle.useSyncExternalStore
– Subscribes to an external state management library.useId
– Generates a stable unique ID for accessibility or keying purposes.useInsertionEffect
– Runs synchronously before DOM mutations; used for CSS-in-JS libraries.
New Hooks from React 19
useFormStatus
– Provides real-time information about form submission state.useActionState
– Simplifies managing asynchronous actions and state transitions.useOptimistic
– Facilitates optimistic UI updates by assuming success before confirmation.use
– Handles asynchronous operations and promises directly within the component.
React Router Hooks
useNavigate
– Navigates programmatically.useLocation
– Accesses the current URL location.useParams
– Retrieves URL parameters.useSearchParams
– Accesses query parameters.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?
No Re-rendering: When you click the button,
count
changes in memory, but React doesn't know it needs to update the UI.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
Triggers Re-renders
When you call
setCount
, React knows the component needs to re-renderThe UI automatically updates to show the new value
Persists Between Renders
React preserves the state value even when the component function runs again
Unlike regular variables that reset each render
Batch Updates (we will learn more below)
React optimizes multiple state updates for performance
Regular variables would force immediate updates (less efficient)
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 :
useState(0)
Initializes the state with
0
as the default value.Returns an array:
[currentState, updaterFunction]
.
count
Holds the current value of the state (initially
0
).Automatically reflects the latest value after each re-render.
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.
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?
Expected Behavior (Without Batching):
You might expect the count to increase by 3 (once for eachsetCount
call)Actual Behavior (With Batching):
The count only increases by 1 because:All three updates use the same
count
value (0) from the current renderReact batches them together and only processes the last one
Why React Batches Updates
Performance Optimization:
Avoids unnecessary intermediate renders
Reduces DOM operations
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
Batching is Automatic: React batches synchronous state updates by default
Functional Updates Help: Use
setCount(prev => prev + 1)
when updates depend on previous statePerformance Benefit: Batching reduces unnecessary renders
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 toNaN
(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
Empty array:
[]
- Runs once on mountAll dependencies:
[prop1, state1, contextValue]
- Runs when any changeFunction dependencies:
[onClick]
- Be careful with inline functionsObject dependencies:
[user]
- Will re-run ifuser
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 thesetInterval
captures the initial value ofcount
(which is0
) and never updates it.As a result, it always logs
0
andsetCount(count + 1)
always sets the value to1
.
Why this happens:
The effect closure captures the
count
value from the initial renderSubsequent interval callbacks see that same initial value
The dependency array tells React the effect doesn't depend on
count
Solutions
Add dependencies (simplest solution):
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(interval); }, [count]); // Now updates correctly
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
Before re-running the effect (when dependencies change)
When the component unmounts
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 thatsetData(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
Debouncing:
useEffect(() => { const timer = setTimeout(() => { // Your effect }, 300); return () => clearTimeout(timer); }, [input]);
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]);
Async Function Directly Inside useEffect
(Not Recommended)
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
Feature | useEffect | useLayoutEffect |
Execution Timing | Runs asynchronously after the browser paints | Runs synchronously before the browser paints |
Use Case | Best 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 Behavior | Non-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 updatingwidth
, 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
Debounce heavy operations (e.g., batch DOM updates).
Avoid unnecessary dependencies (only re-run when truly needed).
Combine multiple DOM reads/writes to prevent layout thrashing.
Here is a simple project to help you understand the hooks mentioned above properly:
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
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, andfocus()
is called directly on the element when the component mounts.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.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.
Value Persists Between Re-renders:
Unlike local component state (
useState
), updating a ref’scurrent
property does not trigger a component re-render.This makes refs useful for storing values like previous state, timers, and instance variables.
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.
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
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} />; }
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>; }
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;
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.
Feature | useRef | useState |
Triggers Re-render | No | Yes |
Mutable Value | Yes (ref.current ) | Immutable (use setter) |
Use Case | DOM access, mutable values | State that affects UI |
Persistence | Across re-renders | But triggers updates |
Initialization | useRef(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
Reusable Component Libraries
Higher-Order Components (HOCs)
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
Avoid Overusing Refs - Most DOM manipulation should be declarative
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 refcreateHandle
: Function returning the object to exposedependencies
: 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
Security: Prevents full DOM access by parent components
Abstraction: Exposes only necessary methods
Consistency: Maintains stable APIs for component libraries
Comparison to Direct Refs
Approach | Parent Access | Child Control | Use Case |
Direct DOM Ref | Full DOM access | None | Simple DOM manipulation |
useImperativeHandle | Only exposed methods | Full control | Component libraries, controlled APIs |
How It Works
React creates an imperative handle object during render
Attaches it to the Fiber node's
ref
propertyParent'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
Operation | Plain Ref | useImperativeHandle |
Mount Time | 1.2ms | 1.5ms (+25%) |
Update Time | 0.3ms | 0.4ms (+33%) |
Memory Overhead | 0 | ~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:
Unnecessary Intermediate Components: Components between the source and destination must accept and forward props they don't actually use.
Reduced Maintainability: Changing the data structure requires modifying all components in the chain.
Tight Coupling: Components become dependent on their position in the hierarchy.
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
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)
useContext
subscribes to the nearest matchingProvider
above it in the component treeWhen the
Provider
's value changes, all components using that context withuseContext
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:
No More Prop Drilling – Pass data directly where it’s needed without passing it through parent components.
Cleaner Code – Less boilerplate and fewer props make code more readable.
Centralized State – Context acts like a lightweight state management solution for local state.
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:
Current state – The existing state value.
Action – An object that describes the change to be made.
useReducer
Syntax
const [state, dispatch] = useReducer(reducer, initialState);
Parameters:
reducer
– A function that takes(state, action)
and returns the new state.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
The
dispatch
function sends an action object to thereducer
function.The
reducer
function takes the current state and the action and calculates the new state.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?
The
useReducer
hook initializesstate
with{ count: 0 }
.Clicking the
+
button triggersdispatch({ type: "INCREMENT" })
, calling thereducer
function.The
reducer
updatesstate
by increasingcount
.Clicking the
-
button triggersdispatch({ type: "DECREMENT" })
, decreasingcount
.
When to Use useReducer
Instead of useState
?
Scenario | useState | useReducer |
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
- Mutating state directly:
state.push(newItem); // Don’t mutate state
- Use
spread
to create a new object or array:
return [...state, newItem];
- 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:
State Management (
useState
) - Stores the theme (light
ordark
).Loading Data on Mount (
useEffect
) - Retrieves the theme fromlocalStorage
when the component first loads.Saving Data on Theme Change (
useEffect
) - UpdateslocalStorage
whenever the theme state changes.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:
State Management (
useState
) - Stores the draft text typed by the user.Loading Data on Mount (
useEffect
) - Retrieves the saved draft fromsessionStorage
when the component first loads.Auto-Saving Draft (
useEffect
) - UpdatessessionStorage
whenever the draft changes.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?
Authentication tokens (JWT, session IDs)
Tracking user behavior (analytics, GDPR-compliant)
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:
State Management (
useState
) - Stores the user's cookie consent status (true
orfalse
).Checking Existing Cookies (
useEffect
) - When the component mounts, it checks for a saved cookie and updates the state.Handling Consent (
handleConsent
) - When the user clicks "Accept" or "Decline", the choice is:Saved in cookies using
js-cookie
Stored for 1 year using
{ expires: 365 }
Updated in the component state for immediate effect.
Conditional Rendering:
If the user accepts cookies, a thank-you message is shown.
If they haven’t given consent, the cookie banner is displayed.
Cookie Behavior
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?
Offline-first apps (Progressive Web Apps)
Complex client-side data (e.g., cached API responses)
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 :
State Management (
useState
) - Stores notes and new note input.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.
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.
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
orsessionStorage
, it can store more data.Useful for offline apps.
Comparison Table: When to Use What?
Storage Method | Persistence | Capacity | Best Use Case | Avoid When |
Local Storage | Forever (until cleared) | ~5MB | User preferences, caching | Sensitive data, large datasets |
Session Storage | Tab session | ~5MB | Form data, per-tab state | Cross-session storage |
Cookies | Configurable (session/expiry) | ~4KB | Auth tokens, server-needed data | Large data, client-only needs |
IndexedDB | Forever (until cleared) | ~50MB+ | Offline apps, large datasets | Simple state, beginners |
React State/Context | In-memory (resets on refresh) | N/A | UI state, real-time updates | Persistent 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
anduseParams
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:
<Router>
The root component that enables client-side routing.
BrowserRouter
(aliased asRouter
) uses HTML5 history API for navigation.
<Routes>
Replaces
<Switch>
from v5.Automatically picks the best matching route (more efficient than
Switch
).
<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.
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/123
→id = "123"
/user/john
→id = "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:
/settings
→tab = undefined
/settings/profile
→tab = "profile"
Best Practices
Use Descriptive Parameter Names
- Prefer
/:userId
over/:id
for clarity.
- Prefer
Handle Missing/Invalid Parameters
Check if the parameter exists before using it:
const { id } = useParams(); if (!id) return <p>User not found!</p>;
Combine with
useEffect
for Data FetchingAlways re-fetch data when the parameter changes:
useEffect(() => { fetchUser(id); }, [id]);
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 ofto="/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:
Authentication Check
Uses
isAuthenticated
prop to determine accessTypically receives
user
object from context/store
Redirection Logic
replace
prevents login page from being in navigation historystate
preserves original destination for post-login redirect
Route Protection
Wraps around any sensitive routes
Can be reused for multiple protected routes
Works with nested routes (children inherit protection)
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:
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
Suspense Behavior
<Suspense fallback={<LoadingSpinner />}> {/* Routes that might trigger lazy loading */} </Suspense>
Shows fallback during component fetch
Prevents layout shifts (CLS metric)
Production Build Impact
Webpack creates separate files like:
home.chunk.js
about.chunk.js
Loaded automatically when routes are visited
Performance Comparison:
Metric | Standard Import | Lazy Loading |
Initial JS | 500KB | 300KB |
Home Page Load | 500KB | 310KB |
About Page Load | 500KB | 320KB |
TTI | 2.5s | 1.8s |
Summary
Feature | Purpose | Implementation |
Nested Routes | Hierarchical layouts | <Outlet /> in parent |
Redirects | Navigate programmatically | <Navigate to="..." /> |
Protected Routes | Restrict access | Conditional <Navigate /> |
Catch-All Route | Handle 404s | <Route path="*" element={...} /> |
Lazy Loading | Optimize performance | React.lazy + Suspense |
Lets understand hooks related to router -
Here are few hooks used by react-router-dom
useNavigate
– Programmatically navigate between routes.useLocation
– Access the current URL location.useParams
– Retrieve dynamic URL parameters.useSearchParams
– Access and manipulate query parameters.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 vianavigate
.
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
withuserId
).
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 likeURLSearchParams
(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:
Hook | Purpose | Example Use Case |
useNavigate | Programmatic navigation | Redirect after login |
useLocation | Access current URL | Analytics tracking |
useParams | Extract URL parameters | Dynamic user profiles |
useSearchParams | Read/update query params | Search filters |
useMatch | Check URL match | Active 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
When a parent component re-renders, all child components re-render by default.
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.HowReact.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
?
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]);
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
?
Preventing Unnecessary Re-renders in Child Components
If a parent component passes a callback to a child wrapped inReact.memo
,useCallback
ensures the child doesn’t re-render unnecessarily.const handleClick = useCallback(() => { console.log('Button clicked!'); }, []); return <MemoizedButton onClick={handleClick} />;
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]);
Dependencies in Effects
If a function is used insideuseEffect
,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
- 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
}, []);
- 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
- 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.
- 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
Scenario | Improvement |
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
Scenario | Performance Cost |
Overusing on simple components | Slower initial render |
Memoizing tiny functions | Memory overhead |
Unnecessary dependency tracking | Complex code |
Rule: Measure first! Use React DevTools’ profiler to identify bottlenecks.
Key Takeaways
useMemo
→ Cache values (objects, arrays, computations).useCallback
→ Cache functions (event handlers, callbacks).Always include dependencies to avoid stale closures.
Avoid premature optimization—profile before memoizing.
Combine with
React.memo
for maximum re-render prevention.
Key Differences Between useMemo
and useCallback
Feature | useMemo | useCallback |
Purpose | Memoizes a computed value | Memoizes a function |
Returns | A value (number, array, object) | A function |
Use Case | Optimizing expensive calculations | Preventing unnecessary re-renders in child components |
Example | const 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
Feature | Explanation |
Stable across re-renders | Same ID on every render. |
SSR-compatible | Matches server/client IDs. |
Prefix-based | Uses : to avoid conflicts with manual IDs. |
Works with concurrent rendering | Safe 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
Method | SSR-Safe | Stable | Concurrent-Mode Safe |
useId | Yes | Yes | Yes |
Math.random() | No | No | No |
uuid /nanoid | No | Yes | No |
Manual counter (let id=0 ) | No | No | No |
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
Hook | Timing | Use Case |
useInsertionEffect | Before DOM mutations | Injecting styles |
useLayoutEffect | After DOM mutations, before paint | Measuring layout |
useEffect | After render & paint | Side effects |
Execution Order
useInsertionEffect
→ Style injection.DOM updates → React applies changes.
useLayoutEffect
→ Layout measurements.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
Purpose: Optimizes CSS-in-JS style injection.
Timing: Runs before DOM mutations (earlier than
useLayoutEffect
).Best for: Libraries like styled-components, not app code.
Cleanup: Always remove injected styles.
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:
Re-render the entire component
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:
Urgent Updates → High-priority updates like typing, clicking, or pressing a button.
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 thesetQuery
state update as non-blocking.While the update is in progress, the UI remains interactive.
isPending
istrue
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:
Immediate state updates (like clicks) are processed with high priority.
Transition updates (like filtering) are processed with low priority.
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:
Feature | useTransition | useDeferredValue |
Type | Function that marks state updates as transitions | Value that React defers updating |
Use Case | For handling expensive state updates | For deferring low-priority values |
Example | Filtering a large list | Passing props to a child component |
UI State | Shows pending state | Keeps 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
When you pass a
value
touseDeferredValue
, React creates a deferred copy of the value.React will try to keep the deferred value in sync with the original value.
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 forsetTimeout()
.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:
Feature | useDeferredValue | useTransition |
Type | Hook to delay value updates | Hook to delay state updates |
Scope | Works at the value level | Works at the state level |
Purpose | Used to defer state propagation | Used to mark a state update as low-priority |
Use Case | Filtering, sorting, expensive rendering | Data fetching, state updates, complex state changes |
Priority Handling | Keeps UI responsive while updating deferred values | Prioritizes 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 viaforwardRef
createHandle
: A function that returns an object with the properties and methods you want to exposedeps
(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:
Breaking Encapsulation: Parent components can access and modify anything in the child component
Implementation Coupling: Parents depend on child implementation details
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):
Server Rendering: The restaurant prepares your pizza and delivers it fully cooked (static HTML)
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
Server: Renders components to HTML (just the visual structure)
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
Faster initial load: Users see content immediately (from server HTML)
Better SEO: Search engines can crawl the static HTML
Smoother experience: Interactivity loads after the initial render
Common Hydration Problems
Mismatch Errors:
Warning: Text content did not match. Server: "Hello" Client: "Hi"
Happens when server and client render different content
Missing Hydration:
Elements are visible but don't respond to clicks
Interactive features don't work
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
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()); }, []);
Use hydration-friendly libraries:
// Instead of direct DOM manipulation useEffect(() => { // Client-side only code }, []);
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 changesgetSnapshot
: Function that returns the current value of the storegetServerSnapshot
(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
Tearing: Ensures all components see the same store version during a render
Hydration: Provides
getServerSnapshot
to match server and clientSubscriptions: Automatically handles cleanup
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:
useCustomStore Hook:
Documents the purpose and parameters of the custom hook
Explains the use of
useSyncExternalStore
with its two required parameters
createStore Function:
Documents the factory function that creates stores
Explains the internal state management mechanism
Describes each returned method (subscribe, getSnapshot, setState)
Store Implementation Details:
Explains the
listeners
Set that tracks subscriptionsDocuments the subscription/unsubscription mechanism
Shows how state updates trigger listener notifications
Counter Component:
Shows how components consume the store
Documents the state access pattern
Explains the state update mechanism via setState
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
: AFormData
object containing the submitted form values.action
: The form’saction
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:
Boilerplate Code – Needed
useState
forloading
,error
, andsuccess
states.Race Conditions – Multiple submissions could cause inconsistent UI updates.
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 :
Hook Import: Clearly states what
useFormStatus
is and its purposeSubmitButton Component:
Explains the component's role as a smart submission button
Documents the
pending
state behaviorIncludes accessibility attributes with explanations
Button Implementation:
Explains the conditional rendering logic
Mentions duplicate submission prevention
Notes the accessibility improvements
MyForm Component:
Describes the form's basic structure
Explains the
action
attribute's purposeDocuments the input field requirements
Accessibility Notes:
Includes ARIA attributes for better screen reader support
Explains the importance of the
name
attribute for form data
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
Listens to Form Events – Automatically hooks into the parent
<form>
’ssubmit
event.Tracks Pending State – Sets
pending: true
when submission starts,false
on completion.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
Feature | useFormStatus | useState | Formik/React Hook Form |
Boilerplate | Minimal | High | Moderate |
Pending State | Built-in | Manual | Manual |
Error Handling | Basic | Manual | Advanced |
Performance | Optimized | Manual | Optimized |
Server Actions | Yes | No | No |
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:
Submission state tracking (pending, success, error)
Automatic state updates after action completion
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 submissionReturns:
state
: Current result (data or error)action
: Wrapped function to triggerpending
: Boolean indicating if action is in progress
Why Do We Need useActionState
?
The Problem Without It - Traditional approaches require:
Manual
useState
for loading/error statesuseEffect
for side effectsComplex error handling
Boilerplate for optimistic updates
How useActionState
Solves This
Automatic Pending State – No manual
isLoading
trackingBuilt-in Error Handling – Catches errors and updates state
Simpler Optimistic UI – Easy rollback on failure
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:
Hook Import:
Clearly explains what
useActionState
is used forMentions its role in managing async actions
submitForm Function:
Documents parameters with JSDoc
Explains the mock API simulation
Describes the return value structure
useActionState Initialization:
Details each parameter's purpose
Explains the return tuple structure
Notes the initial state value
Form Component:
Explains the form's action binding
Documents automatic FormData handling
Input Field:
Notes the required
name
attributeIncludes accessibility and validation attributes
Submit Button:
Explains the dynamic disabled state
Documents the loading state text change
Includes ARIA attributes for accessibility
Success Message:
Uses
aria-live
for screen reader announcementsOnly renders when submission succeeds
Additional Best Practices Included:
Accessibility:
aria-busy
for loading statesaria-live
for dynamic updatesProper labeling for form inputs
Validation:
HTML5
required
attributeProper input
type="email"
Type Safety:
JSDoc for function parameters
Clear state structure documentation
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:
Pending State Activation
Immediately sets
pending = true
Marks the action as "in progress" in React's scheduler
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:
FormData Handling
Automatically collects form inputs into
FormData
Converts to a usable object via
Object.fromEntries()
Error Boundaries
If the async function throws:
try { await fn(); } catch (e) { /* Preserves last valid state */ }
Suspense Integration
- Can suspend rendering while waiting (compatible with React Suspense)
3. State Update Phase
Success Case:
Before | During | After |
state = null | pending = true | state = {success: true} |
pending = false | state = null | pending = false |
Error Case:
Before | During | After |
state = {data: 1} | pending = true | state = {data: 1} (unchanged) |
pending = false | Error occurs | pending = 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
Non-Urgent Updates
Actions are marked as "transition" updates
Allows high-priority UI interactions (e.g., typing) to interrupt
Batched Updates
Combines multiple state changes into a single re-render
Prevents intermediate "janky" states
Performance Benefits
Traditional Approach | useActionState |
Manual useState updates | Atomic state transitions |
Possible duplicate renders | Single render per lifecycle |
Race condition risks | Queue management by React |
Real-World Example Flow
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)
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
Predictable State - No stale data or intermediate "half-loaded" states
Built-in Resilience - Automatic rollback on failures maintains UI consistency
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
Feature | useActionState | useState + useEffect | Redux Thunk |
Boilerplate | Minimal | High | High |
Pending State | Built-in | Manual | Manual |
Error Handling | Automatic | Manual | Manual |
Optimistic UI | Easy | Complex | Moderate |
Server Actions | Native | No | No |
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
state
: The current "source of truth" stateUpdate function: How to merge optimistic updates
Returns
optimisticState
: The state including pending updatesaddOptimistic
: 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
Initial State - Tracks the "real" state (server state)
Optimistic Update - When
addOptimistic
is called:Creates temporary state
Shows this to user immediately
Background Processing
Async action runs
On success: new state becomes truth
On failure: reverts to last valid state
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
Feature | useOptimistic | Manual State | Libraries |
Boilerplate | Minimal | High | Moderate |
Rollback | Automatic | Manual | Varies |
Loading States | Built-in | Manual | Sometimes |
React 19 | Native | Compatible | Adapter 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
Promise Hell
// Old way const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []);
Context Boilerplate
// Verbose context consumption const theme = useContext(ThemeContext);
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
Initial Render
If promise exists, React checks its status
If pending, suspends the component
If resolved, returns the value immediately
Re-renders
Reuses cached value if same promise
Triggers re-render if promise changes
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
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
Suspense Boundaries
Place at meaningful UI sections
Avoid too granular boundaries
Context Optimization
- Works like
useContext
- triggers re-renders on changes
- Works like
Comparison with Alternatives
Feature | use Hook | useEffect | useContext | Libraries |
Async Data | Built-in | Manual | No | Yes |
Context | Yes | No | Yes | No |
Suspense | Native | Manual | No | Some |
Conditional | Yes | No | No | No |
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
- Error Handling
function SafeComponent() {
try {
const data = use(unstablePromise);
return <DataView data={data} />;
} catch (error) {
return <ErrorView error={error} />;
}
}
- 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.
Subscribe to my newsletter
Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by