React Fundamentals & Core Concepts
1. Explain the Virtual DOM and how React uses it.
The Virtual DOM is a lightweight, in-memory representation of the actual DOM. React creates a virtual tree structure of UI elements. When state or props change, React builds a new Virtual DOM tree. It then performs a process called reconciliation, comparing the new tree with the previous one to identify the minimal set of changes needed. These changes are then batched and applied efficiently to the real DOM, reducing direct DOM manipulations which are often expensive. This optimization significantly improves performance.
2. Differentiate between Props and State in React.
Props (short for properties) are used to pass data from a parent component to a child component. They are immutable within the child component, meaning a child cannot modify its own props. Props facilitate component communication and reusability. State, on the other hand, is data managed within a component. It is mutable and represents the component's internal data that can change over time. When state changes, the component re-renders.
// Props example
function ChildComponent(props) {
return <p>Hello, {props.name}!</p>;
}
// State example
function Counter() {
const [count, setCount] = React.useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
3. What is JSX, and what are its benefits?
JSX (JavaScript XML) is a syntax extension for JavaScript that allows you to write HTML-like elements directly within your JavaScript code. It's not mandatory but is highly recommended because it makes the UI structure visually intuitive and easier to understand. JSX is transformed by tools like Babel into React.createElement() calls, which then create React elements. Its benefits include improved readability, static type checking (with TypeScript), and leveraging JavaScript's full power within your markup.
const element = <h1>Hello, JSX!</h1>;
// Behind the scenes, Babel transforms this to:
// const element = React.createElement('h1', null, 'Hello, JSX!');
4. How does React's reconciliation algorithm work?
React's reconciliation algorithm (also known as the diffing algorithm) is a highly optimized process. When a component's state or props change, React creates a new Virtual DOM tree. It then compares this new tree with the previous one. The algorithm works based on two main assumptions:
- Two elements of different types will produce different trees.
- Developers can hint at which child elements may be stable across renders with a
keyprop. When comparing, React first compares root elements. If types differ (e.g.,<div>to<span>), it destroys the old tree and builds a new one. If types are the same, it updates only the changed attributes. For lists, it useskeysto identify unique elements and perform minimal DOM updates, avoiding re-rendering entire lists.
5. Explain the purpose of keys when rendering lists in React.
Keys are special string attributes you must include when creating lists of elements in React, typically when mapping over an array. Their purpose is to help React identify which items in a list have changed, been added, or been removed. When a list is re-rendered, React uses keys to efficiently reconcile the old and new lists. Without stable keys, React might perform inefficient updates, leading to performance issues and potential bugs, especially with stateful list items. Keys must be unique among siblings. Using index as a key is generally discouraged if the list order can change or items can be added/removed, as it can cause unexpected behavior.
function ItemList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // item.id is a stable, unique identifier
))}
</ul>
);
}
6. What is Component Lifecycle in a functional component with Hooks?
In modern React (functional components with Hooks), the traditional class component lifecycle methods are replaced by Hooks, primarily useEffect.
- Mounting:
useEffectwith an empty dependency array ([]) runs once after the initial render, similar tocomponentDidMount. - Updating:
useEffectruns after every render where one of its dependencies has changed, similar tocomponentDidUpdate. - Unmounting: The cleanup function returned by
useEffectruns just before the component is removed from the DOM, similar tocomponentWillUnmount.
import React, { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// This runs after initial mount and whenever count changes
console.log('Component rendered or count changed:', count);
return () => {
// This runs on unmount or before re-running the effect
console.log('Cleanup for count:', count);
};
}, [count]); // Dependency array: effect re-runs if count changes
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
Hooks & State Management
7. Describe the useState Hook and how it works.
The useState Hook allows functional components to manage state. It returns an array containing two elements: the current state value and a function to update it. When the update function is called, React re-renders the component with the new state. useState can take an initial state value as an argument, which can be a direct value or a function that computes the initial state, useful for expensive calculations that should only run once.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 0 is the initial state
const [name, setName] = useState(() => {
// Lazy initial state for expensive calculations
return localStorage.getItem('userName') || 'Guest';
});
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Name: {name}</p>
<input value={name} onChange={e => setName(e.target.value)} />
</>
);
}
8. Explain useEffect and its dependency array.
The useEffect Hook lets you perform side effects in functional components. Side effects include data fetching, DOM manipulation, subscriptions, or setting up timers. It takes two arguments: a function containing the effect logic and an optional dependency array.
- No dependency array: Effect runs after every render.
- Empty array (
[]): Effect runs once after the initial render and cleans up on unmount (similar tocomponentDidMountandcomponentWillUnmount). - Array with dependencies (
[dep1, dep2]): Effect runs after the initial render and whenever any of the dependencies change. It cleans up before re-running the effect or on unmount.
9. What is useContext and when would you use it?
The useContext Hook provides a way to consume values from a React Context. It allows you to pass data through the component tree without manually passing props down at every level (prop drilling). This is particularly useful for global data like themes, authenticated user information, or locale settings. useContext takes a Context object (created with React.createContext()) as an argument and returns the current value for that Context. It enables specific components to subscribe to context changes.
import React, { useContext, createContext } from 'react';
const ThemeContext = createContext('light'); // Default value
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext); // Consumes the context value
return <div style={{ background: theme === 'dark' ? '#333' : '#eee', color: theme === 'dark' ? 'white' : 'black' }}>The current theme is {theme}</div>;
}
10. When would you use useReducer over useState?
You would use useReducer over useState in scenarios involving complex state logic that involves multiple sub-values, or when the next state depends on the previous one. useReducer is also beneficial when state updates are derived from a common pattern (e.g., CRUD operations) or when you need to manage global state with a reducer function, similar to Redux. It provides a more structured and predictable way to manage state, making it easier to test and reason about, especially for larger applications or when state transitions are explicit actions.
const initialState = { count: 0 };
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 CounterWithReducer() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
11. How do you create and use a custom Hook? Provide an example.
A custom Hook is a JavaScript function whose name starts with "use" and that can call other Hooks. They allow you to extract reusable stateful logic from components, making it easy to share across different components without duplicating code. Custom Hooks promote cleaner code by separating concerns and improving readability.
Example: A custom hook to track window width.
import React, { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures this effect runs once on mount and cleans up on unmount
return width;
}
function MyResponsiveComponent() {
const width = useWindowWidth(); // Using the custom Hook
return <div>Window width: {width}px</div>;
}
12. Discuss different approaches to global state management in React.
Several approaches exist for global state management:
- React Context API +
useReducer: Suitable for moderately complex global state without external libraries. It avoids prop drilling but can lead to performance issues if the context value changes frequently and many components consume it. - Zustand/Jotai: Lightweight, performant, and often simpler alternatives to Redux. They offer fine-grained subscriptions and minimal boilerplate. Zustand uses a single store object, while Jotai focuses on atomic state.
- Recoil: Developed by Facebook, it provides a graph-based state management solution focusing on atoms and selectors, offering great flexibility and performance for large applications.
- Redux Toolkit: A popular, opinionated, and robust solution for large-scale applications, providing predictable state management with a centralized store, actions, and reducers. It comes with powerful debugging tools.
- Apollo Client / TanStack Query (React Query): Primarily for managing server-side state (caching, synchronization, background re-fetching). While not general-purpose global state managers, they often eliminate the need for one for async data.
Performance Optimization & Advanced Patterns
13. What are useMemo and useCallback, and when should you use them?
useMemo and useCallback are Hooks for memoization, designed to optimize performance by preventing unnecessary re-renders or re-calculations.
useMemo(callback, dependencies): Memoizes a value. It recalculates the value only when one of itsdependencieschanges. Use it to avoid expensive calculations on every render.useCallback(callback, dependencies): Memoizes a function. It returns a memoized version of the callback that only changes if one of thedependencieschanges. Use it to prevent unnecessary re-renders of child components that receive the function as a prop, especially withReact.memo.
import React, { useState, useMemo, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Memoized value: expensive calculation runs only when count changes
const expensiveValue = useMemo(() => {
console.log('Calculating expensive value...');
return count * 2;
}, [count]);
// Memoized function: changes only when count changes
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Expensive value: {expensiveValue}</p>
<button onClick={handleClick}>Increment Count</button>
<ChildComponent onClick={handleClick} /> {/* Child benefits from memoized function */}
</div>
);
}
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent re-rendered');
return <button onClick={onClick}>Child Button</button>;
});
14. Explain React.memo and its use case.
React.memo is a Higher-Order Component (HOC) used to optimize functional components by preventing unnecessary re-renders. It works by memoizing the component's rendered output. When a component wrapped with React.memo receives new props, React performs a shallow comparison of the new props with the previous props. If the props are shallowly equal, React skips rendering the component and reuses the last rendered result. This is beneficial for "pure" components that always render the same output given the same props.
// Before memo
function MyComponent(props) {
// ... render logic ...
}
// After memo
const MyMemoizedComponent = React.memo(MyComponent);
15. How do you implement code splitting and lazy loading in React?
Code splitting is a technique to break down a large JavaScript bundle into smaller, on-demand chunks. This helps improve initial page load performance by loading only the code necessary for the user's current view. In React, you implement it using React.lazy for component-level lazy loading and Suspense to display a fallback UI while the component is loading.
import React, { lazy, Suspense } from 'react';
// Lazily load the HeavyComponent
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading...</div>}>
{/* HeavyComponent will only load when rendered */}
<HeavyComponent />
</Suspense>
</div>
);
}
16. What are Error Boundaries and how do you use them?
Error Boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. In functional components, you typically create an Error Boundary as a class component (as Hooks don't provide equivalent functionality for componentDidCatch or getDerivedStateFromError for child errors).
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.error("Uncaught error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<MyProblematicComponent />
</ErrorBoundary>
);
}
17. Explain the concept of React Portals.
React Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This is useful for scenarios where you need to break out of the component's standard DOM stacking context, such as modals, tooltips, or notifications. While a portal's children are rendered elsewhere in the DOM, they still behave like normal React children, participating in the same component tree (e.g., event bubbling works as expected). You create a portal using ReactDOM.createPortal(child, domNode).
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '20px',
zIndex: 1000
}}>
{children}
</div>,
document.getElementById('modal-root') // This DOM node must exist in index.html
);
}
// In index.html: <div id="root"></div> <div id="modal-root"></div>
18. Compare Higher-Order Components (HOCs) and Custom Hooks.
| Feature | Higher-Order Components (HOCs) | Custom Hooks |
|---|---|---|
| Concept | Function that takes a component and returns a new component. | JavaScript function that uses other Hooks to extract stateful logic. |
| Composition | Wraps the original component, adding props/behavior. | Composes behavior within a functional component. |
| Prop Drilling | Can introduce prop collisions if not careful. | No prop drilling concerns, as logic is local to the consumer. |
| Render Tree | Adds an extra layer to the component tree. | Doesn't add extra layers to the component tree. |
| Readability | Can lead to "wrapper hell" with multiple HOCs. | Cleaner, more direct composition, especially with multiple Hooks. |
| Primary Use | Enhancing components with cross-cutting concerns (e.g., auth, logging). | Reusing stateful logic (e.g., form handling, data fetching). |
| Modern React | Still valid but less common; Hooks are generally preferred for new logic. | Preferred approach for logic reuse in functional components. |
Data Fetching, Routing & Build Tools
19. How do you handle data fetching in modern React applications?
Modern React applications typically leverage specialized libraries for data fetching and caching, often eliminating the need for complex global state management for server-side data.
- TanStack Query (React Query) / SWR: These libraries abstract away the complexities of data fetching, caching, revalidation, and error handling. They provide hooks like
useQueryoruseSWRthat manage the request lifecycle, keep data fresh, and handle background updates, focus revalidation, and pagination out-of-the-box. fetchAPI / Axios withuseEffect: For simpler cases or when you need more control, you can still useuseEffectwithfetchor Axios. This approach requires manual handling of loading states, errors, and caching.- React Server Components (RSCs): In frameworks like Next.js, RSCs allow you to fetch data directly on the server during the initial render, passing fully rendered UI components to the client, which can significantly improve performance and developer experience.
20. Explain Server-Side Rendering (SSR), Client-Side Rendering (CSR), and Static Site Generation (SSG) in the context of React frameworks.
These are different strategies for rendering React applications:
- Client-Side Rendering (CSR): The browser receives a minimal HTML file (usually just
<div id="root"></div>) and then fetches the JavaScript bundle. React then renders the entire UI on the client-side. Good for interactive SPAs, but can have slower initial load and worse SEO. - Server-Side Rendering (SSR): The server renders the initial HTML for the page, including the React components, and sends it to the browser. The browser then "hydrates" this static HTML with React, making it interactive. Improves initial load time and SEO. Frameworks like Next.js use this (
getServerSideProps). - Static Site Generation (SSG): Pages are pre-rendered at build time as static HTML files. These files are then served directly from a CDN, offering excellent performance and security. No server computation is needed per request. Ideal for content-heavy sites (blogs, marketing pages). Next.js uses this (
getStaticProps).
21. What are React Server Components (RSCs) and what problem do they solve?
React Server Components (RSCs) are a paradigm introduced to blur the lines between client and server rendering, offering a way to render components directly on the server and stream them to the client. They solve several problems:
- Bundle Size: Server components are never sent to the client, drastically reducing client-side JavaScript bundles.
- Performance: Data fetching can happen directly on the server, closer to the database, eliminating network waterfalls and improving initial load performance.
- Security: Sensitive logic or API keys can remain on the server without being exposed to the client.
- Developer Experience: Allows direct
async/awaitfor data fetching within components, simplifying data flow. RSCs enable a hybrid approach where you can interleave client components (for interactivity) with server components (for data fetching and static content).
22. Describe the typical routing setup in a React application using React Router DOM.
React Router DOM is the most popular library for declarative routing in React applications.
BrowserRouter: Uses the HTML5 history API (pushState,replaceState) to keep your UI in sync with the URL. It's typically wrapped around your entire application.Routes: A wrapper component that manages and renders the first child<Route>that matches the current URL.Route: Defines a path and the component to render when that path matches. It can have nested routes.Link/NavLink: Components for navigation, preventing full page reloads.NavLinkprovides styling capabilities for active links.useParams/useNavigate/useLocation: Hooks for accessing URL parameters, programmatically navigating, and getting current location info, respectively.
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users/123">User Profile</Link>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/users/:id" element={<UserProfile />} />
</Routes>
</BrowserRouter>
);
}
// UserProfile component might use useParams
import { useParams } from 'react-router-dom';
function UserProfile() {
const { id } = useParams();
return <h2>User ID: {id}</h2>;
}
23. What are the advantages of using Vite over Webpack for a React project?
Vite has gained significant popularity as a build tool for modern web projects, often preferred over Webpack due to its focus on speed and developer experience.
- Speed: Vite uses native ES modules for development, skipping the bundling step entirely during dev server startup. This results in lightning-fast cold starts and hot module replacement (HMR). Webpack, conversely, needs to bundle the entire application before serving.
- Simplicity: Vite has a much simpler configuration out of the box, often requiring zero configuration for common setups. Webpack configuration can be complex and verbose.
- Developer Experience: The rapid feedback loop from Vite's speed significantly improves the development workflow.
- Underlying Tools: Vite uses esbuild for bundling production builds (which is significantly faster than Webpack's JavaScript-based bundler) and Rollup for CSS/asset handling, whereas Webpack uses its own pipeline. While Webpack is powerful and mature, Vite offers a superior experience for many modern React projects.
24. Discuss the benefits of using a monorepo setup for large React applications.
A monorepo is a single repository containing multiple, distinct projects (e.g., several React applications, a shared component library, a backend API). Benefits include:
- Code Sharing: Easy to share code (e.g., UI components, utility functions, types) across projects without publishing packages, ensuring consistency and reducing duplication.
- Atomic Changes: A single pull request can update multiple dependent projects simultaneously, ensuring compatibility.
- Simplified Dependency Management: Common dependencies can be hoisted, reducing installation size and time.
- Unified Tooling: All projects can leverage a consistent set of linters, formatters, and build tools.
- Easier Refactoring: Large-scale refactors across multiple related projects become simpler and safer. Tools like Nx or Turborepo facilitate monorepo management by optimizing builds and caching.
Testing, Error Handling & Architecture
25. How do you approach testing React components? Mention common types of tests and tools.
Testing React components involves different levels:
- Unit Tests: Focus on testing individual functions, hooks, or small components in isolation. Tools: Jest (test runner), Vitest (fast, Vite-native alternative to Jest), React Testing Library (RTL) (for querying and interacting with components like a user).
- Integration Tests: Verify that several components or modules work correctly together. RTL is also excellent here, ensuring components integrate correctly within a larger UI.
- End-to-End (E2E) Tests: Simulate real user scenarios across the entire application, often in a browser. Tools: Cypress, Playwright.
- Snapshot Tests: Use Jest to capture a component's rendered output (or a data structure) and compare it against a saved snapshot. Useful for detecting unexpected UI changes.
// Example Unit/Integration test with RTL and Vitest
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import { Counter } from './Counter';
test('Counter increments count when button is clicked', async () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /count: 0/i });
expect(button).toBeInTheDocument();
await userEvent.click(button);
expect(screen.getByRole('button', { name: /count: 1/i })).toBeInTheDocument();
});
26. Describe best practices for structuring a large React application.
Structuring a large React application is crucial for maintainability and scalability.
- Feature-based Grouping: Organize files by feature rather than by type (e.g.,
features/auth/Login.jsx,features/auth/authSlice.jsinstead ofcomponents/Login.jsx,redux/authSlice.js). - Atomic Design Principles: Break UI into atoms (buttons, inputs), molecules (forms, search bars), organisms (headers, footers), templates, and pages.
- Shared/Common Folder: For truly generic utilities, hooks, or components used across many features (e.g.,
shared/components/Button.jsx,shared/hooks/useDebounce.js). - Clear Boundaries with APIs: Define clear interfaces for communication between different parts of the application (e.g.,
api/auth.js,api/products.js). - Configuration & Constants: Centralize environment variables, constants, and theme definitions.
- Routing: Define routes clearly, potentially nested within feature modules.
- Testing Folder Structure: Place tests alongside the code they test (e.g.,
Button.test.jsxnext toButton.jsx).
27. How do you handle errors gracefully in a React application beyond Error Boundaries?
Error Boundaries catch errors in the component tree, but other strategies are needed for different error types:
- Asynchronous Errors: For errors in
Promiserejections (e.g., data fetching withfetchor Axios), usetry...catchblocks withinasyncfunctions or.catch()with Promises. Modern data fetching libraries (TanStack Query, SWR) provide dedicated error states and handlers. - Event Handlers: Errors within event handlers (e.g.,
onClick) don't propagate to Error Boundaries. Implementtry...catchdirectly in these handlers or wrap them with a higher-order function that handles errors. - Server Errors: Display user-friendly messages for API failures. Use HTTP status codes and API error messages to inform the user.
- Global Error Logging: Integrate with error reporting services (Sentry, Bugsnag) to log errors that occur in production, often including details from
componentDidCatch(for Error Boundaries) ortry...catchblocks. - User Feedback: Provide visual cues (toasts, alerts, inline messages) to users when an error occurs, guiding them on what to do next.
Scenario & System Design
28. Design a component structure for a complex data table with features like sorting, filtering, pagination, and inline editing.
A robust data table requires a clear separation of concerns.
DataTableContainer(Smart Component):- Manages table-level state:
data,loading,error,currentPage,pageSize,sortColumn,sortDirection,filters. - Handles data fetching (using a hook like
useQueryoruseEffect). - Implements
useReduceror a custom hook for complex state logic (sorting, filtering, pagination). - Passes derived data and callback functions down to
DataTableas props.
- Manages table-level state:
DataTable(Presentational Component):- Receives
columns,rows,onSort,onFilterChange,onPageChange,onEditRowas props. - Renders
TableHeader,TableBody,TablePagination. - Doesn't manage its own data or complex logic.
- Receives
TableHeader: Renders column headers. TriggersonSortandonFilterChangecallbacks.TableBody: Renders individualTableRowcomponents, potentially withReact.memofor performance.TableRow: Renders cells. HandlesonEditRowactions for inline editing, managing its own temporary edit state.TablePagination: Renders page controls. TriggersonPageChange.FilterComponent(Reusable): Separate component for each filter type (text input, select dropdown).LoadingSpinner,ErrorMessage: Rendered byDataTableContainerbased on loading/error state.
This structure allows each component to focus on its specific responsibility, promoting reusability and testability.
29. You've identified a React component that is rendering too frequently and causing performance issues. How would you debug and optimize it?
Debugging:
- React Developer Tools Profiler: Use the "Profiler" tab to record renders and identify which components are re-rendering and how long they take. Look for unexpected re-renders of child components when their props haven't visually changed.
why-did-you-render(library): A powerful library that alerts you in the console about unnecessary re-renders, explaining why a component re-rendered (e.g., props changed, state changed, context changed).- Console Logs: Strategically place
console.logstatements in render functions oruseEffectto track render cycles and state changes.
Optimization Strategies:
React.memo: Wrap pure functional components that don't need to re-render if their props are shallowly equal.useMemo: Memoize expensive calculations or objects/arrays passed as props to child components, so they don't trigger unnecessary re-renders.jsxconst memoizedData = useMemo(() => processData(data), [data]);useCallback: Memoize event handlers or functions passed as props to child components, preventing the child from re-rendering due to a new function reference on every parent render.jsxconst memoizedClickHandler = useCallback(() => handleClick(), [dependencies]);- State Colocation: Keep state as close as possible to where it's used. Lifting state up too high can cause many unrelated components to re-render.
- List Optimization: Ensure
keyprops are stable and unique for list items. - Virtualization (Windowing): For very long lists, use libraries like
react-windoworreact-virtualizedto render only the visible items, dramatically improving performance. - Debouncing/Throttling: For frequently firing events (e.g.,
onChangeon input,onScroll), debounce or throttle the event handlers to limit their execution rate. - Context Optimization: If using Context, ensure the context provider's value changes only when necessary, or consider splitting large contexts into smaller, more focused ones to minimize consumer re-renders.
30. How would you choose a state management solution (e.g., Context API, Redux Toolkit, Zustand, TanStack Query) for a new, large-scale React application?
Choosing a state management solution depends on several factors:
-
Nature of State:
- Server State (async data): For data fetching, caching, and synchronization with APIs, TanStack Query or SWR are highly recommended. They handle much of the complexity (
loading,error,stale-while-revalidate, retries) out-of-the-box. - Client State (UI state, global preferences):
- Local Component State (
useState,useReducer): Always start here. If state is only needed by one component, keep it local. - React Context API +
useReducer: Good for infrequent global updates (e.g., theme, auth status) or when prop drilling becomes cumbersome, but can lead to performance issues if many components consume a frequently changing context. - Zustand/Jotai/Recoil: Excellent for shared client state that changes frequently or needs atomic updates. They are lightweight, performant, and offer fine-grained subscriptions without boilerplate. Often a good modern default for complex client state.
- Redux Toolkit: Best for very large, complex applications with highly critical, predictable state interactions, strict update patterns, and a need for powerful debugging tools and middleware. It has a steeper learning curve but offers immense control.
- Local Component State (
- Server State (async data): For data fetching, caching, and synchronization with APIs, TanStack Query or SWR are highly recommended. They handle much of the complexity (
-
Application Scale and Complexity:
- Small/Medium (simple global state): Context API or local state combined with custom hooks.
- Medium/Large (complex global state): Zustand/Jotai or Recoil offer a great balance of power and simplicity.
- Very Large (enterprise, critical predictability): Redux Toolkit.
-
Team Familiarity: Choose a solution that your team is comfortable with to ensure productivity and maintainability.
-
Performance Needs: Lightweight solutions like Zustand/Jotai and dedicated server-state libraries often offer better out-of-the-box performance than broad, general-purpose state managers, especially for frequent updates.
A common modern stack combines TanStack Query for server state with Zustand/Jotai for client-side UI state.
Quick revision table
| Topic | One-line answer | Common follow-up |
|---|---|---|
| Virtual DOM | In-memory representation of UI, enabling efficient DOM updates via reconciliation. | How does it compare to shadow DOM? |
| Props vs State | Props pass data top-down (immutable); State manages internal component data (mutable). | When to lift state up vs. pass props down? |
useEffect | Performs side effects; dependency array controls re-execution (mount, update, unmount). | How to handle race conditions in useEffect for data fetching? |
useContext | Consumes data from a Context without prop drilling for global-like data. | What are its performance implications for frequently changing values? |
| Custom Hooks | Reusable functions encapsulating stateful logic, starting with use. | How do custom hooks differ from HOCs? |
useMemo/useCallback | useMemo memoizes values, useCallback memoizes functions to prevent unnecessary re-renders. | When are they harmful or lead to more complexity than benefit? |
| Code Splitting | Loading only necessary code chunks on demand with lazy and Suspense. | How does it affect SEO? |
| Error Boundaries | Catch JS errors in child component trees, display fallback UI, and log errors. | Can they catch errors in event handlers or useEffect? |
| React Portals | Render children into a different DOM node, breaking out of parent's DOM hierarchy. | How do events bubble from a portal? |
| React Server Components | Components rendered on the server, reducing client bundle size and improving performance. | What's the difference between SSR and RSCs? |
| Data Fetching | Libraries like TanStack Query/SWR handle caching, revalidation, and loading states efficiently. | How do you handle mutations with these libraries? |
| Testing | Unit, integration, E2E tests using Jest/Vitest and React Testing Library. | How do you test asynchronous components or custom hooks? |
| Monorepos | Single repository for multiple projects, facilitating code sharing and atomic changes. | What tools do you use for monorepo management (e.g., Nx, Turborepo)? |
How to prepare
- Deep Dive into Hooks: Understand the core Hooks (
useState,useEffect,useContext,useReducer,useRef,useCallback,useMemo) thoroughly. Practice implementing common patterns with them. - Modern React Ecosystem: Familiarize yourself with current best practices for data fetching (TanStack Query/SWR), state management (Zustand, Jotai), and build tools (Vite).
- Performance Optimization: Learn and apply techniques like
React.memo,useCallback,useMemo, code splitting, and virtualization. Be ready to explain when and why to use them. - Framework Awareness (Next.js/Remix): Understand concepts like SSR, SSG, and especially React Server Components, even if the role isn't explicitly for a framework-specific developer. These are becoming core to the React ecosystem.
- Practice Problem Solving: Work through common algorithm and data structure problems, but also be prepared to whiteboard component designs, optimize existing code, and debug performance issues. Articulate your thought process clearly.
