Use React.memo for pure functional components.
Use useMemo and useCallback to memoize expensive values or functions.
Key optimization: Use correct keys in lists and avoid anonymous functions in JSX when possible.
React.memo: Wraps a component to prevent re-render unless props change.
useMemo: Caches the result of a function inside the component. Can be used for expensive calculations
const MyComponent=React.memo(({data})=>{
return <div>this is {data}</div>
});
This will only change when data props change.
const filteredData=useMemo(()=>{
return data.filter(item=>item.isActive);
},[data]);
For expensive calculations
For moderately complex state, we can use multiple useState hooks.
For more complex scenarios (e.g., interdependent states), prefer useReducer or external libraries like Zustand or Redux.
Store tokens in localStorage or httpOnly cookies.
Use a global state (like Context or Redux) to track auth state.
Protect routes using HOCs or conditional rendering.
Infinite loops caused by incorrect dependency arrays.
Not cleaning up, side effects properly.
Solution: Always define dependencies accurately, and clean up using the return function.
useEffect(() => {
const timer = setInterval(doSomething, 1000);
return () => clearInterval(timer);
}, []);
Prop drilling happens when you pass data through multiple layers of components unnecessarily.
Solutions:
Use React Context API for medium complexity.
For large apps, use a state management library like Redux, Jotai, or Zustand.
Avoid updating state on unmounted components.
Use cleanup functions.
Cancel async operations when the component unmounts (e.g., AbortController for fetch).
useEffect(()=>{
var controller=new AbortController();
signal=controller.signal;
const fetchData=async()=>{
const response=await fetch(url,{...options,signal});
const result=await response.json();
};
return ()=>controller.abort;
},[]);
Use React DevTools Profiler.
Identify re-renders using console logs or profiler.
Optimize render logic, split components, memoize props.
Here's how it works step-by-step:
Single Listener:
When your React app loads, React attaches just one event listener for each event type (like click, keydown, etc.) at the root (or document level).
Event Bubbling:
When you interact with a button or any element, the event bubbles up through the DOM to the root.
Synthetic Events:
React wraps the native browser event inside its own SyntheticEvent system —
a lightweight, cross-browser wrapper — giving a consistent API across all browsers.
Dispatching:
React figures out which component the event originated from, and calls your event handler (like onClick) properly.
Efficiency:
Because React doesn't add new real DOM event listeners for each component, it's much faster and uses less memory — even if your app has thousands of elements!
When to Use it?
useEffect ? when you don't need to block the browser (default).
useLayoutEffect ? when you need to measure or manipulate DOM immediately before the user sees anything.
Always prefer useEffect first.
Only use useLayoutEffect if you notice a flicker or need to measure/adjust layout before paint.
Because useLayoutEffect can block rendering and cause performance issues if used unnecessarily.
Controlled components are those whose form data is handled by the React component's state.
Uncontrolled components store their data in the DOM itself.
Controlled Example:
function ControlledInput() {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
Uncontrolled Example:
function UncontrolledInput() {
const inputRef = useRef(null);
const handleClick = () => alert(inputRef.current.value);
return <>
<input ref={inputRef} />
<button onClick={handleClick}>Show Value</button>
</>;
}
Using error boundaries, we can handle the errors in react.
Example:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
// Log error
}
render() {
if (this.state.hasError) return <h1>Something went wrong.</h1>;
return this.props.children;
}
}
Use React.memo to prevent unnecessary re-renders.
Use useMemo and useCallback for expensive calculations or function definitions.
Lazy load components with React.lazy.
Use code splitting with dynamic import().
Prop drilling is passing data through many nested components. To avoid it we can use the following.
Use Context API or a global store like Redux to store the auth state, then use ProtectedRoute components to guard routes.
Example:
function ProtectedRoute({ children }) {
const auth = useContext(AuthContext);
return auth.isAuthenticated ? children : <Navigate to="/login" />;
}
Reconciliation is the process by which React updates the DOM. When state changes, React builds a virtual DOM tree and compares it with the previous tree (diffing). Only the changes are applied to the real DOM.
How does it work?
React is declarative and component-based, using a Virtual DOM for faster updates, while jQuery is imperative and manually updates the real DOM directly. React scales better for large apps because it abstracts away manual DOM handling.
Keys help React identify which items changed, are added, or removed, and enhance performance by minimizing DOM operations.
It helps React track which items changed, added, or removed during re-rendering.
With keys, React can efficiently update only the items that actually changed — instead of re-rendering the entire list.
Without a proper key, React cannot reliably identify
This can cause
React Fiber is a reimplementation of React's core reconciliation algorithm, introduced in React 16. It aims to improve performance, especially for complex applications, by enabling incremental rendering. Instead of performing updates in one go, Fiber breaks down rendering into smaller, manageable units of work. This allows React to pause, prioritize, and resume work as needed, leading to smoother user experiences.
The following are the key features of React fiber.
Incremental Rendering:
It splits rendering work into chunks, allowing React to pause and resume, ensuring smoother UIs.
Prioritization:
Fiber enables React to prioritize updates, giving precedence to urgent tasks like user interactions.
Concurrency:
It lays the groundwork for concurrent mode, allowing React to handle multiple updates simultaneously without blocking the main thread.
Work Loop:
Fiber introduces a work loop that processes units of work in a cooperative scheduling manner, allowing pausing and resuming work.
React Fiber represents the nodes of the DOM tree as fiber nodes, linked together to form a fiber tree. This structure mirrors the component tree and facilitates efficient updates.
import { useEffect } from 'react';
function useComponentDidMount(callback) {
useEffect(() => {
callback();
// Empty dependency array ensures this runs once
}, []);
}
// Usage
useComponentDidMount(() => {
console.log('Component mounted');
});
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
}
Usage:
const debouncedSearch = useDebounce(searchQuery, 500);
This can be used in search boxes, filters, and resizing to reduce function calls.
React itself doesn't inherently prevent memory leaks, but it provides tools and patterns to manage component lifecycles effectively, which can help avoid them. Memory leaks in React applications typically occur when resources are not properly released when components unmount.
To prevent memory leaks, we should follow the best practices below.
Use the Cleanup Function in useEffect:
This function allows you to execute code when a component unmounts, which can be used to clear timers, unsubscribe from events, and cancel API requests.
Clear Timers and Intervals:
Always clear timers and intervals using clearTimeout and clearInterval within the cleanup function of useEffect or in the componentWillUnmount lifecycle method (for class components).
Cancel API Requests:
Use an AbortController to cancel ongoing API requests when a component unmounts.
Remove Event Listeners:
Remove event listeners using removeEventListener within the cleanup function of useEffect or in the componentWillUnmount lifecycle method.
Avoid Global References:
Be mindful of storing references in global objects or contexts that are not cleaned up properly.
Use Strict Mode:
Enable strict mode in your JavaScript code to help catch potential issues that could lead to memory leaks.
Example:
useEffect(() => {
const controller = new AbortController();
fetch('url', { signal: controller.signal });
return () => controller.abort(); // Cleanup
}, []);
React batching is a mechanism that groups multiple state updates into a single re-render to optimize performance and user experience. This reduces unnecessary DOM manipulations, leading to smoother UI updates and faster loading times. React 18 introduced automatic batching, which extends batching to various scenarios like setTimeout, promises, and native event handlers, further enhancing performance.
What it does:
React's batching mechanism prevents individual state updates from triggering immediate re-renders.
Instead, it groups these updates and applies them as a single batch, ensuring only one re-render occurs for all grouped updates.
This optimization is particularly useful in event handlers and lifecycle methods where multiple state changes might occur quickly.
Why it's important:
Improved performance:
By reducing the number of re-renders, React applications become more efficient and responsive.
Smoother UI:
Fewer DOM manipulations lead to a more fluid and less choppy user experience.
Simplified development:
Developers can write more intuitive code without having to manually optimize for batching.
Concurrent Mode in React is a set of new features that help React apps stay responsive even when rendering large components or handling complex updates. It allows React to interrupt rendering tasks, work on high-priority updates first, and resume or abandon less critical tasks as needed. This makes the user interface feel smoother and more responsive, especially during time-consuming rendering operations.
Key Features and Concepts
Interruptible Rendering:
React can pause rendering to handle other events, like user input, ensuring the UI remains responsive.
Prioritizing Updates:
React can prioritize updates, processing important ones first and deferring less urgent ones.
Time Slicing:
Rendering work is broken into smaller chunks, allowing React to pause and resume rendering without blocking the main thread.
Suspense:
Components can "suspend" rendering while waiting for data, displaying a fallback UI until the data is ready.
Transitions:
Help differentiate between urgent and non-urgent updates, ensuring the UI remains responsive during transitions.
Automatic Batching:
Multiple state updates are grouped into a single re-render, improving performance.
Benefits of concurrent mode
Improved User Experience:
Smoother and more responsive UI, especially during complex rendering tasks.
Better Performance:
Efficient handling of updates and rendering, leading to faster initial render times.
Enhanced Error Handling:
Improved error boundaries for handling errors during rendering, suspending, or resuming components.
How it Works?
Concurrent Mode introduces a new rendering mechanism that allows React to work on multiple tasks concurrently. It does not mean that state updates are executed in parallel, but rather that they can overlap, with React prioritizing tasks based on their urgency.
import React, { useState, useTransition } from 'react';
function MyComponent() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
const handleClick = () => {
startTransition(() => {
setCount(c => c + 1);
});
};
return (
<div>
{isPending && <p>Updating...</p>}
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
In this example, useTransition is used to mark the state update as non-urgent. If other urgent updates occur while this update is being processed, React can prioritize them, ensuring the UI remains responsive.
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
It improves initial load time. Use when components are large or used conditionally.
For large apps, we can use
React throws an error:
"Rendered more hooks than during the previous render."
You should not update state inside render because it causes infinite re-renders:
Wrong:
const MyComponent = () => {
const [val, setVal] = useState(0);
setVal(1); // Bad
return <div>{val}</div>;
};
Instead of this, we should use useEffect.
useImperativeHandle is a React Hook that lets you customize the handle exposed as a ref.
import { useImperativeHandle, forwardRef, useRef } from 'react';
const Input = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}));
return <input ref={inputRef} />;
});
function Parent() {
const inputRef = useRef();
return <>
<Input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>;
}
Used when Parent needs to call child’s methods (imperative control).
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (e) => {
if (!ref.current || ref.current.contains(e.target)) return;
handler(e);
};
document.addEventListener("mousedown", listener);
return () => document.removeEventListener("mousedown", listener);
}, [ref, handler]);
}
It can be used with dropdowns, modals, tooltips, etc.