React Hooks and Modern React Interview Questions

Table of Contents +

Hooks are central to modern React interviews, so this section collects the full hook-focused question set in one place. It includes hook rules, effect behavior, refs, reducers, context with hooks, memoization hooks, concurrent-era hooks, and custom hooks.

This page includes 51 questions in this topic group.

What are hooks?

Hooks is a special JavaScript function that allows you use state and other React features without writing a class. This pattern has been introduced as a new feature in React 16.8 and helped to isolate the stateful logic from the components.

Let’s see an example of useState hook:

import { useState } from "react";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</>
);
}

Note: Hooks can be used inside an existing function component without rewriting the component.

What rules need to be followed for hooks?

You need to follow two rules in order to use hooks,

  1. Call Hooks only at the top level of your react functions: You should always use hooks at the top level of react function before any early returns. i.e, You shouldn’t call Hooks inside loops, conditions, or nested functions. This will ensure that Hooks are called in the same order each time a component renders and it preserves the state of Hooks between multiple re-renders due to useState and useEffect calls.

Let’s see the difference using an example, Correct usage::

function UserProfile() {
// Correct: Hooks called at the top level
const [name, setName] = useState('John');
const [country, setCountry] = useState('US');
return (
<div>
<h1>Name: {name}</h1>
<p>Country: {country}</p>
</div>
);
}

Incorrect usage::

function UserProfile() {
const [name, setName] = useState('John');
if (name === 'John') {
// Incorrect: useState is called inside a conditional
const [country, setCountry] = useState('US');
}
return (
<div>
<h1>Name: {name}</h1>
<p>Country: {country}</p> {/* This will throw an error if the name condition isn't met */}
</div>
);
}

The useState hook for the country field is being called conditionally within an if block. This can lead to inconsistent state behavior and may cause hooks to be called in a different order on each re-render.

  1. Call Hooks from React Functions only: You shouldn’t call Hooks from regular JavaScript functions or class components. Instead, you should call them from either function components or custom hooks.

Let’s find the difference of correct and incorrect usage with below examples,

Correct usage::

//Example1:
function Counter() {
// Correct: useState is used inside a functional component
const [count, setCount] = useState(0);
return <div>Counter: {count}</div>;
}
//Example2:
function useFetchData(url) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => setData(data));
}, [url]);
return data;
}
function UserProfile() {
// Correct: Using a custom hook here
const user = useFetchData('https://some-api.com/user');
return (
<div>
<h1>{user ? user.name : 'Loading profile...'}</h1>
</div>
);
}

Incorrect usage::

//Example1
function normalFunction() {
// Incorrect: Can't call hooks in normal functions
const [count, setCount] = useState(0);
}
//Example2
function fetchData(url) {
// Incorrect: Hooks can't be used in non-React functions
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => setData(data));
}, [url]);
return data;
}

In the above incorrect usage example, both useState and useEffect are used in non-React functions(normalFunction and fetchData), which is not allowed.

How do you ensure hooks followed the rules in your project?

React team released an ESLint plugin called eslint-plugin-react-hooks that enforces Hook’s two rules. It is part of Hooks API. You can add this plugin to your project using the below command,

npm install eslint-plugin-react-hooks --save-dev

And apply the below config in your ESLint config file,

// Your ESLint configuration
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error"
}
}

This plugin also provide another important rule through react-hooks/exhaustive-deps. It ensures that the dependencies of useEffect, useCallback, and useMemo hooks are correctly listed to avoid potential bugs.

useEffect(() => {
// Forgetting `message` will result in incorrect behavior
console.log(message);
}, []); // Here `message` should be a dependency

The recommended eslint-config-react-app preset already includes the hooks rules of this plugin. For example, the linter enforce proper naming convention for hooks. If you rename your custom hooks which as prefix β€œuse” to something else then linter won’t allow you to call built-in hooks such as useState, useEffect etc inside of your custom hook anymore.

Note: This plugin is intended to use in Create React App by default.

What is useEffect hook? How to fetch data with React Hooks?

The useEffect hook is a React Hook that lets you perform side effects in function components. Side effects are operations that interact with the outside world or system and aren’t directly related to rendering UI - such as fetching data, setting up subscriptions, timers, manually manipulating the DOM, etc.

In function components, useEffect replaces the class component lifecycle methods(componentDidMount, componentDidUpdate and componentWillUnmount) with a single, unified API.

Syntax

useEffect(() => {
// Side effect logic here
return () => {
// Cleanup logic (optional)
};
}, [dependencies]);

This effect hook can be used to fetch data from an API and to set the data in the local state of the component with the useState hook’s update function.

Here is an example of fetching a list of ReactJS articles from an API using fetch.

import React from "react";
function App() {
const [data, setData] = React.useState({ hits: [] });
React.useEffect(() => {
fetch("http://hn.algolia.com/api/v1/search?query=react")
.then((response) => response.json())
.then((data) => setData(data));
}, []);
return (
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;

A popular way to simplify this is by using the library axios.

We provided an empty array as second argument to the useEffect hook to avoid activating it on component updates. This way, it only fetches on component mount.

Is Hooks cover all use cases for classes?

Hooks doesn’t cover all use cases of classes but there is a plan to add them soon. Currently there are no Hook equivalents to the uncommon getSnapshotBeforeUpdate and componentDidCatch lifecycles yet.

What is the stable release for hooks support?

React includes a stable implementation of React Hooks in 16.8 release for below packages

  1. React DOM
  2. React DOM Server
  3. React Test Renderer
  4. React Shallow Renderer

Why do we use array destructuring (square brackets notation) in useState?

When we declare a state variable with useState, it returns a pair - an array with two items. The first item is the current value, and the second is a function that updates the value. Using [0] and [1] to access them is a bit confusing because they have a specific meaning. This is why we use array destructuring instead.

For example, the array index access would look as follows:

var userStateVariable = useState("userProfile"); // Returns an array pair
var user = userStateVariable[0]; // Access first item
var setUser = userStateVariable[1]; // Access second item

Whereas with array destructuring the variables can be accessed as follows:

const [user, setUser] = useState("userProfile");

What are the sources used for introducing hooks?

Hooks got the ideas from several different sources. Below are some of them,

  1. Previous experiments with functional APIs in the react-future repository
  2. Community experiments with render prop APIs such as Reactions Component
  3. State variables and state cells in DisplayScript.
  4. Subscriptions in Rx.
  5. Reducer components in ReasonReact.

What is the purpose of eslint plugin for hooks?

The ESLint plugin enforces rules of Hooks to avoid bugs. It assumes that any function starting with β€œuse” and a capital letter right after it is a Hook. In particular, the rule enforces that,

  1. Calls to Hooks are either inside a PascalCase function (assumed to be a component) or another useSomething function (assumed to be a custom Hook).
  2. Hooks are called in the same order on every render.

What is the difference between useState and useRef hook?

  1. useState causes components to re-render after state updates whereas useRef doesn’t cause a component to re-render when the value or state changes. Essentially, useRef is like a β€œbox” that can hold a mutable value in its (.current) property. 2. useState allows us to update the state inside components. While useRef allows referencing DOM elements and tracking values.

What are the differences between useEffect and useLayoutEffect hooks?

useEffect and useLayoutEffect are both React hooks that can be used to synchronize a component with an external system, such as a browser API or a third-party library. However, there are some key differences between the two:

  • Timing: useEffect runs after the browser has finished painting, while useLayoutEffect runs synchronously before the browser paints. This means that useLayoutEffect can be used to measure and update layout in a way that feels more synchronous to the user.

  • Browser Paint: useEffect allows browser to paint the changes before running the effect, hence it may cause some visual flicker. useLayoutEffect synchronously runs the effect before browser paints and hence it will avoid visual flicker.

  • Execution Order: The order in which multiple useEffect hooks are executed is determined by React and may not be predictable. However, the order in which multiple useLayoutEffect hooks are executed is determined by the order in which they were called.

  • Error handling: useEffect has a built-in mechanism for handling errors that occur during the execution of the effect, so that it does not crash the entire application. useLayoutEffect does not have this mechanism, and errors that occur during the execution of the effect will crash the entire application.

In general, it’s recommended to use useEffect as much as possible, because it is more performant and less prone to errors. useLayoutEffect should only be used when you need to measure or update layout, and you can’t achieve the same result using useEffect.

Can you describe the useMemo() Hook?

The useMemo() Hook in React is used to optimize performance by memoizing the result of expensive calculations. It ensures that a function is only re-executed when its dependencies change, preventing unnecessary computations on every re-render.

Syntax

const memoizedValue = useMemo(() => computeExpensiveValue(arg), [dependencies]);
  • computeExpensiveValue:
    A function that returns the computed result.

  • dependencies:
    An array of values that, when changed, will cause the memoized function to re-run.

If the dependencies haven’t changed since the last render, React returns the cached result instead of re-running the function.

Let’s exaplain the usage of useMemo hook with an example of user search and its respective filtered users list.

Example: Memoizing a Filtered List

import React, { useState, useMemo } from 'react';
const users = [
{ id: 1, name: 'Sudheer' },
{ id: 2, name: 'Brendon' },
{ id: 3, name: 'Charlie' },
{ id: 4, name: 'Dary' },
{ id: 5, name: 'Eden' }
];
export default function UserSearch({ users }) {
const [searchTerm, setSearchTerm] = useState('');
const [counter, setCounter] = useState(0);
// Memoize the filtered user list based on the search term
const filteredUsers = useMemo(() => {
console.log("Filtering users...");
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [searchTerm]);
return (
<div>
<h2>Counter: {counter}</h2>
<button onClick={() => setCounter(prev => prev + 1)}>Increment Counter</button>
<h2>Search Users</h2>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Enter name"
/>
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

In the above example:

  • The filteredUsers list is only recomputed when searchTerm changes.
  • Pressing the β€œIncrement Counter” button does not trigger the filtering logic again, as it’s not a dependency.
  • The console will only log β€œFiltering users…” when the search term updates.

Can Hooks be used in class components?

No, Hooks cannot be used inside class components. They are specially designed for function components. This is because hooks depend on the sequence in which they are called during a component’s render, something that’s only guaranteed in functional components. However, both class and function components can coexist in the same application.

What is an updater function? Should an updater function be used in all cases?

An updater function is a form of setState where you pass a function instead of a direct value. This function receives the previous state as an argument and returns the next state.

The updater function expression looks like below,

setCount(prevCount => prevCount + 1); // Safe and predictable

Here, prevCount => prevCount + 1 is the updater function.

In the React community, there’s often a recommendation to use updater functions when updating state that depends on its previous value. This helps prevent unexpected behaviors that can arise from working with outdated or β€œstale” state.

While using an updater function is a good habit, it’s not always necessary. In most cases, React batches updates and ensures that the state is up-to-date at the beginning of the event handler, so you typically don’t run into stale state issues during a single synchronous event. However, if you’re doing multiple updates to the same state variable within a single handler, using the updater form ensures that each update correctly uses the latest state value, rather than a potentially outdated one.

Example: Multiple Updates in One Handler

function handleCount() {
setCounter(a => a + 1);
setCounter(a => a + 1);
setCounter(a => a + 1);
}

In this example, a => a + 1 is an updater function. React queues these updater functions and applies them sequentially, each using the most recent state value. As a result, the counter will correctly increment by 3.

In many cases, such as setting state based on user input or assigning static values, you don’t need the updater function:

setName('Sudheer');

Can useState take a function as an initial value?

Yes, useState can take a function as an initial value, and this is a useful feature in React called lazy initialization. This function is also known as initializer function.

When you call useState(initialValue), you normally pass in a value directly:

const [count, setCount] = useState(0); // initial value is 0

But if calculating that initial value is expensive or involves logic, you can pass a function that returns the value:

const [count, setCount] = useState(() => {
// This function only runs once - when the component first renders
return expensiveComputation();
});

This function avoids doing heavy computation on every render. If you don’t use this function form and invokes it directly, the function will run everytime the component renders and impact the performance. For example, the below usage is not recommended.

const [count, setCount] = useState(expensiveComputation());

What types of values can useState hold?

The useState hook accepts different types of values.

  • Primitives: number, string, boolean
  • Arrays
  • Objects
  • Functions
  • null or undefined

But you needs to be cautious with reference types (objects/arrays) because React compares old and new values by reference, so direct mutations won’t trigger a re-render. For example, the correct and wrong ways of state updates as shown below,

user.name = "Sudheer"; //wrong way
setUser(prev => ({ ...prev, name: 'Sudheer' })); //correct way

What happens if you call useState conditionally?

As per rules of React Hooks, hooks must be called unconditionally. For example, if you conditionally call it:

if (someCondition) {
const [state, setState] = useState(0);
}

React will throw a runtime error because it relies on the order of Hook calls, and conditional logic breaks that order.

Is useState Synchronous or Asynchronous?

The useState hook is synchronous, but state updates are asynchronous. When you call useState(), it runs synchronously and returns the state variable and setter function as tuple.

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

This happens immediately during rendering. However, the state update function (setState) is asynchronous in the sense that it doesn’t update the state immediately. React batches updates and applies them before the next render. You won’t see the updated value immediately after calling setState.

Example:

const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // ❗️Still logs the old value
}

The > console.log(count) prints the old value, because the update hasn’t happened yet.

To see the updated state value, you can use useEffect() hook. It runs after the component has re-rendered. By the time useEffect runs:

  • The component has been updated.
  • The state contains the new value.
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log('Clicked count (old):', count); // Old value
};
useEffect(() => {
console.log('Updated count:', count); // New value
}, [count]); // Only runs when `count` changes
return <button onClick={handleClick}>Count: {count}</button>;
}

Can you explain how useState works internally?

React’s hooks, including useState, rely on some internal machinery that keeps track of state per component and per hook call during rendering. Here’s a simplified explanation of the internal mechanics:

1. Hook List / Linked List

  • React maintains a linked list or array of β€œhook states” for each component.
  • When a component renders, React keeps track of which hook it is currently processing via a cursor/index.
  • Each call to useState() corresponds to one β€œslot” in this list.

2. State Storage

  • Each slot stores:
  • The current state value.
  • A queue of pending state updates.

3. Initial Render

  • When the component first renders, React:
  • Creates a new slot for useState with the initial state (e.g., 0).
  • Returns [state, updaterFunction].

4. Updater Function

  • The updater function (setCount) is a closure that, when called:
  • Enqueues a state update to React’s internal queue.
  • Schedules a re-render of the component.

5. Re-render and State Update

  • On the next render:
  • React processes all queued updates for each hook slot.
  • Updates the stored state value accordingly.
  • Returns the new state to the component.

6. Important: Hook Order

  • Hooks must be called in the same order on every render so React can match hook calls to their internal slots.
  • That’s why you can’t call hooks conditionally.

The pseudocode for internal implementation of useState looks like below,

let hookIndex = 0;
const hooks = [];
function useState(initialValue) {
const currentIndex = hookIndex;
if (!hooks[currentIndex]) {
// First render: initialize state
hooks[currentIndex] = {
state: initialValue,
queue: [],
};
}
const hook = hooks[currentIndex];
// Process queued updates
hook.queue.forEach(update => {
hook.state = update(hook.state);
});
hook.queue = [];
// Define updater function
function setState(action) {
// action can be new state or function(state) => new state
hook.queue.push(typeof action === 'function' ? action : () => action);
scheduleRender(); // triggers React re-render
}
hookIndex++;
return [hook.state, setState];
}

What would the context value be for no matching provider?

When a component calls useContext(SomeContext) but no matching <SomeContext.Provider> is present higher up in the component tree, the default value passed to React.createContext(defaultValue) is returned.

const ThemeContext = React.createContext('light'); // 'light' is the default value
function ThemedComponent() {
const theme = useContext(ThemeContext);
return <div>Current theme: {theme}</div>;
}
// No ThemeContext.Provider anywhere in the tree

In this case, theme will be β€˜light’. It’s the default value you provided when you created the context.

Note: If you don’t specify a default value, the context value will be undefined when used without a provider:

const AuthContext = React.createContext(); // No default
function Profile() {
const auth = useContext(AuthContext);
// auth will be undefined if there's no AuthContext.Provider
}

How do reactive dependencies in the useEffect dependency array affect its execution behavior?

The useEffect hook accepts an optional dependencies argument that accepts an array of reactive values. The dependency array determines when the effect runs. i.e, It makes useEffect reactive to changes in specified values.

How Dependency Array Affects Behavior

  1. Empty Dependency Array: **[]**
useEffect(() => {
// runs once after the initial render
}, []);
  • Effect runs only once (like componentDidMount).
  • Ignores all state/prop changes.
  1. With Specific Dependencies: **[count, user]**
useEffect(() => {
// runs after initial render
// AND whenever `count` or `user` changes
}, [count, user]);
  • Effect runs on first render, and
  • Again every time any dependency value changes.
  1. No Dependency Array (Omitted)

    useEffect(() => {
    // runs after **every** render
    });
    • Effect runs after every render, regardless of what changed.
    • Can lead to performance issues if not used carefully.

React uses shallow comparison of the dependencies. If any value has changed (!==), the effect will re-run.

Note: This hook works well when dependencies are primitives or memoized objects/functions.

When and how often does React invoke the setup and cleanup functions inside a useEffect hook?

  1. Setup Function Execution (useEffect)

    The setup function (or the main function) you pass to useEffect runs at specific points:

    1. After the component is mounted (if the dependency array is empty [])

    2. After every render (if no dependency array is provided)

    3. After a dependency value changes (if the dependency array contains variables)

    4. Cleanup Function Execution (Returned function from useEffect)

      The cleanup function is called before the effect is re-executed and when the component unmounts.

What happens if you return a Promise from useEffect?

You should NOT return a Promise from useEffect. React expects the function passed to useEffect to return either nothing (undefined) or a cleanup function (synchronous function). i.e, It does not expect or handle a returned Promise. If you still return a Promise, React will ignore it silently, and it may lead to bugs or warnings in strict mode.

Incorrect:

useEffect(async () => {
await fetchData(); // ❌ useEffect shouldn't be async
}, []);

Correct:

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

Can you have multiple useEffect hooks in a single component?

Yes, multiple useEffect hooks are allowed and recommended when you want to separate concerns.

useEffect(() => {
// Handles API fetch
}, []);
useEffect(() => {
// Handles event listeners
}, []);

Each effect runs independently and helps make code modular and easier to debug.

How do you prevent infinite loops with useEffect?

Infinite loops happen when the effect updates state that’s listed in its own dependency array, which causes the effect to re-run, updating state again and so on.

Infinite loop scenario:

useEffect(() => {
setCount(count + 1);
}, [count]); // Triggers again every time count updates

You need to ensure that setState calls do not depend on values that cause the effect to rerun, or isolate them with a guard.

useEffect(() => {
if (count < 5) {
setCount(count + 1);
}
}, [count]);

What are the usecases of useLayoutEffect?

You need to use useLayoutEffect when your effect must run before the browser paints, such as:

  • Reading layout measurements (e.g., element size, scroll position)
  • Synchronously applying DOM styles to prevent visual flicker
  • Animating layout or transitions
  • Integrating with third-party libraries that require DOM manipulation

If there’s no visual or layout dependency, prefer useEffect - it’s more performance-friendly.

useLayoutEffect(() => {
const width = divRef.current.offsetWidth;
if (width < 400) {
divRef.current.style.background = 'blue'; // prevents flicker
}
}, []);

How does useLayoutEffect work during server-side rendering (SSR)?

The useLayoutEffect hook does not run on the server, because there is no DOM. React issues a warning in server environments like Next.js if useLayoutEffect is used directly.

This can be mitigated using a conditional polyfill:

const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;

i.e, Use useIsomorphicLayoutEffect in components that render both on client and server.

What happens if you use useLayoutEffect for non-layout logic?

Using useLayoutEffect for logic unrelated to layout or visual DOM changes (such as logging, data fetching, or analytics) is not recommended. It can lead to performance issues or even unexpected behavior.

Example: Anti-pattern

useLayoutEffect(() => {
console.log("Tracking analytics");
fetch('/log-page-view');
}, []);

The above usage delays the paint of the UI just to send a network request, which could (and should) be done after paint using useEffect.

How does useLayoutEffect cause layout thrashing?

The useLayoutEffect can cause layout thrashing when you repeatedly read and write to the DOM in ways that force the browser to recalculate layout multiple times per frame. This is because useLayoutEffect runs before the browser paints, these reflows happen synchronously, blocking rendering and degrading performance.

Example:

function ThrashingComponent() {
const ref = useRef();
useLayoutEffect(() => {
const height = ref.current.offsetHeight; //Read
ref.current.style.height = height + 20 + 'px'; //Write
const newHeight = ref.current.offsetHeight; //Read again - forces reflow
}, []);
return <div ref={ref}>Hello</div>;
}

In the above code, each read/write cycle triggers synchronous reflows, blocking the main thread and delays UI rendering.

This issue can be avoided by batching your DOM reads and writes and prevent unnecessary reads after writes.

How Do You Use useRef to Access a DOM Element in React? Give an example.

The useRef hook is commonly used in React to directly reference and interact with DOM elements - like focusing an input, scrolling to a section, or controlling media elements.

When you assign a ref to a DOM element using useRef, React gives you access to the underlying DOM node via the .current property of the ref object.

Example: Focus an input

import React, { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null); // create the ref
const handleFocus = () => {
inputRef.current.focus(); // access DOM element and focus it
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleFocus}>Focus the input</button>
</div>
);
}

Note: The DOM reference is only available after the component has mounted - typically accessed in useEffect or event handlers.

Can you use useRef to persist values across renders?

Yes, you can use useRef to persist values across renders in React. Unlike useState, changing .current does not cause re-renders, but the value is preserved across renders.

Example:

function Timer() {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
console.log("Render count:", renderCount.current);
});
return <div>Check console for render count.</div>;
}

Can useRef be used to store previous values?

Yes, useRef is a common pattern when you want to compare current and previous props or state without causing re-renders.

Example: Storing previous state value

import { useEffect, useRef, useState } from 'react';
function PreviousValueExample() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}

Is it possible to access a ref in the render method?

Yes, you can access a ref in the render method, but what you get from it depends on how you’re using the ref and when in the component lifecycle you’re rendering.

For example, when using ref to access a DOM node (e.g., divRef.current), it’s not immediately available on the first render.

const divRef = useRef(null);
console.log(divRef.current); // ❌ null on initial render
return <div ref={divRef}>Hello</div>;

What are the common usecases of useRef hook?

Some of the common cases are:

  • Automatically focus an input when a component mounts.
  • Scroll to a specific element.
  • Measure element dimensions (offsetWidth, clientHeight).
  • Control video/audio playback.
  • Integrate with non-React libraries (like D3 or jQuery).

What is useImperativeHandle Hook? Give an example.

useImperativeHandle is a React Hook that allows a child component to expose custom functions or properties to its parent component, when using ref. It is typically used with forwardRef and is very useful in cases like modals, dialogs, custom inputs, etc., where the parent needs to control behavior imperatively (e.g., open, close, reset).

Example: Dialog component

import React, {
useRef,
useState,
useImperativeHandle,
forwardRef,
} from 'react';
import './Dialog.css';
const Dialog = forwardRef((props, ref) => {
const [isOpen, setIsOpen] = useState(false);
const [formData, setFormData] = useState('');
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
reset: () => setFormData(''),
}));
if (!isOpen) return null;
return (
<div className="dialog">
<h2>Dialog</h2>
<input
type="text"
value={formData}
placeholder="Type something..."
onChange={(e) => setFormData(e.target.value)}
/>
<br />
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
);
});
function Parent() {
const dialogRef = useRef();
return (
<div>
<h1>useImperativeHandle Dialog Example</h1>
<button onClick={() => dialogRef.current.open()}>Open Dialog</button>
<button onClick={() => dialogRef.current.reset()}>Reset Dialog</button>
<button onClick={() => dialogRef.current.close()}>Close Dialog</button>
<Dialog ref={dialogRef} />
</div>
);
}
export default Parent;

When should you use useImperativeHandle?

The useImperativeHandler hook will be used in below cases:

  • You want to expose imperative methods from a child component
    • Custom input controls exposing focus, clear, or validate methods
    • Modal components exposing open() and close() methods
    • Scroll containers exposing scrollToTop() or scrollToBottom() methods
  • You want to hide internal implementation but provide controlled external access.
  • You’re building reusable component libraries (e.g., inputs, modals, form controls).

Is that possible to use useImperativeHandle without forwardRef?

No. useImperativeHandle only works when the component is wrapped in forwardRef. It’s the combination that allows parent components to use a ref on a function component.

How is useMemo different from useCallback?

The following table compares both useMemo and useCallback:

FeatureuseMemouseCallback
PurposeMemoizes the result of a computationMemoizes a function reference
ReturnsA value (e.g., result of a function)A function
UsageuseMemo(() => computeValue(), [deps])useCallback(() => doSomething(), [deps])
Primary Use CaseAvoid expensive recalculationsPrevent unnecessary re-creations of functions
Common ScenarioFiltering, sorting, calculating derived dataPassing callbacks to child components
When It’s UsefulWhen the value is expensive to computeWhen referential equality matters (e.g., props)
Recomputed WhenDependencies changeDependencies change
Returned Value TypeAny (number, object, array, etc.)Always a function
OverheadSlight (evaluates a function and caches result)Slight (caches a function reference)

Does useMemo prevent re-rendering of child components?

The useMemo hook does not directly prevent re-rendering of child components. Its main purpose is to memoize the result of an expensive computation so that it doesn’t get recalculated unless its dependencies change. While this can improve performance, it doesn’t inherently control whether a child component re-renders.

However, useMemo can help prevent re-renders when the memoized value is passed as a prop to a child component that is wrapped in React.memo. In that case, if the memoized value doesn’t change between renders (i.e., it has the same reference), React.memo can skip re-rendering the child. So, while useMemo doesn’t stop renders on its own, it works in combination with tools like React.memo to optimize rendering behavior.

What is useCallback and why is it used?

The useCallback is a React Hook used to memoize function definitions between renders. It returns the same function reference unless its dependencies change. This is especially useful when passing callbacks to optimized child components (e.g. those wrapped in React.memo) to prevent unnecessary re-renders.

Example:

const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);

Without useCallback, a new function is created on every render, potentially causing child components to re-render unnecessarily.

What are Custom React Hooks, and How Can You Develop One?

Custom Hooks in React are JavaScript functions that allow you to extract and reuse component logic using React’s built-in Hooks like useState, useEffect, etc.

They start with the word β€œuse” and let you encapsulate logic that multiple components might share-such as fetching data, handling forms, or managing timers-without repeating code.

Let’s explain the custom hook usage with useFetchData example. The useFetchData custom Hook is a reusable function in React that simplifies the process of fetching data from an API. It encapsulates common logic such as initiating the fetch request, managing loading and error states, and storing the fetched data. By using built-in Hooks like useState and useEffect, useFetchData provides a clean interface that returns the data, loading, and error values, which can be directly used in components.

import { useState, useEffect } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null); // Holds the response
const [loading, setLoading] = useState(true); // Loading state
const [error, setError] = useState(null); // Error state
useEffect(() => {
let isMounted = true; // Prevent setting state on unmounted component
setLoading(true);
fetch(url)
.then((response) => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then((json) => {
if (isMounted) {
setData(json);
setLoading(false);
}
})
.catch((err) => {
if (isMounted) {
setError(err.message);
setLoading(false);
}
});
return () => {
isMounted = false; // Clean-up function to avoid memory leaks
};
}, [url]);
return { data, loading, error };
}

The above custom hook can be used to retrieve users data for AuthorList, ReviewerList components.

Example: AuthorList component

function AuthorList() {
const { data, loading, error } = useFetchData('https://api.example.com/authors');
if (loading) return <p>Loading authors...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.map((author) => (
<li key={author.id}>{author.name}</li>
))}
</ul>
);
}

Some of the benefits of custom hooks are:

  • Promotes code reuse
  • Keeps components clean and focused
  • Makes complex logic easier to test and maintain

What is the useId hook and when should you use it?

The useId hook is a React hook introduced in React 18 that generates unique IDs that are stable across server and client renders. It’s primarily used for accessibility attributes like linking form labels to inputs.

Syntax

const id = useId();

Example: Accessible Form Input

import { useId } from 'react';
function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email:</label>
<input id={id} type="email" />
</div>
);
}

When to Use

  • Generating unique IDs for form elements (htmlFor, aria-describedby, aria-labelledby)
  • Creating stable IDs in server-side rendering (SSR) applications
  • Avoiding ID collisions when the same component is rendered multiple times

When NOT to Use

  • As keys in a list (use data-based keys instead)
  • As CSS selectors or query selectors
  • For any purpose that requires the ID to be predictable

Note: The IDs generated by useId contain colons (:) which may not work in CSS selectors. For multiple related IDs, you can use the same id as a prefix: ${id}-firstName, ${id}-lastName.

What is the useDeferredValue hook?

The useDeferredValue hook is used to defer updating a part of the UI to keep other parts responsive. It accepts a value and returns a β€œdeferred” version of that value that may lag behind. This is useful for optimizing performance when rendering expensive components.

Syntax

const deferredValue = useDeferredValue(value);

Example: Search with Deferred Results

import { useState, useDeferredValue, useMemo } from 'react';
function SearchResults({ query }) {
// Expensive computation or large list filtering
const results = useMemo(() => {
return largeDataSet.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
return (
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</div>
);
}

The input stays responsive while the expensive SearchResults component re-renders with a slight delay using the deferred value.

What is the useTransition hook and how does it differ from useDeferredValue?

The useTransition hook allows you to mark certain state updates as non-urgent transitions, keeping the UI responsive during expensive re-renders. It returns a isPending flag and a startTransition function.

Syntax

const [isPending, startTransition] = useTransition();

Example: Tab Switching

import { useState, useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('home');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onClick={() => selectTab('home')}>Home</button>
<button onClick={() => selectTab('posts')}>Posts (slow)</button>
<button onClick={() => selectTab('contact')}>Contact</button>
{isPending && <Spinner />}
{tab === 'home' && <HomeTab />}
{tab === 'posts' && <PostsTab />} {/* Expensive component */}
{tab === 'contact' && <ContactTab />}
</div>
);
}

Differences from useDeferredValue

FeatureuseTransitionuseDeferredValue
ControlsState updates (wraps setState)Values (wraps a value)
Use caseWhen you control the state updateWhen you receive a value from props or other hooks
Returns[isPending, startTransition]Deferred value
Pending stateBuilt-in isPending flagManual comparison needed

What is the useSyncExternalStore hook?

The useSyncExternalStore hook is designed to subscribe to external stores (non-React state sources) in a way that’s compatible with concurrent rendering. It’s primarily used by library authors for state management libraries.

Syntax

const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe: Function to subscribe to the store, returns an unsubscribe function
  • getSnapshot: Function that returns the current store value
  • getServerSnapshot: Optional function for SSR that returns the initial server snapshot

Example: Browser Online Status

import { useSyncExternalStore } from 'react';
function getSnapshot() {
return navigator.onLine;
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, () => true);
}
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? 'βœ… Online' : '❌ Disconnected'}</h1>;
}

This hook ensures that when the external store changes, React re-renders consistently without tearing (showing inconsistent data).

What is the useInsertionEffect hook?

The useInsertionEffect hook is designed for CSS-in-JS library authors to inject styles into the DOM before any layout effects run. It fires synchronously before DOM mutations.

Syntax

useInsertionEffect(() => {
// Insert styles here
return () => {
// Cleanup
};
}, [dependencies]);

Execution Order

1. useInsertionEffect β†’ Inject styles
2. DOM mutations β†’ React updates DOM
3. useLayoutEffect β†’ Read layout, synchronously re-render if needed
4. Browser paint β†’ User sees the result
5. useEffect β†’ Side effects run

Example: Dynamic Style Injection

import { useInsertionEffect } from 'react';
let isInserted = new Set();
function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
}
}, [rule]);
}
function Button() {
useCSS('.dynamic-btn { background: blue; color: white; }');
return <button className="dynamic-btn">Click me</button>;
}

Note: This hook is not intended for application code. It’s specifically for CSS-in-JS libraries like styled-components or Emotion to prevent style flickering.

How do you share state logic between components using custom hooks?

Custom hooks allow you to extract and share stateful logic between components without changing their hierarchy. The state itself is not shared-each component using the hook gets its own isolated state.

Example: useLocalStorage Hook

import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get stored value or use initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Update localStorage when state changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Usage in multiple components
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
function FontSizeSelector() {
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<input
type="range"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
);
}

Both components use useLocalStorage, but each has its own independent state that persists to localStorage.

What is the useDebugValue hook?

The useDebugValue hook is used to display a label for custom hooks in React DevTools. It helps developers debug custom hooks by showing meaningful information.

Syntax

useDebugValue(value);
useDebugValue(value, formatFn); // With optional formatter

Example: Custom Hook with Debug Value

import { useState, useEffect, useDebugValue } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// Shows "OnlineStatus: Online" or "OnlineStatus: Offline" in DevTools
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}

With Formatting Function (for expensive computations)

function useUser(userId) {
const [user, setUser] = useState(null);
// The format function only runs when DevTools is open
useDebugValue(user, (user) => user ? `User: ${user.name}` : 'Loading...');
return user;
}

Note: Only use useDebugValue in custom hooks that are part of shared libraries. It’s not necessary for every custom hook in application code.

How do you handle cleanup in useEffect?

The cleanup function in useEffect is used to clean up side effects before the component unmounts or before the effect runs again. This prevents memory leaks, stale data, and unexpected behavior.

Syntax

useEffect(() => {
// Setup code
return () => {
// Cleanup code
};
}, [dependencies]);

Common Cleanup Scenarios

1. Event Listeners

useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

2. Timers and Intervals

useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);

3. Subscriptions

useEffect(() => {
const subscription = dataSource.subscribe(handleChange);
return () => subscription.unsubscribe();
}, [dataSource]);

4. Abort Fetch Requests

useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(response => response.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
});
return () => controller.abort();
}, [url]);

When Cleanup Runs:

  • Before the component unmounts
  • Before re-running the effect when dependencies change

What are the differences between useEffect and useEvent (experimental)?

useEvent is an experimental hook (not yet stable in React) designed to solve the problem of creating stable event handlers that always access the latest props and state without causing re-renders or needing to be in dependency arrays.

The Problem useEvent Solves

// Problem: onTick changes on every render, causing interval to reset
function Timer({ onTick }) {
useEffect(() => {
const id = setInterval(() => {
onTick(); // Uses stale closure if onTick is not in deps
}, 1000);
return () => clearInterval(id);
}, [onTick]); // Adding onTick causes interval to reset frequently
}

Solution with useEvent (Experimental)

import { useEvent } from 'react'; // Experimental
function Timer({ onTick }) {
const stableOnTick = useEvent(onTick);
useEffect(() => {
const id = setInterval(() => {
stableOnTick(); // Always calls latest onTick
}, 1000);
return () => clearInterval(id);
}, []); // No dependency needed!
}

Key Differences

FeatureuseEffectuseEvent (experimental)
PurposeRun side effectsCreate stable callbacks
RunsAfter renderDuring render (creates function)
ReturnsCleanup functionStable event handler
ClosureCaptures values at render timeAlways accesses latest values
DependenciesMust list all used valuesNot needed in other hooks’ deps

Note: Until useEvent is stable, you can use useCallback with useRef as a workaround for stable callbacks.

What are the best practices for using React Hooks?

Following best practices ensures your hooks are predictable, maintainable, and bug-free.

1. Follow the Rules of Hooks

  • Only call hooks at the top level (not inside loops, conditions, or nested functions)
  • Only call hooks from React functions (components or custom hooks)

2. Use the ESLint Plugin

Terminal window
npm install eslint-plugin-react-hooks --save-dev
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

3. Keep Hooks Focused and Simple

// ❌ Bad: One hook doing too much
function useEverything() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [theme, setTheme] = useState('light');
// ... lots of unrelated logic
}
// βœ… Good: Separate concerns
function useUser() { /* user logic */ }
function usePosts() { /* posts logic */ }
function useTheme() { /* theme logic */ }

4. Use Descriptive Names for Custom Hooks

// ❌ Bad
function useData() { }
// βœ… Good
function useUserAuthentication() { }
function useFetchProducts() { }
function useFormValidation() { }

5. Properly Manage Dependencies

// ❌ Bad: Missing dependency
useEffect(() => {
fetchUser(userId);
}, []); // userId is missing
// βœ… Good: All dependencies listed
useEffect(() => {
fetchUser(userId);
}, [userId]);

6. Avoid Inline Object/Function Dependencies

// ❌ Bad: New object on every render
useEffect(() => {
doSomething(options);
}, [{ page: 1, limit: 10 }]); // Always different reference
// βœ… Good: Memoize or extract
const options = useMemo(() => ({ page: 1, limit: 10 }), []);
useEffect(() => {
doSomething(options);
}, [options]);

7. Clean Up Side Effects

Always return a cleanup function when subscribing to events, timers, or external data sources.

Share & Connect