Next.js: What Are React Hooks and How Do They Work? | TaQtiQ
Next.js: What Are React Hooks and How Do They Work?
Emmanuel Learmount·
11 min read
An overview of what React hooks are, how they are used, and a look at what the most common hooks are.
Listen to article0:00 / 0:00
What are hooks?
React hooks are functions that allow you to "hook into" React state and lifecycle features.
Hooks allow you to manage local state, run side effects, access context, and optimise performance.
In Next.js hooks work the same as in React.
In Next.js, hooks can only be used in client components (components marked with the 'use client' directive) because they need to tap into client side interactivity (clicking a button, typing an input, etc) and rely on browser APIs and component lifecycle events that only exist in the browser.
What are the rules of hooks?
There are two main rules of hooks:
Only call hooks at the top level.
Do not call hooks inside loops (for, while), conditions (if, switch), or nested functions. Only use hooks at the top level of your React function, before any early returns.
Only call hooks from React functions.
Do not call hooks from regular JavaScript functions. You should only call hooks from:
React function components (components that return JSX).
Custom hooks (functions whose name start with use).
Why do these rules exist?
React relies on call order to track state.
React does not use key names to match useState or useEffect calls to their stored values.
Instead, React maintains an internal array of state pairs for each component. It relies entirely on the exact order in which hooks are called on every single render.
If a hook is placed inside an if statement that only evaluates to true on some renders, the sequence of hook calls changes.
By forcing hooks to be called at the top level, React guarantees that hooks run in the exact same order on every render.
Scope isolation.
Restricting hooks to functional components and custom hooks ensures that state, side effects, and lifecycle behaviours remain strictly scoped to React's component tree and runtime memory.
Predictability and static analysis.
Standardising these rules enables automated tooling (like the official eslint-plugin-react-hooks) to analyse code at compile time and automatically catch potential state bugs before code ever reaches production.
Here is an overview of some common hooks in Next.js, how they work and what they do:
useState
The useState hook is React's primary mechanism for giving functional components their own memory.
It lets a component hold onto data across renders and triggers a UI update whenever that data changes.
Syntax
You import useState from react and call it inside a component:
Initial argument (0): The starting value for the variable on the components initial render.
Return value (array destructuring):useState returns an array with exactly two elements:
The current state value (count): holds the value for the current render.
The state setter function (setCount): Updates the state and tells React to queue a re-render.
How useState works
In Next.js, every time a component re-renders, the function runs from to bottom. Meaning all local variables inside it are wiped and recreated.
use state gets around this by storing state outside your function component and inside React's hidden internal memory.
On first render:
React creates a state cell in memory.
It assigns the initial value (0) to that cell.
It returns [0, setCount].
On subsequent renders:
React ignores the initial value.
It looks up the cell based on the order it was called.
It returns the updated value saved in memory alongside the setter function.
Calling the setter function (setCount(1)) does two things:
It updates the state value stored in React's internal memory cell.
It flags the component as "dirty" and schedules a re-render.
Key things to keep in mind:
State updates are asynchronous. This means that setting state does not immediately change the variable in the current line of code. Instead, it triggers a re-render:
const [count, setCount] = useState(0);
functionhandleClick() {
setCount(count + 1);
console.log(count); // Still prints 0. Value updates on next render.
}
If your new state depends on the previous state, pass a function to the setter instead of the raw value. This ensures you are working with the most up to date state:
setCount(prevCount => prevCount + 1);
Objects and arrays must be replaced, not mutated: React checks if state changed using strict equality (Object.is). If you mutate an object directly, React will not trigger a re-render:
// ❌ Won't trigger a re-render:
user.name = 'Alice';
setUser(user);
// ✅ Triggers a re-render:setUser({ ...user, name: 'Alice' });
Lazy initialisation:
When you pass a value or the result of a function call directly into useState, the expression is evaluated on every single render:
// ❌ Without Lazy Initializationconst [data, setData] = useState(calculateHeavyInitialData());
Initial render: React calls calculateHeavyInitialData(), gets the value and stores it in state.
Re-render 1:calculateHeavyData() is ran again before calling useState. React sees it already has state saved from the first render, discards the new value and moves on.
Re-render 2: It runs again, and gets discarded again.
If calculateHeavyData() contains heavy computations, you are slowing down your application for a calculation that React completely ignores.
To solve this, you should pass an anonymous function in instead:
// ✅ With Lazy Initializationconst [data, setData] = useState(() =>calculateHeavyInitialData());
Now:
Initial render: React calls the function () => calculateHeavyInitialData(), gets the state and saves it.
Subsequent renders: React skips the initialiser function entirely because the state is already initialised. calculateHeavyInitialData() is never executed again.
useEffect
useEffect is Reacts tool for managing side effects.
Side effects are operations that interact with the world outside of a components rendering loop (e. g. fetching data, setting timers or manually accessing the browser DOM).
Syntax
useEffect takes two parameters:
A callback function (the effect).
An optional dependency array.
import { useEffect, useState } from'react';
functionUserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 1. The Effect: Code that runs AFTER renderfetchUserData(userId).then(data =>setUser(data));
// 2. The Cleanup (Optional): Code that runs before unmount/re-effectreturn() => {
console.log('Cleaning up old user fetch...');
};
}, [userId]); // 3. The Dependency Array: Controls when the effect runs
}
The second argument controls the lifecycle behaviour of the effect.
There are three different variations:
No dependency array (runs on every render):
useEffect(() => {
console.log('Component rendered or re-rendered.');
});
Executes after the initial render and on every subsequent render.
Empty dependency array:
useEffect(() => {
console.log('Component appeared on screen for the first time.');
}, []);
This runs only once after the initial render (when the component mounts to the screen)
Dependencies included [a, b] (runs on mount and dependency changes):
useEffect(() => {
console.log(`userId changed to ${userId}, fetching new data...`);
}, [userId]);
Runs on mount and then re-runs only if one of the values in the array (userId) changes between renders.
The cleanup function:
If your effect returns a function, React treats it as a cleanup routine.
Render step: React executes the function component body, generates the JSX, updates the DOM, and paints the screen for the user.
Browser paint: The user immediately sees the visual changes on their screen.
Effect step: React runs the useEffect callback asynchronously in the background so that it doesn't block or slow down screen rendering.
Key things to keep in mind:
Include all used values in dependencies: If the effect uses a variable from the component scope (props or state), it must be listed in the dependency array. Leaving it out leads to "stale closure" bugs where the effect references outdated data.
Avoid infinite loops:useEffect can trigger infinite loops when the same state variable is listed in the dependency array without a conditional check:
Don't use useEffect for transforming data: You don't need useEffect to transform state before rendering. Calculate derived data directly in the body of the function or use useMemo instead.
useContext
useContext is Reacts built in method for sharing data globally across the component tree.
It is the solution for a common issue known as "prop drilling".
Syntax
Setting up and using useContext requires three parts:
Creating
Providing
Consuming.
1. Creating the context
Create a Context object using createContext. This is usually placed in its own file or at the top level of a module:
import { createContext } from'react';
// Create context with an optional default valueexportconstThemeContext = createContext('light');
2. Providing the context
Wrap the component subtree that needs access to the data in a <Context.Provider> and pass the data you want to share into its value prop:
When the value prop passed to <Context.Provider> changes, every component that calls useContext for that specific Context will automatically re-render with the updated value.
If a component calls useContext(ThemeContext) but is not rendered inside a matching <ThemeContext.Provider>, it will fall back to the default value defined inside createContext(...).
Components can consume as many contexts as needed:
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
Stay in the loop
Get notified when we publish new articles and product updates.
Comments (0)