useState and useReducer are both used to manage component state, but they have distinct use cases.
useState is simpler and ideal for managing individual pieces of state. It's great for simple state management scenarios.
useReducer is more appropriate for complex state logic, such as when state depends on previous states or if you need to handle more complex state transitions. It’s a better choice for managing multiple related states or performing complex updates.
Example use case for useReducer:
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:
return state;
}
}
const Component = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
};
useReducer is ideal when the logic becomes more complex, such as in forms with multiple fields or managing nested states.
Context API is a way to manage state globally in React without passing props through every level of the component tree. It provides a mechanism to share state across the entire app (or a part of it).
Context is ideal for global state such as user authentication, themes, language preferences, or other data that needs to be accessed by multiple components across the app.
Example:
const ThemeContext = React.createContext('light');
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemedComponent = () => {
const { theme, setTheme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
</div>
);
};
// Wrap your app with the provider
const App = () => (
<ThemeProvider>
<ThemedComponent />
</ThemeProvider>
);
Context is not a replacement for state management libraries like Redux but is useful for scenarios where you need lightweight global state management.
Higher-order components (HOCs) are an advanced technique in React for reusing component logic. An HOC is a function that takes a component as an argument and returns a new, enhanced component. It's a pattern that emerges from React's compositional nature, allowing developers to share behavior between components without duplicating code. HOCs do not modify the original component passed into them. Instead, they return a new component with the desired functionality.
const withLog = (WrappedComponent) => {
return (props) => {
console.log("Component WrappedComponent.name is rendering");
return <WrappedComponent {...props} />;
};
};
const MyComponent = () => {
return <div>My Component</div>;
};
const MyComponentWithLog = withLog(MyComponent);
In this example, withLog is the HOC. It takes MyComponent and returns a new component that logs a message to the console before rendering MyComponent. MyComponentWithLog now has this enhanced functionality.
HOCs are useful for various tasks, including:
Authentication: Wrapping components to check if a user is logged in.
Authorization: Controlling access to components based on user roles.
Data fetching: Providing data to components.
State management: Sharing stateful logic.
Styling and theming: Applying consistent styles.
Logging and error handling: Adding cross-cutting concerns.
For large-scale React applications, it’s essential to focus on modularity and separation of concerns. The key is to organize the code into clearly defined modules or features.
Below are some practices we should follow.
Component-based Structure: Use atomic design principles—breaking UI into small, reusable components.
Folder Structure: Organize the app by features (rather than types) for better scalability and easier navigation, e.g., src/features/auth, src/features/products.
State Management: Use Context API or Redux for global state management, ensuring that state is consistent and easy to manage across different components.
Code Splitting: Use React.lazy() and Suspense for lazy loading of components to improve initial load time.
Performance Optimization: Use memoization (e.g., React.memo, useMemo) to prevent unnecessary re-renders and improve performance.
Error Boundaries are React components that catch JavaScript errors in their child components, log those errors, and display a fallback UI instead of crashing the app.
Example:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.log('Error caught in boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
const BrokenComponent = () => {
throw new Error('Error in component!');
return <div>Broken Component</div>;
};
const App = () => (
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
);
Error boundaries should be used at a component or page level to prevent the entire app from crashing due to a single error.
We can follow the techniques below:
Memoization: Use React.memo to prevent unnecessary re-renders of functional components. This is helpful when the component props don't change often.
const MemoizedComponent = React.memo(MyComponent);
Lazy Loading: Implement code splitting using React.lazy() and Suspense for components that aren’t needed immediately, which reduces the initial loading time.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
Avoid Re-renders: Use useMemo to memoize expensive computations or derived data. This can help prevent recalculations on every render.
const computedValue = useMemo(() => expensiveCalculation(input), [input]);
Virtualization: For rendering long lists, use react-window or react-virtualized to render only the visible items in the viewport, improving performance in lists with many items.
We can follow the techniques below:
Memoization: Use React.memo to prevent unnecessary re-renders of functional components. This is helpful when the component props don't change often.
const MemoizedComponent = React.memo(MyComponent);
Lazy Loading: Implement code splitting using React.lazy() and Suspense for components that aren’t needed immediately, which reduces the initial loading time.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
Avoid Re-renders: Use useMemo to memoize expensive computations or derived data. This can help prevent recalculations on every render.
const computedValue = useMemo(() => expensiveCalculation(input), [input]);
Virtualization: For rendering long lists, use react-window or react-virtualized to render only the visible items in the viewport, improving performance in lists with many items.
In React, controlled and uncontrolled components represent two distinct ways of handling form data. Controlled components use React's state to manage form data, while uncontrolled components rely on the DOM to handle it. This difference primarily affects how data is tracked and updated within the component.
Uncontrolled Components
Uncontrolled Components are the components that do not rely on the React state and are handled by the DOM. So, in order to access any value that has been entered, we take the help of refs.
import React, { useRef } from "react";
function App() {
const inputRef = useRef(null);
function handleSubmit() {
console.log(`Name: ${inputRef.current.value}`);
}
return (
<div className="App">
<form onSubmit={handleSubmit}>
<label>Name :</label>
<input
type="text"
name="name"
ref={inputRef}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
export default App;
Controlled Components
In React, Controlled Components are those in which form’s data is handled by the component’s state. It takes its current value through props and makes changes through callbacks like onClick, onChange, etc. A parent component manages its own state and passes the new values as props to the controlled component.
import { useState } from "react";
function App() {
const [name, setName] = useState("");
function handleSubmit() {
console.log(`Name: ${name}`);
}
return (
<div className="App">
<form onSubmit={handleSubmit}>
<label>Name:</label>
<input
name="name"
value={name}
onChange={(e) =>
setName(e.target.value)
}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
export default App;
Controlled components are preferred in complex forms for validation and dynamic updates.
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.
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.
React re-renders a component when its state or props change. However, frequent re-renders can impact the app’s performance, especially for large applications. To optimize re-renders, we can use the following strategies:
React.memo: Wrap functional components with React.memo to memoize the component’s output and avoid re-renders if the props haven’t changed.
const MemoizedComponent = React.memo(MyComponent);
useMemo: Use useMemo to memoize expensive computations, preventing them from being recalculated on every render.
const computedValue = useMemo(() => expensiveComputation(input), [input]);
useCallback: Use useCallback to memoize callback functions so they don’t change on every render.
const memoizedCallback = useCallback(() => { doSomething() }, []);
PureComponent: For class components, use React.PureComponent instead of React.Component, as it only re-renders when props or state change.
class MyComponent extends React.PureComponent { ... }
Virtualization: For large lists or tables, use react-window or react-virtualized to render only the visible items in the DOM.
React Context is used to pass data through the component tree without having to pass props manually at every level. It’s particularly useful for managing global state or sharing data across deeply nested components.
Steps to Use Context API:
Create a Context: Use React.createContext() to create a context that will hold the state and provide it to components.
const MyContext = React.createContext();
Provider Component: Wrap your application (or part of it) with a Provider component, passing the value you want to share as a prop.
const MyProvider = ({ children }) => {
const [state, setState] = useState("Initial State");
return (
<MyContext.Provider value={{ state, setState }}>
{children}
</MyContext.Provider>
);
};
Consumer Component: Use the useContext hook to consume the context value in any functional component.
const MyComponent = () => {
const { state, setState } = useContext(MyContext);
return (
<div>
<p>{state}</p>
<button onClick={() => setState("Updated State")}>Update</button>
</div>
);
};
Use Case: The Context API is often used for theming, user authentication, and language preferences where the same data is required by many components.
In React, event handling is based on the synthetic event system. This system is a wrapper around the browser’s native event handling mechanism, providing consistency across different browsers.
Event Binding: React binds events to the root DOM, meaning events are not directly attached to individual elements. Instead, React uses event delegation to handle all events through a single listener.
Synthetic Events: React’s SyntheticEvent is normalized across browsers, ensuring consistent behavior. It wraps around the native events like click, focus, etc.
const handleClick = (event) => {
console.log(event.type); // Click
};
return <button onClick={handleClick}>Click Me</button>;
Event Delegation: React uses event delegation by attaching a single listener to the root DOM node (document). This reduces memory consumption and improves performance.
Example: When you click on a child element, React handles it through its delegated event system and invokes the appropriate handler.
React.memo is a higher-order component (HOC) used to memoize functional components to avoid unnecessary re-renders. It works by shallowly comparing the props of the component and re-rendering only if the props have changed. It's especially useful for optimizing child components that receive props from their parent.
const MyComponent = React.memo(({ name }) => {
console.log("Component re-rendered");
return <div>{name}</div>;
});
useMemo is a hook that memoizes the result of a function. It helps optimize expensive calculations or derived values that don’t need to be recomputed every time the component re-renders.
const result = useMemo(() => expensiveComputation(input), [input]);
Difference:
React.memo memoizes the entire component, preventing re-renders based on prop changes.
useMemo memoizes values or calculations inside a component.
Code splitting is the practice of splitting your application’s bundle into smaller chunks that can be loaded on demand. This helps in reducing the initial load time of the app, as only the necessary code is loaded first, while the rest of the code is loaded as needed.
React provides several methods for code splitting:
React.lazy() and Suspense:
React.lazy() allows you to dynamically import components, while Suspense handles loading states until the component is ready.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
React Router:
React Router supports code splitting by lazily loading route components.
Dynamic Imports:
Using import() syntax for dynamic imports allows you to split the code at a finer level of granularity.
Benefits:
Class components have a set of lifecycle methods that can be used to control the behavior of components at various stages of their lifecycle:
componentDidMount: Called after the component is mounted (rendered to the screen). It’s typically used for fetching data.
componentDidMount() {
console.log('Component mounted');
}
componentDidUpdate: Called after the component updates (e.g., after receiving new props or state).
componentDidUpdate(prevProps, prevState) {
if (this.state.someValue !== prevState.someValue) {
// Handle state change
}
}
componentWillUnmount: Called before the component is unmounted from the DOM, useful for cleanup tasks like removing event listeners.
shouldComponentUpdate: Used to decide whether a component should re-render. It’s used for performance optimization.
There are two types of form components in React:
Controlled Components:
React controls the form input via the state, ensuring that the state is always in sync with the input value.
const ControlledForm = () => {
const [inputValue, setInputValue] = useState('');
const handleChange = (e) => {
setInputValue(e.target.value);
};
return (
<form>
<input type="text" value={inputValue} onChange={handleChange} />
</form>
);
};
Uncontrolled Components:
React does not directly control the input; instead, you use the ref to interact with the DOM element.
const UncontrolledForm = () => {
const inputRef = useRef(null);
const handleSubmit = () => {
alert(inputRef.current.value);
};
return (
<form>
<input ref={inputRef} type="text" />
<button type="button" onClick={handleSubmit}>Submit</button>
</form>
);
};
When to use which?
Controlled Components: Use when you need fine-grained control over form inputs (e.g., validation, dynamic updates).
Uncontrolled Components: Use when you don’t need to track the form state frequently.
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.
In React, controlled and uncontrolled components represent two distinct ways of handling form data. Controlled components use React's state to manage form data, while uncontrolled components rely on the DOM to handle it. This difference primarily affects how data is tracked and updated within the component.
Uncontrolled Components
Uncontrolled Components are the components that do not rely on the React state and are handled by the DOM. So, in order to access any value that has been entered, we take the help of refs.
import React, { useRef } from "react";
function App() {
const inputRef = useRef(null);
function handleSubmit() {
console.log(`Name: ${inputRef.current.value}`);
}
return (
<div className="App">
<form onSubmit={handleSubmit}>
<label>Name :</label>
<input
type="text"
name="name"
ref={inputRef}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
export default App;
Controlled Components
In React, Controlled Components are those in which form’s data is handled by the component’s state. It takes its current value through props and makes changes through callbacks like onClick, onChange, etc. A parent component manages its own state and passes the new values as props to the controlled component.
import { useState } from "react";
function App() {
const [name, setName] = useState("");
function handleSubmit() {
console.log(`Name: ${name}`);
}
return (
<div className="App">
<form onSubmit={handleSubmit}>
<label>Name:</label>
<input
name="name"
value={name}
onChange={(e) =>
setName(e.target.value)
}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
export default App;
Controlled components are preferred in complex forms for validation and dynamic updates.
We can follow the techniques below:
Memoization: Use React.memo to prevent unnecessary re-renders of functional components. This is helpful when the component props don't change often.
const MemoizedComponent = React.memo(MyComponent);
Lazy Loading: Implement code splitting using React.lazy() and Suspense for components that aren’t needed immediately, which reduces the initial loading time.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
Avoid Re-renders: Use useMemo to memoize expensive computations or derived data. This can help prevent recalculations on every render.
const computedValue = useMemo(() => expensiveCalculation(input), [input]);
Virtualization: For rendering long lists, use react-window or react-virtualized to render only the visible items in the viewport, improving performance in lists with many items.
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.
Code splitting is the practice of splitting your application’s bundle into smaller chunks that can be loaded on demand. This helps in reducing the initial load time of the app, as only the necessary code is loaded first, while the rest of the code is loaded as needed.
React provides several methods for code splitting:
React.lazy() and Suspense:
React.lazy() allows you to dynamically import components, while Suspense handles loading states until the component is ready.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
React Router:
React Router supports code splitting by lazily loading route components.
Dynamic Imports:
Using import() syntax for dynamic imports allows you to split the code at a finer level of granularity.
Benefits:
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.
To implement micro-frontend architecture in a React app, you'll break down your application into smaller, self-contained modules, or micro-frontends, which can then be deployed and updated independently. This involves creating separate React apps for each micro-frontend, integrating them into a host application using techniques like Module Federation or iframe-based integration, and managing communication and styling consistency between them.
Server-Side Rendering (SSR) in React allows content to be rendered on the server before being sent to the client, which can provide faster load times and improved SEO.
Steps for SSR:
Set up a Node.js Server: Use express or any other Node.js server to handle server-side rendering.
ReactDOMServer: Use the ReactDOMServer.renderToString() method to render your React components to HTML on the server.
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
app.get('*', (req, res) => {
const content = ReactDOMServer.renderToString(<App />);
res.send('
<html>
<body>
<div id="root">content</div>
</body>
</html>
');
});
Hydration: On the client side, you will "hydrate" the HTML content rendered on the server using ReactDOM.hydrate() to make it interactive.
import ReactDOM from 'react-dom';
ReactDOM.hydrate(<App />, document.getElementById('root'));
Trade-offs of SSR vs CSR:
Advantages:
Faster initial page load: Server pre-renders HTML, so content is visible to users sooner.
Improved SEO: Search engines can crawl the fully rendered page.
Disadvantages:
Server Load: The server must render the page for each request, leading to higher CPU usage.
Complexity: SSR setup is more complex than CSR, requiring additional tooling (like Webpack, Babel, etc.).
Slower Interactivity: After initial HTML load, JavaScript still needs to be downloaded and executed, making the app interactive.
React Suspense is a feature in React that lets you manage asynchronous data fetching in a more declarative way. It allows you to pause rendering until a component has finished loading its resources (e.g., fetching data or loading code).
Suspense with a lazy-loaded component:
const MyComponent = React.lazy(() => import('./MyComponent'));
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
Suspense for Data Fetching:
Starting from React 18, Suspense can be used to manage data fetching in addition to lazy-loaded components. You can use React Query or other similar libraries to integrate Suspense for data fetching.
const { data, isLoading } = useQuery('fetchData', fetchData);
if (isLoading) {
return <Suspense fallback={<div>Loading...</div>} />;
}
return <div>{data}</div>;
Integrating with Redux or Context API:
When combining Suspense with a state management solution like Redux or Context API, you can suspend data fetching in higher-order components and show loading states until the data is ready. For global state management, store the fetched data in the global state and propagate it across your app.
Dispatch actions to fetch data and store it in the Redux store.
Wrap components with Suspense and render them once the state is available.
import { useDispatch, useSelector } from 'react-redux';
const MyComponent = () => {
const dispatch = useDispatch();
const data = useSelector(state => state.data);
if (!data) {
dispatch(fetchData()); // Trigger data fetching
return <div>Loading...</div>;
}
return <div>{data}</div>;
};
Context API:
Use Context to provide fetched data and allow child components to access it once it's available.
const DataContext = React.createContext();
const DataProvider = ({ children }) => {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return (
<DataContext.Provider value={data}>
{children}
</DataContext.Provider>
);
};
const MyComponent = () => {
const data = useContext(DataContext);
if (!data) return <div>Loading...</div>;
return <div>{data}</div>;
};
Custom hooks are a powerful way to extract and reuse logic in React. They allow you to encapsulate complex logic or state management in a reusable function, making components cleaner and easier to maintain.
Custom Hook for LocalStorage Synchronization:
The following custom hook stores a value in localStorage and synchronizes it with the state.
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
// Get from localStorage or use the initial value
const storedValue = localStorage.getItem(key);
const initial = storedValue ? JSON.parse(storedValue) : initialValue;
const [value, setValue] = useState(initial);
// Update localStorage when state changes
const setStoredValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue];
}
export default useLocalStorage;
Usage:
const MyComponent = () => {
const [name, setName] = useLocalStorage('name', 'John Doe');
return (
<div>
<h1>Hello, {name}</h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
};
Profiling Tools:
Common Fixes:
Chunk-based preloading enhances TTI by strategically loading code chunks. It involves splitting the application code into smaller, manageable chunks and preloading those that are essential for initial rendering and user interaction.
Implementation steps include:
Code Splitting:
Use dynamic imports or tools like Webpack to split the application into chunks. Route-based splitting is common, where each route or major section of the app becomes a separate chunk.
Identifying Critical Chunks:
Determine the chunks necessary for the initial view and interaction. This typically includes the main application logic, core components, and any immediately visible content.
Preloading Critical Chunks:
Employ <link rel="preload"> tags in the HTML <head> or use a library like react-loadable to preload critical chunks. Preloading hints to the browser to download these resources with high priority.
Lazy Loading Non-Critical Chunks:
Implement lazy loading for other chunks that are not immediately needed, such as components for less-used features or content below the fold. This ensures they are only loaded when required, reducing the initial load size.
// Example using dynamic import for code splitting and React.lazy for lazy loading
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./components/Home'));
const About = lazy(() => import('./components/About'));
const Dashboard = lazy(() => import('./components/Dashboard'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Suspense>
</Router>
);
}
export default App;
Redux: Good for large apps with predictable state and complex state flows, but introduces boilerplate. Supports middleware and time-travel debugging.
Context API: Simpler, but causes propagation of re-renders down the component tree. Avoid using for high-frequency updates (like mouse position or timers).
Zustand: Minimal boilerplate with powerful features (selectors, middleware) and better re-render performance than Context API due to shallow comparisons and internal optimization.
Use Redux for shared, deeply nested state, Context for themes/auth, Zustand for local yet shareable states like drag positions or form wizard steps.
Create a useAnalytics hook that sends events to a 3rd party.
const useAnalytics = () => {
const trackEvent = (category, action, label = '') => {
if (window.gtag) {
window.gtag('event', action, {
event_category: category,
event_label: label,
});
}
};
return { trackEvent };
};
Usage:
const { trackEvent } = useAnalytics();
trackEvent('Form', 'Submit', 'Contact Us');
You can also combine with useEffect to send pageview events on route changes via next/router.
You can create a custom hook like useSocket to abstract socket connection logic.
import { useEffect, useState } from 'react';
import io from 'socket.io-client';
const useSocket = (url, eventName) => {
const [data, setData] = useState(null);
useEffect(() => {
const socket = io(url);
socket.on(eventName, (msg) => {
setData(msg);
});
return () => socket.disconnect();
}, [url, eventName]);
return data;
};
Usage:
const liveData = useSocket('wss://my-server.com', 'new-message');
Advantages:
import { useEffect, useState } from 'react';
const useDebounce = (value, delay = 300) => {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
};
Usage:
const searchTerm = useDebounce(inputValue, 500);
import { useEffect, useRef, useState } from 'react';
function useThrottle(value, delay) {
const [throttledValue, setThrottledValue] = useState(value);
const lastExecuted = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastExecuted.current >= delay) {
setThrottledValue(value);
lastExecuted.current = Date.now();
}
}, delay - (Date.now() - lastExecuted.current));
return () => clearTimeout(handler);
}, [value, delay]);
return throttledValue;
}
Usage:
function SearchInput() {
const [search, setSearch] = useState('');
const throttledSearch = useThrottle(search, 1000);
useEffect(() => {
if (throttledSearch) {
// Call API or perform heavy logic
console.log('Searching for:', throttledSearch);
}
}, [throttledSearch]);
return (
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
);
}
Memory leaks often occur due to:
Detection tools:
Chrome DevTools ? Performance ? Heap Snapshots
React DevTools ? Profiler
Example (bad pattern):
useEffect(() => {
const id = setInterval(() => {
console.log('leak?');
}, 1000);
}, []);
Fix (cleanup):
useEffect(() => {
const id = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(id);
}, []);
Best practices:
Always return a cleanup function in useEffectUse AbortController with fetch
Remove event listeners and subscriptions
const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const goOnline = () => setIsOnline(true);
const goOffline = () => setIsOnline(false);
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
return () => {
window.removeEventListener('online', goOnline);
window.removeEventListener('offline', goOffline);
};
}, []);
return isOnline;
};
React runs on the main thread. To prevent UI blocking from heavy computations, offload work to Web Workers.
1. Create a worker file:
// worker.js
self.onmessage = function (e) {
const result = heavyComputation(e.data);
self.postMessage(result);
};
function heavyComputation(data) {
return data.map(x => x * 100); // sample transformation
}
2. In React component:
import { useEffect } from 'react';
useEffect(() => {
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage([1, 2, 3]);
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
return () => worker.terminate();
}, []);
Use libraries like comlink to simplify Worker messaging with Promises.