React 18 has supercharged the way we build functional components. Hooks are a game-changer, letting us write cleaner, more reusable, and easier-to-understand React components. In this blog, we'll dive into 15 essential hooks, from the basics to advanced techniques, and see how they can elevate your React projects.
1. useState
The useState
hook allows you to add state variables to functional components. State represents dynamic data, which can change over time based on user interactions or other factors.
Use Case: Managing simple state like form inputs, counters, toggles, etc.
Example: Counter
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initial count is set to 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Here, clicking the "Increment" button updates count
, and useState
re-renders the component whenever count
changes.
2. useEffect
useEffect
lets you perform side effects in functional components, like data fetching, setting up subscriptions, or manually modifying the DOM. By default, useEffect
runs after every render, but it can be configured to run conditionally.
Use Case: Fetching data on component mount, setting up event listeners, or updating the document title.
Example: Fetching Data
import React, { useEffect, useState } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(setData);
}, []); // Empty dependency array: runs once on mount
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
Here, the useEffect
runs once after the component mounts (thanks to the empty dependency array), fetching data and storing it in data
.
3. useContext
useContext
lets you access global data (like themes or user authentication) that would otherwise have to be passed through many levels of components as props.
Use Case: Accessing global data, such as themes, languages, or authentication status, without prop drilling.
Example: Theme Context
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function ThemedComponent() {
const theme = useContext(ThemeContext);
return <div>Current theme: {theme}</div>;
}
By using useContext
, you can easily access ThemeContext
in any component without needing to pass theme
as a prop through multiple layers.
4. useReducer
useReducer
is an alternative to useState
for managing more complex state logic. It works similarly to Redux and is especially helpful for managing states that rely on multiple transitions or involve complex data updates.
Use Case: Managing state with complex logic, like form handling or multi-step workflows.
Example: Counter with Reducer
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
useReducer
is ideal here because it separates action types and transitions, making it easy to extend with new actions without cluttering the code.
5. useRef
useRef
is a versatile hook that provides a way to store mutable values without causing re-renders. It's commonly used to access and manipulate DOM elements directly.
Use Case: Storing references to DOM elements, managing timers, or holding previous state values.
Example: Focusing an Input Field
import React, { useRef } from 'react';
function FocusInput() {
const inputRef = useRef();
return (
<div>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</div>
);
}
Here, inputRef.current.focus()
directly manipulates the input element, focusing it without a re-render.
6. useMemo
useMemo
is used to optimize performance by memoizing the result of expensive calculations between renders. It only recalculates when its dependencies change.
Use Case: Improving performance for costly calculations or derived values that don’t need recalculating each render.
Example: Expensive Calculation
import React, { useMemo, useState } from 'react';
function ExpensiveCalculation({ value }) {
const result = useMemo(() => {
return performExpensiveCalculation(value);
}, [value]);
return <div>Result: {result}</div>;
}
By memoizing performExpensiveCalculation
, React only recalculates when value
changes, which can significantly improve performance.
7. useCallback
useCallback
is similar to useMemo
but memoizes functions. It's useful for passing stable references of callback functions, especially to child components that rely on shallow comparison.
Use Case: Preventing unnecessary re-renders when passing functions as props to child components.
Example: Passing Stable Callback
import React, { useCallback, useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
return <Child increment={increment} />;
}
function Child({ increment }) {
return <button onClick={increment}>Increment</button>;
}
Using useCallback
ensures that increment
retains the same reference between renders, avoiding unnecessary re-renders of Child
.
8. useLayoutEffect
useLayoutEffect
is like useEffect
but runs synchronously after all DOM mutations. It’s useful when you need to read layout information and make immediate updates to prevent visual flickers.
Use Case: Manipulating the DOM immediately after render to avoid flickering or incorrect initial layout.
Example: Adjusting Layout
import React, { useLayoutEffect, useRef } from 'react';
function ResizableBox() {
const boxRef = useRef();
useLayoutEffect(() => {
const box = boxRef.current;
box.style.width = '200px';
box.style.height = '200px';
}, []);
return <div ref={boxRef} style={{ backgroundColor: 'lightblue' }} />;
}
useLayoutEffect
runs before the component is painted to the screen, ensuring layout adjustments happen immediately.
9. useImperativeHandle
useImperativeHandle
customizes the ref value exposed to a parent component when using forwardRef
. This allows you to limit what actions the parent can perform on a child component.
Use Case: Exposing specific methods or properties to parent components.
Example: Custom Input with Focus Control
import React, { useImperativeHandle, forwardRef, useRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
});
function Parent() {
const ref = useRef();
return (
<div>
<CustomInput ref={ref} />
<button onClick={() => ref.current.focus()}>Focus Input</button>
</div>
);
}
With useImperativeHandle
, you expose only specific methods like focus
, providing better control to the parent.
10. useDebugValue
useDebugValue
helps in adding custom labels for your custom hooks to make debugging easier in React DevTools.
Use Case: Debugging custom hooks more effectively.
Example: Debugging a Custom Hook
import React, { useDebugValue, useState } from 'react';
function useCustomHook(value) {
useDebugValue(value > 10 ? 'High' : 'Low');
return value;
}
function Component() {
const value = useCustomHook(15);
return <div>Value: {value}</div>;
}
In React DevTools, you’ll see a label "High" or "Low" based on the hook’s state, making it clearer when debugging.
11. useId
useId
generates unique IDs that are stable across the client and server. This is especially helpful when creating accessible components that require unique identifiers (e.g., for aria
labels or htmlFor
attributes), without worrying about ID collisions.
Use Case: Associating labels with form elements or ARIA attributes in a way that’s unique across all instances of a component.
Example: Accessible Form Fields
import React, { useId } from 'react';
function FormField() {
const id = useId(); // Generates a unique ID for this component instance
return (
<div>
<label htmlFor={id}>Username</label>
<input id={id} type="text" />
</div>
);
}
By using useId
, every FormField
instance will have a unique id
, which is essential for accessibility and avoids conflicts when rendering multiple instances of the same component.
12. useTransition
useTransition
provides a way to prioritize rendering so that the UI remains responsive during updates that may take longer. It allows you to mark state updates as "transitions" to avoid blocking the main thread and keep the UI interactive.
Use Case: When triggering an update that could delay or "freeze" the UI, such as filtering or sorting a large data set.
Example: Filtering a Large List
import React, { useState, useTransition } from 'react';
function SearchableList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
// Expensive filtering operation marked as a transition
const filteredItems = items.filter(item => item.includes(value));
setFilteredItems(filteredItems);
});
};
return (
<div>
<input type="text" value={query} onChange={handleSearch} placeholder="Search..." />
{isPending ? <p>Loading...</p> : <List items={filteredItems} />}
</div>
);
}
Here, useTransition
ensures that the UI remains responsive by deferring the expensive filtering calculation until it has time to complete. While the filtering occurs, isPending
will be true
, showing a "Loading..." message.
13. useDeferredValue
useDeferredValue
lets you delay updates to a value until the main work is done, helping to prevent UI flickering and maintain smooth rendering for low-priority updates.
Use Case: Displaying non-urgent changes, such as search suggestions, after critical UI tasks are completed.
Example: Deferred Search Suggestions
import React, { useState, useDeferredValue } from 'react';
function SearchInput() {
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input); // Defers this value update
return (
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type to search..."
/>
<SearchSuggestions query={deferredInput} />
</div>
);
}
In this example, SearchSuggestions
only receives deferredInput
, which will lag slightly behind input
. This way, SearchSuggestions
will update only after more immediate tasks are complete, reducing flickering as the user types quickly.
14. useSyncExternalStore
useSyncExternalStore
ensures that React components stay synchronized with external sources of data (such as global state managers or APIs) and handle external updates correctly in concurrent rendering.
Use Case: Integrating with third-party data sources, libraries, or global stores, where changes need to reflect immediately in the component.
Example: Integrating with a Global Store
import React, { useSyncExternalStore } from 'react';
import { subscribe, getState } from './globalStore';
function StoreSubscriber() {
// Uses the global store’s subscribe and getState functions
const state = useSyncExternalStore(subscribe, getState);
return <div>Current Store State: {JSON.stringify(state)}</div>;
}
Here, useSyncExternalStore
allows StoreSubscriber
to stay in sync with the global store without additional state management code, even in React’s concurrent mode.
15. useInsertionEffect
useInsertionEffect
is a low-level hook for injecting styles into the DOM. It runs before DOM mutations, making it ideal for dynamically injecting styles in libraries where critical CSS rendering order matters.
Use Case: Adding styles in libraries or handling third-party CSS where style injection order is critical.
Example: Injecting Styles Dynamically
import React, { useInsertionEffect } from 'react';
function DynamicStyleComponent() {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = '.dynamic { color: blue; }';
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
return <div className="dynamic">Styled Text</div>;
}
This hook ensures that the .dynamic
style is applied immediately before the DOM is rendered, avoiding flickering or style conflicts. It’s particularly valuable in complex styling scenarios where style order matters.
Conclusion
React 18 has introduced a new era of functional component development. Hooks provide a flexible and efficient way to manage state, side effects, and other complex behaviour's within your components.
Each of these React hooks serves a unique purpose that helps you tackle specific challenges in building modern, efficient, and user-friendly applications. By understanding when and why to use them, you can unlock powerful functionality within your components, handle asynchronous rendering smoothly, and maintain a responsive and accessible UI.
If you know any such example to leverage the hooks effectively please share in the comments below. Your insights will be valuable to other developers.
Happy coding! 🚀
Top comments (4)
Good post!
The fact alone that there are 15 hooks you need to learn is what makes React a terrible library when compared to more modern ones. The learning curve on React is too steep.
I know hooks can seem like a lot to learn, but they really add a ton of flexibility once you get used to them. I hope this post helps you find this examples easy to follow.
I'll stick to Svelte instead. One of the top performers nowadays, plus its learning curve is far easier.