An introduction to React hooks for beginners — Part 3

Mayank C
Tech Tonic

--

This article series is a crash course on the 15 built-in React hooks for beginners.

Introduction

React Hooks were introduced in version 16.8, providing a new approach to managing state and side effects within functional components. They eliminate the need for class components in many cases, simplifying code structure and promoting a better organization.

This article series presents a beginner-friendly introduction to React Hooks, exploring their key concepts and common functionalities. Overall, we’ll look into the following 15 hooks (listed alphabetically):

  • useCallback: Memoizes callback functions to prevent unnecessary re-renders.
  • useContext: Accesses context values established with React.createContext.
  • useDebugValue: Logs values in React DevTools for debugging purposes.
  • useDeferredValue: Suspends rendering until a value is resolved.
  • useEffect: Performs side effects, such as data fetching, subscriptions, and DOM manipulation.
  • useId: Generates unique IDs within a component.
  • useImperativeHandle: Exposes imperative methods from a custom hook.
  • useInsertionEffect: Performs effects when a DOM node is inserted into the document.
  • useLayoutEffect: Similar to useEffect, but executes effects after DOM mutations are committed.
  • useMemo: Memoizes the results of expensive computations.
  • useReducer: Implements complex state logic with a reducer function.
  • useRef: Creates mutable references to DOM elements or values.
  • useState: Manages component state variables and update functions.
  • useSyncExternalStore: Connects component state to an external store.
  • useTransition: Manages animations and concurrent rendering.

In this last part, we’ll be looking at hooks 11 to 15: useReducer, useRef, useState, useSyncExternalStore, and useTransition.

  • The Part 1 of this series has covered hooks 1 to 5.
  • The Part 2 of this series has covered hooks 6 to 10.

Let’s get started.

11 useReducer

The useReducer hook, introduced in React 16.8, offers an alternative mechanism for managing state in functional components. Unlike useState, it accepts a reducer function as input, allowing for more complex state logic and centralized state management. This promotes better organization and scalability in applications with intricate state requirements.

Functionality

Invocation: You invoke useReducer with three arguments:

  • Reducer Function: This function takes the current state and an action object as arguments, returning the updated state based on the action type and payload.
  • Initial State: The initial state object representing the starting point for your application’s state.
  • Initializer Function (Optional): This function receives the initial state and returns a modified state value, allowing for potential side effects or asynchronous operations during initialization.

State and Dispatch: The hook returns an array containing two values:

  • Current State: The current state object accessible throughout the component.
  • Dispatch Function: This function accepts action objects, triggering the reducer function to update the state based on the provided action.

Benefits

  • Complex State Management: Suitable for managing intricate state logic beyond simple values, especially in larger applications.
  • Centralized State Updates: Enables centralized state management through the reducer function, promoting better organization and maintainability.
  • Flexibility: Allows for customizability in state updates based on your specific requirements.

Considerations

  • Learning Curve: Requires understanding reducer functions and action creators, which might have a steeper learning curve compared to useState.
  • Overkill for Simple Cases: For simple state management needs, useState might be a more straightforward choice.
  • Debugging Complexity: Debugging complex state logic involving reducers can be more challenging than debugging individual state variables.

Code Sample

Basic usage

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

const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
};

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>
);
}

Shopping cart with multiple items and actions

const initialState = {
items: [],
};

const reducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
return {
items: [...state.items, action.payload],
};
case 'REMOVE_ITEM':
return {
items: state.items.filter((item) => item.id !== action.payload.id),
};
case 'UPDATE_QUANTITY':
return {
items: state.items.map((item) =>
item.id === action.payload.id ? action.payload : item
),
};
default:
return state;
}
};

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

const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};

const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: { id: itemId } });
};

const updateQuantity = (itemId, newQuantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id: itemId, quantity: newQuantity } });
};

return (
<div>
<h2>Shopping Cart</h2>
<ul>
{state.items.map((item) => (
<li key={item.id}>
{item.name} (Quantity: {item.quantity})
<button onClick={() => removeItem(item.id)}>Remove</button>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
/>
</li>
))}
</ul>
<button onClick={() => addItem({ name: 'Product A', quantity: 1 })}>
Add Product A
</button>
</div>
);
}

12 useRef

The useRef hook, introduced in React 16.3, offers a mechanism to persist values between component renders. Unlike state, which triggers re-renders when updated, values stored in useRef don't cause re-renders, making them suitable for scenarios where you need mutable data outside the React state management cycle.

Functionality

Invocation:

You invoke useRef with an optional initial value, creating a ref object with a current property. This property holds the persisted value.

Usage:

  • Assign a value to the ref.current property to store it.
  • Access the stored value using ref.current.

Benefits

  • Persisting DOM Nodes: Useful for accessing or manipulating DOM elements directly, bypassing React’s virtual DOM diffing algorithm.
  • Storing Mutable Data: Suitable for holding values that you don’t want to trigger re-renders, like timers, animation references, or form references.
  • Integration with External Libraries: Often used when interacting with third-party libraries that expect DOM node or mutable value references.

Considerations

  • Not for State Management: Not intended for managing UI-related data, as changing ref.current doesn't directly update the component's state or cause re-renders.
  • Careful Cleanup: Remember to clean up resources attached to the ref using the useEffect hook's cleanup function to avoid memory leaks or unintended side effects.
  • Limited Scope: The stored value persists only within the component where the ref is defined.

Code Samples

Basic usage

function MyComponent() {
const inputRef = useRef(null); // Initial value is null

const handleClick = () => {
if (inputRef.current) {
inputRef.current.focus(); // Access and manipulate DOM element
}
};

return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}

Measuring a component’s width and height after it renders

function MyComponent() {
const elementRef = useRef(null);

useEffect(() => {
if (elementRef.current) {
const { width, height } = elementRef.current.getBoundingClientRect();
console.log("Element dimensions:", width, height);
}
}, []); // Run only once after initial render

return <div ref={elementRef}>Content</div>;
}

13 useState

This is the most important hook in the React world!

The useState hook, introduced in React 16.8, serves as the fundamental mechanism for managing state within functional components. State refers to data that drives a component's rendering and can change over time, triggering re-renders to reflect these changes. useState offers a simple and declarative way to create and update state variables within functional components.

Functionality

Invocation:

You invoke useState with an initial value as its argument. This value represents the starting point for your state variable.

Return Value:

useState returns an array containing two elements:

  • Current State: The current value of the state variable, accessible throughout the component using the first element of the array.
  • State Setter Function: A function that allows you to update the state variable. Calling this function with a new value triggers a re-render of the component with the updated state.

Benefits

  • Simplicity: Provides a straightforward way to manage state in functional components, promoting cleaner and more readable code.
  • Reactivity: Ensures UI updates based on state changes, keeping the rendered output consistent with the current state.
  • Declarative Style: Encourages a declarative approach to describing component behavior based on state, enhancing maintainability.

Considerations

  • Overuse: Avoid unnecessary state management, as excessive state variables can impact performance and complexity.
  • Immutability: When updating state, consider using immutable updates (e.g., spreading existing state) to avoid unintended side effects.
  • State Lifecycles: For complex state logic or shared state across components, consider using context or state management libraries like Redux.

Code Samples

Classic button counter problem

// Simulating a counter component:

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

const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);

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

Todo list

function TodoList() {
const [todos, setTodos] = useState([]); // Initial state is an empty array

const addTodo = (newTodo) => {
setTodos([...todos, newTodo]); // Create a new array with the added item
};

const toggleTodo = (index) => {
setTodos((prevTodos) =>
prevTodos.map((todo, i) =>
i === index ? { ...todo, completed: !todo.completed } : todo
)
); // Update completion state using an immutable approach
};

const removeTodo = (index) => {
setTodos((prevTodos) => prevTodos.filter((todo, i) => i !== index));
};

// ... Rest of the component logic for rendering the todo list ...
}

14 useSyncExternalStore

The useSyncExternalStore hook, introduced in React 18, serves a specific purpose within the context of synchronizing state or values from external stores with React components. It offers a mechanism for subscribing to these external sources and ensuring the React component's state stays up-to-date whenever the external value changes. This is primarily used in React development with CSS-in-JS libraries for tasks like managing global styles or injecting SVG elements based on external data.

Functionality

Invocation: You invoke useSyncExternalStore with three arguments:

  • Subscribe Function: This function takes the current state of the external store and returns a function that unsubscribes from the store.
  • GetSnapshot Function: This function reads the current value from the external store and returns a snapshot representing the relevant data for the component.
  • GetServerSnapshot Function (Optional): This function is used for server-side rendering and returns a snapshot that can be used during initial hydration.

Behavior

  • useSyncExternalStore subscribes to the external store using the provided subscribe function.
  • Whenever the external value changes, the subscribed function is called, triggering a re-render of the component.
  • The getSnapshot function is used to extract the relevant data from the external store during each render.

Benefits

  • Synchronization: Ensures React components stay in sync with external state or values, improving reactivity and consistency.
  • Performance Optimization: Optimizes re-renders by only updating the component when the relevant data in the external store changes.
  • Library-Specific Integration: Primarily used within CSS-in-JS libraries for managing their internal state and interactions with external data sources.

Considerations

  • Limited Scope: Primarily used for internal library logic, not general React development scenarios.
  • Understanding Required: Requires understanding of the specific library’s usage and how it interacts with external stores.

Code Sample

Simulating Global Theme State

// Conceptual code, not actual library syntax
function MyComponent() {
const theme = useSyncExternalStore(
// Subscribe to theme changes
() => getTheme().subscribe((newTheme) => console.log("Theme changed!")),
// Extract current theme data
() => getTheme().colors.primary,
// No server-side rendering (optional)
);

const styles = {
color: theme.colors.primary,
};

return (
<div style={styles}>
{/* Content styled based on theme colors */}
</div>
);
}

Simulating Global Data from API

// Conceptual code, not actual library syntax
function MyComponent() {
const data = useSyncExternalStore(
// Subscribe to data updates
() => getData().subscribe((newData) => console.log("Data updated!")),
// Extract relevant data for rendering
() => getData().items.slice(0, 3),
// No server-side rendering (optional)
);

// ... Rest of the component logic to render the data ...

return (
<div>
{/* Render the first 3 items from the data */}
</div>
);
}

15 useTransition

Introduced in React 18, the useTransition hook offers a mechanism to manage the priority and rendering of state updates, particularly those that might be computationally expensive or cause temporary performance hiccups. It allows you to mark certain state updates as transitions, delaying their rendering until after critical updates and user interactions have processed. This helps maintain a smooth and responsive user experience while complex updates run in the background.

Functionality

Invocation: You invoke useTransition with no arguments. It returns an array containing two values:

  • isPending: A boolean indicating whether a transition is currently ongoing (true) or not (false).
  • startTransition: A function that accepts a callback containing the state update logic. This callback executes inside the transition, meaning its effects wouldn’t be immediately reflected in the UI.

Behavior

  • When you call startTransition, the provided callback executes, updating the state as usual.
  • However, the component doesn’t re-render immediately. React postpones this re-render until after processing critical updates and user interactions, prioritizing a smooth user experience.
  • Once these critical tasks are complete, React re-renders the component with the updated state from the transitioned update.

Benefits

  • Improved User Experience: Prevents UI stutters or freezes during long-running state updates, enhancing perceived performance.
  • Prioritization: Ensures critical updates and user interactions remain responsive even during complex background operations.
  • Fine-Grained Control: Allows you to selectively mark only necessary updates as transitions, avoiding unnecessary delays.

Considerations

  • Overuse: Don’t overuse transitions for minor updates, as they can still add complexity and overhead.
  • Clarity: Ensure the transition callback represents a logically separate update to maintain code readability.
  • Fallback UI: Consider providing a fallback UI while the transition is ongoing to improve user perception.

Code Samples

Basic usage

function MyComponent() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);

const fetchData = async () => {
setIsLoading(true); // Indicate loading state
const newData = await fetchSomeData(); // Simulate long-running operation
setData(newData); // Transitioned state update
setIsLoading(false); // Remove loading state
};

const { isPending } = useTransition();

// ... Rest of the component logic ...

return (
<div>
{isLoading && <p>Loading...</p>} // Loading indicator while transition is pending
{!isLoading && !isPending && <p>Data: {JSON.stringify(data)}</p>} // Render data if not loading or transitioning
{!isLoading && isPending && <p>Data updating...</p>} // Temporary message while transition is ongoing
<button onClick={() => startTransition(() => fetchData())}>Fetch Data</button>
</div>
);
}

Animated search bar with debounced filtering

function SearchBar() {
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState([]);
const { isPending } = useTransition();

const handleSearchChange = (newTerm) => {
setSearchTerm(newTerm);
startTransition(() => {
// Debounce search logic (simulated delay)
setTimeout(() => {
setSearchResults(filterData(newTerm)); // Triggered in the transition
}, 200);
});
};

// ... Rest of the component logic for rendering search bar and results ...

return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
/>
{isPending && <p>Searching...</p>}
{!isPending && searchResults.length > 0 && (
<ul>
{searchResults.map((result) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
)}
{!isPending && searchResults.length === 0 && <p>No results found.</p>}
</div>
);
}

That’s all about the last five hooks. We’ve covered all 15 hooks. I hope this article has been of help to you.

  • The Part 1 of this series has covered hooks 1 to 5.
  • The Part 2 of this series has covered hooks 6 to 10.

--

--