For experienced frontend developers, React interviews often focus on advanced concepts that evaluate both problem-solving abilities and architectural knowledge. With over a decade of React experience, you're expected to have a strong grasp of the framework's core principles, as well as its more sophisticated patterns and performance optimization strategies. To help you shine in these interviews, we've compiled a list of 50 React JS interview questions. These questions cover everything from React's lifecycle and hooks to context management, error boundaries, and performance bottlenecks, ensuring you're well-prepared to demonstrate your expertise in handling complex challenges and building scalable applications.
The Virtual DOM (VDOM) is a lightweight, in-memory representation of the real DOM. When a React component's state or props change, React first updates the VDOM instead of the actual DOM. Then, React performs a "diffing" algorithm to calculate the minimum number of changes required to update the real DOM, ensuring efficient UI rendering.
function Counter() {const [count, setCount] = React.useState(0);return (<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>);}
React's reconciliation algorithm identifies changes in the VDOM tree and updates only the affected parts of the real DOM. It uses the following steps:
key
props to identify stable elements across renders.componentDidMount
useState
, useEffect
, etc.) for state and lifecycleThe Fiber architecture is a re-implementation of React's reconciliation algorithm designed to improve rendering. It breaks rendering into units of work that can be paused and resumed, allowing React to prioritize high-priority tasks (e.g., user input).
React Fragments allow grouping child elements without adding an extra DOM node.
<div>
elements in the DOM, which can cause layout or CSS issuesfunction List() {return (<><li>Item 1</li><li>Item 2</li></>);}
// Parent component passes propsconst Parent = () => {return <Child name="John" />;};// Child component receives propsconst Child = ({ name }) => {return <h1>Hello, {name}</h1>;};
Lifting state up means moving the state to the nearest common ancestor of the components that need to access it. This is necessary for sharing state between sibling components.
Example:
// Lifting state upconst Parent = () => {const [counter, setCounter] = useState(0);return (<div><Child1 counter={counter} /><Child2 setCounter={setCounter} /></div>);};const Child1 = ({ counter }) => <h1>{counter}</h1>;const Child2 = ({ setCounter }) => (<button onClick={() => setCounter((prev) => prev + 1)}>Increment</button>);
In this example, the state is managed in the Parent
component, and both child components access it via props.
To manage deeply nested state, you can use one of the following techniques:
useState
: For simpler state updates in nested structures.useReducer
: More useful for complex or deeply nested state, where actions and reducers help keep state changes predictable.const initialState = { user: { name: 'John', age: 30 } };function reducer(state, action) {switch (action.type) {case 'UPDATE_NAME':return { ...state, user: { ...state.user, name: action.payload } };default:return state;}}const Component = () => {const [state, dispatch] = useReducer(reducer, initialState);return (<button onClick={() => dispatch({ type: 'UPDATE_NAME', payload: 'Jane' })}>Update Name</button>);};
// Controlled componentconst ControlledInput = () => {const [value, setValue] = useState('');return <input value={value} onChange={(e) => setValue(e.target.value)} />;};// Uncontrolled componentconst UncontrolledInput = () => {const inputRef = useRef();return <input ref={inputRef} />;};
Here are some ways to prevent unnecessary re-renders:
// Using React.memoconst MyComponent = React.memo(({ name }) => {return <h1>{name}</h1>;});// Using useMemoconst computedValue = useMemo(() => expensiveComputation(a, b), [a, b]);// Using useCallbackconst memoizedCallback = useCallback(() => {console.log('This function is memoized');}, []);
Hooks allow using state and lifecycle features in functional components, making them concise, reusable, and easier to test.
useEffect
hook, and how do you manage dependencies in it?The useEffect
hook handles side effects like fetching data, updating the DOM, or setting up subscriptions in functional components. Dependencies, specified as the second argument ([]
), control when the effect runs:
[]
): Runs only after the initial render[dep1, dep2]
): Runs when any dependency changesuseMemo
and useCallback
?The key difference between useMemo
and useCallback
lies in what they cache:
useMemo
: Caches the result of a computation to avoid re-computing it unless its dependencies change. Useful for optimizing expensive calculations.useCallback
: Caches a function instance to avoid unnecessary re-creation unless its dependencies change. Useful for preventing child components from re-rendering when passed as props.const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);const memoizedCallback = useCallback(() => callback(a, b), [a, b]);
useReducer
hook in React and when should it be used?The useReducer
hook manages complex state logic in functional components, serving as an alternative to useState
. It's ideal when state has multiple sub-values or when the next state relies on the previous one. It accepts a reducer function and an initial state.
const [state, dispatch] = useReducer(reducer, initialState);
useEffect
and useLayoutEffect
in React?useEffect
and useLayoutEffect
are both used for handling side effects in React functional components but differ in timing:
useEffect
runs asynchronously after the DOM has painted, ideal for tasks like data fetching or subscriptions.useLayoutEffect
runs synchronously after DOM mutations but before the browser paints, useful for tasks like measuring DOM elements or synchronizing the UI with the DOM.Example:
import React, { useEffect, useLayoutEffect, useRef } from 'react';function Example() {const ref = useRef();useEffect(() => {console.log('useEffect: Runs after DOM paint');});useLayoutEffect(() => {console.log('useLayoutEffect: Runs before DOM paint');console.log('Element width:', ref.current.offsetWidth);});return <div ref={ref}>Hello</div>;}
React class components have lifecycle methods for different phases:
constructor
: Initializes state or binds methodscomponentDidMount
: Runs after the component mounts, useful for API calls or subscriptionscomponentDidMount() {console.log('Component mounted');}
shouldComponentUpdate
: Determines if the component should re-rendercomponentDidUpdate
: Runs after updates, useful for side effectscomponentWillUnmount
: Cleans up (e.g., removing event listeners).componentWillUnmount() {console.log('Component will unmount');}
These methods allow you to manage component behavior throughout its lifecycle.
shouldComponentUpdate
and React.memo
optimize re-renders?shouldComponentUpdate
: A lifecycle method in class components. Return false to prevent unnecessary renders.React.memo
: A higher-order component for functional components to prevent re-renders unless props change.const MemoizedComponent = React.memo(({ prop }) => <div>{prop}</div>);
key
prop in lists, and how does React use it to optimize rendering?The key
prop uniquely identifies elements in a list, enabling React to efficiently update the DOM by matching keys during the reconciliation process. Without unique keys, React may unnecessarily re-render elements, leading to performance issues and bugs.
{items.map((item) => <ListItem key={item.id} value={item.value} />);}
React.PureComponent
and when it should be used?React.PureComponent
is a class component that performs a shallow comparison of props and state to determine if the component should re-render. Use it when the component's render output depends solely on its props and state.
Code Splitting: Use React.lazy
and Suspense
to load components only when needed, improving initial load time
Memoization: Use useMemo
and React.memo
to memoize expensive calculations and components, preventing unnecessary re-renders
Debouncing/Throttling: Limit frequent updates (like search inputs) with debouncing or throttling to optimize performance
Pagination and Infinite Scroll: Load data in chunks with pagination or infinite scroll to reduce rendering large datasets at once
Higher-order components (HOCs) are functions that take a component and return a new one with added props or behavior, facilitating logic reuse across components.
const withExtraProps = (WrappedComponent) => {return (props) => <WrappedComponent {...props} extraProp="value" />;};const EnhancedComponent = withExtraProps(MyComponent);
The Context API allows sharing state across the component tree without prop drilling. You create a context with React.createContext()
, wrap your app with a Provider to supply the state, and access it with useContext()
in any component.
Use the Context API for global state (like themes, user data) that needs to be accessed by multiple components, avoiding prop drilling. Example:
const ThemeContext = React.createContext();const App = () => {const [theme, setTheme] = useState('light');return (<ThemeContext.Provider value={{ theme, setTheme }}><SomeComponent /></ThemeContext.Provider>);};
React.lazy
and Suspense
?React.lazy
dynamically loads a component only when needed. Suspense
displays a fallback UI while loading.
const LazyComponent = React.lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>);}
Custom hooks are functions that encapsulate reusable logic.
function useCounter(initialValue = 0) {const [count, setCount] = React.useState(initialValue);const increment = () => setCount((prev) => prev + 1);return { count, increment };}
Use useState
to manage form data, where the form elements' values are controlled by React state. This allows for easy validation and manipulation of form inputs.
const [value, setValue] = useState('');const handleChange = (e) => setValue(e.target.value);return <input value={value} onChange={handleChange} />;
Use useRef
to directly access DOM elements and their values, which can be useful for simple forms or when you want to avoid managing state.
const inputRef = useRef();const handleSubmit = () => console.log(inputRef.current.value);return <input ref={inputRef} />;
Controlled components offer more flexibility and control, while uncontrolled components can be simpler and more efficient for certain use cases.
useRef
in form validation, and how would you implement it?useRef
allows direct access to DOM elements, which is useful for form validation in uncontrolled components or when you need to interact with form fields without managing their state. It can also be used to store values or functions without causing re-renders.
Implementation: You can use useRef to reference input elements and then access their values for validation when the form is submitted.
const inputRef = useRef();function handleSubmit() {const value = inputRef.current.value;if (value.trim() === '') {console.log('Input is required!');} else {console.log('Input value:', value);}}return <input ref={inputRef} />;
For robust form validation, use libraries like Formik or React Hook Form, which handle validation and error management efficiently.
For simple forms, use useState
to track input values and validation errors:
const [value, setValue] = useState('');const [error, setError] = useState('');const handleSubmit = () => {if (!value) setError('Field is required');else console.log('Form submitted:', value);};
Use libraries for complex forms, and useState
for basic validation.
To handle dynamic form fields, use state arrays to manage and update the fields as they are added or removed.
Example:
const [fields, setFields] = useState(['']);const addField = () => setFields([...fields, '']);const removeField = (index) => setFields(fields.filter((_, i) => i !== index));return (<>{fields.map((_, index) => (<input key={index} />))}<button onClick={addField}>Add Field</button></>);
This approach allows you to dynamically add or remove form fields while keeping the state updated.
Debouncing delays execution of an action (e.g., API call) until a specified time has elapsed since the last input.
const handleInput = debounce((value) => console.log(value), 300);
render
, fireEvent
, and screen
for assertions.test('renders button', () => {render(<button>Click Me</button>);expect(screen.getByText('Click Me')).toBeInTheDocument();});
Snapshot testing captures a component's rendered output (DOM structure) at a specific point in time and compares it to a saved baseline to detect unintended changes. It helps ensure UI consistency by alerting you to unexpected modifications.
Use async/await
and waitFor
from React Testing Library to wait for updates after async operations.
Example:
import { render, screen, waitFor } from '@testing-library/react';import MyComponent from './MyComponent';test('displays data after fetching', async () => {render(<MyComponent />);await waitFor(() =>expect(screen.getByText(/fetched data/i)).toBeInTheDocument(),);});
This ensures the test waits for async updates before making assertions.
Mock API calls using Jest's jest.mock
or libraries like Axios Mock Adapter to simulate responses and test components in isolation.
Example:
import axios from 'axios';import { render, screen, waitFor } from '@testing-library/react';import MyComponent from './MyComponent';jest.mock('axios');test('displays fetched data', async () => {axios.get.mockResolvedValue({ data: { message: 'Hello, World!' } });render(<MyComponent />);await waitFor(() =>expect(screen.getByText('Hello, World!')).toBeInTheDocument(),);});
Key Points: -jest.mock
: Replaces the actual module with a mock. 0 Mock Responses: Use mockResolvedValue
or mockRejectedValue
to simulate API behavior.
React Router maps URL paths to components, enabling navigation in single-page apps. Dynamic routing allows you to use URL parameters to render components based on dynamic values.
Example:
import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';function UserPage() {const { id } = useParams(); // Access dynamic parameterreturn <h1>User ID: {id}</h1>;}export default function App() {return (<BrowserRouter><Routes><Route path="/user/:id" element={<UserPage />} /> {/* Dynamic path */}</Routes></BrowserRouter>);}
Key Features:
:id
captures dynamic data from the URL.useParams
Hook: Accesses these dynamic values for rendering.Nested routes allow you to create hierarchies of components, and useParams
helps access dynamic route parameters.
<Outlet>
: Renders child routes within a parent layoutuseParams
: Retrieves route parameters for dynamic routingimport {BrowserRouter,Routes,Route,Outlet,useParams,} from 'react-router-dom';function UserProfile() {const { userId } = useParams();return <h2>User ID: {userId}</h2>;}function App() {return (<BrowserRouter><Routes><Route path="user/:userId" element={<Outlet />}><Route path="profile" element={<UserProfile />} /></Route></Routes></BrowserRouter>);}
BrowserRouter: Uses the HTML5 History API to manage navigation, enabling clean URLs without the hash (#
). It requires server-side configuration to handle routes correctly, especially for deep linking.
HashRouter: Uses the hash (#
) portion of the URL to simulate navigation. It doesn't require server-side configuration, as the hash is never sent to the server. This makes it suitable for environments where server-side routing isn't possible (e.g., static hosting).
To implement private routes, create a component that checks if the user is authenticated before rendering the desired route.
Example:
import { Navigate } from 'react-router-dom';function PrivateRoute({ children }) {return isAuthenticated ? children : <Navigate to="/login" />;}
PrivateRoute
: Checks authentication and either renders the children (protected routes) or redirects to the login page.<Navigate>
: Replaces the deprecated <Redirect>
for redirecting in React Router v6+.Use the useLocation hook to get the current route, and conditionally apply styles for the active state.
Example:
import { useLocation } from 'react-router-dom';function NavBar() {const location = useLocation();return (<nav><ul><li className={location.pathname === '/home' ? 'active' : ''}>Home</li><li className={location.pathname === '/about' ? 'active' : ''}>About</li></ul></nav>);}
Error boundaries are React components that catch JavaScript errors anywhere in the component tree and log those errors, preventing the entire app from crashing. They display a fallback UI instead of the component tree that crashed.
class ErrorBoundary extends React.Component {state = { hasError: false };static getDerivedStateFromError() {return { hasError: true };}componentDidCatch(error, info) {console.log(error, info);}render() {if (this.state.hasError) {return <h1>Something went wrong!</h1>;}return this.props.children;}}
To implement global error handling, you can use Error Boundaries at a top level in your application (such as wrapping your entire app or specific routes). This ensures that even if a component crashes, the rest of the app continues to function.
For asynchronous errors (like API calls), use try-catch
within async
functions, and handle them using state to show error messages. You can also integrate error boundaries to catch these errors in components.
const fetchData = async () => {try {const data = await fetch('/api/data');const result = await data.json();} catch (error) {setError(error.message);}};
Use React Suspense for async components, and display a fallback UI such as a loading spinner or error message when the component or data is still loading.
<Suspense fallback={<Loading />}><MyComponent /></Suspense>
You can also combine error boundaries with loading states to handle network failures.
For large-scale applications, use external error logging services such as Sentry or LogRocket. Integrate these tools into your app to automatically capture and monitor errors in production. Additionally, maintain custom error boundaries and provide meaningful error messages for debugging.
React portals provide a way to render children outside the parent component's DOM hierarchy. They are useful for modals, tooltips, or any UI element that needs to break out of the regular DOM flow but remain part of the React tree.
ReactDOM.createPortal(<Modal />, document.getElementById('modal-root'));
React Suspense is a feature that allows components to "wait" for something (like data or a code-split chunk) before rendering. It improves user experience by showing a fallback UI until the data is ready.
<Suspense fallback={<Loading />}><DataFetchingComponent /></Suspense>
Concurrent Mode allows React to work on multiple tasks simultaneously without blocking the main UI thread. It enables React to prioritize updates and provide smoother rendering for complex applications.
React uses the priority system in Concurrent Mode to schedule updates. It can break up large updates into smaller chunks and give priority to user interactions (like clicks or input) to ensure the app remains responsive.
To avoid blocking the UI, use Web Workers, setTimeout
, or requestIdleCallback
for offloading heavy computations. Alternatively, break tasks into smaller parts and use React's Suspense or useMemo to only recompute when necessary.
Example using setTimeout
for deferring computation:
const [data, setData] = useState(null);useEffect(() => {setTimeout(() => {const result = computeExpensiveData();setData(result);}, 0);}, []);