React interview questions and answers for 2025
React.js Interview Questions for Freshers and Interediate Levels
What are the key features of React.js?
React.js is a JavaScript library used for building user interfaces, primarily for single-page applications (SPAs). Some of its key features include:
- Component-Based Architecture – Applications are built using reusable, independent components, making development more modular and maintainable.
- Virtual DOM – React uses a lightweight copy of the DOM to optimize updates, improving performance by reducing direct manipulations of the real DOM.
- Declarative UI – React makes UI development more predictable by using a declarative approach, where UI updates automatically reflect changes in state.
- Unidirectional Data Flow – Data flows from parent to child components through props, making state management more predictable.
- Hooks – Introduced in React 16.8, Hooks (
useState
,useEffect
, etc.) allow functional components to manage state and side effects without using class components. - JSX (JavaScript XML) – A syntax extension that allows writing UI structures using HTML-like code within JavaScript.
- React Fiber – A reconciliation algorithm introduced in React 16 to improve rendering performance and handle UI updates efficiently.
- State Management – React provides built-in state management (
useState
,useReducer
) and integrates well with external libraries like Redux or Context API. - Server-Side Rendering (SSR) Support – With frameworks like Next.js, React can improve SEO and initial load performance through SSR.
These features make React.js efficient, flexible, and widely adopted for modern web and mobile application development.
How does React differ from other JavaScript frameworks like Angular or Vue?
React.js differs from Angular and Vue in several key ways:
- Library vs. Framework – React is a library for building UI components, focusing on the view layer, whereas Angular is a full-fledged framework that includes built-in solutions for routing, state management, and dependency injection. Vue is more flexible, sitting between React and Angular, offering both a UI library and framework-like features.
- Rendering Approach – React uses a Virtual DOM for efficient UI updates, while Angular uses real DOM with change detection, and Vue also leverages a Virtual DOM but with a more lightweight reactivity system.
- Component-Based Architecture – All three use a component-driven approach, but React relies on JSX (JavaScript XML), while Angular and Vue use templating languages (HTML + directives).
- State Management – React uses useState, useReducer, and Context API for state management, but often requires external libraries like Redux or Recoil for complex state handling. Angular has RxJS and Services, and Vue has Vuex or Pinia for state management.
- Learning Curve – Angular has a steeper learning curve due to TypeScript, decorators, and a complex ecosystem. React has a moderate learning curve, especially with Hooks replacing class components. Vue is considered easier to learn due to its simpler syntax and clearer documentation.
- Performance – React and Vue are generally faster than Angular due to their efficient rendering mechanisms, but Angular is well-suited for large-scale enterprise applications where structure and built-in features matter.
- Ecosystem & Use Cases:
- React is widely used in startups and large-scale applications (e.g., Facebook, Instagram, Netflix).
- Angular is preferred for enterprise applications (e.g., Google, Microsoft).
- Vue is popular in small-to-medium applications and companies that prioritize simplicity (e.g., Alibaba, Xiaomi).
- Data Flow – React follows a unidirectional data flow, meaning data moves in a single direction from parent to child components, making state management more predictable. In contrast, Angular supports bidirectional data binding, allowing automatic synchronization between the model and the view, which simplifies form handling but can introduce complexity in larger applications.
Overall, React provides flexibility and a strong ecosystem, making it a preferred choice for developers building scalable and interactive single-page applications (SPAs).
What is JSX, and why is it used in React?
JSX (JavaScript XML) is a syntax extension for JavaScript that allows writing HTML-like code within JavaScript files. It is used in React to define UI components in a more readable and declarative way.
Why is JSX used in React?
- Improves Readability – JSX makes UI code more intuitive by combining HTML-like syntax with JavaScript logic.
- Enhances Developer Productivity – Writing UI components using JSX is more concise and maintainable compared to using
React.createElement()
. - Babel Compilation – JSX is not native JavaScript; it is transpiled by Babel into standard JavaScript calls (
React.createElement
) before being executed in the browser. - Supports JavaScript Expressions – JSX allows embedding JavaScript expressions within
{}
inside HTML-like structures (e.g.,{user.name}
inside a<h1>
tag). - Ensures Security – JSX prevents Cross-Site Scripting (XSS) attacks by automatically escaping values before rendering.
- Works with Virtual DOM – JSX makes it easier to define React components, which React efficiently updates using the Virtual DOM.
Example of JSX
const Greeting = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
This gets converted to:
const Greeting = ({ name }) => {
return React.createElement('h1', null, `Hello, ${name}!`);
};
In short, JSX simplifies UI development in React, making code more readable, maintainable, and efficient.
Explain the Virtual DOM and how it improves performance.
Virtual DOM and How React Fiber Improves Performance
The Virtual DOM (VDOM) is a lightweight, in-memory representation of the real DOM. React uses it to efficiently update and render UI components without directly modifying the actual DOM, which can be slow and resource-intensive.
How the Virtual DOM Works
- Initial Render – React creates a Virtual DOM tree that mirrors the real DOM.
- State/Props Update – When data changes, React generates a new Virtual DOM tree.
- Diffing Algorithm – React compares (diffs) the new Virtual DOM with the previous one to detect changes.
- Efficient Updates – Only the modified elements (or their parts) are updated in the real DOM using a process called reconciliation.
How React Fiber Enhances Performance
React introduced Fiber architecture in React 16 to further optimize rendering. Unlike the previous synchronous reconciliation process, Fiber:
- Enables Incremental Rendering – React splits updates into small tasks, allowing it to pause, prioritize, or resume rendering.
- Prioritizes UI Updates – User interactions (e.g., animations, input events) are handled before less critical background updates.
- Supports Concurrent Mode – Future improvements in React, such as concurrent rendering, leverage Fiber to enhance responsiveness.
Why It Improves Performance
- Minimizes Direct DOM Manipulation – The real DOM is slow because updates trigger reflows and repaints; the Virtual DOM reduces these costly operations.
- Batch Updates – React batches multiple updates together instead of rendering each change separately.
- Optimized Rendering with Fiber – Instead of blocking execution, Fiber breaks work into chunks that React processes efficiently.
Example:
- Without the Virtual DOM: Every update modifies the real DOM, causing multiple reflows and affecting performance.
- With the Virtual DOM and Fiber: React updates only the necessary elements, making UI updates faster and smoother.
Conclusion
The Virtual DOM, combined with React Fiber, significantly improves performance by reducing unnecessary updates, prioritizing rendering, and enabling smoother UI interactions. This makes React applications more efficient and responsive, especially for complex UIs.
What is the difference between functional components and class components?
React provides two types of components: functional components and class components.
1. Functional Components
Functional components are JavaScript functions that accept props
and return JSX. They are simpler and used primarily for UI rendering.
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;
Key Features:
- Stateless (before Hooks) – Originally, they could not manage state but gained stateful behavior with Hooks (
useState
,useEffect
). - Simpler Syntax – More concise and readable.
- Better Performance – No need to instantiate a class, making them more efficient.
- Encouraged in Modern React – Since React 16.8, Hooks have made functional components the preferred approach.
2. Class Components
Class components use ES6 classes and extend React.Component.
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
Key Features:
- Stateful – Manages local state with
this.state
and updates it usingthis.setState()
.
- Lifecycle Methods – Uses methods like
componentDidMount()
,componentDidUpdate()
, andcomponentWillUnmount()
. - More Boilerplate – Requires explicit
constructor
,this
bindings, and method definitions.
Key Differences
Feature | Functional Components | Class Components |
---|---|---|
Syntax | Function-based | ES6 class-based |
State | useState Hook |
this.state |
Lifecycle | useEffect Hook |
Lifecycle methods (componentDidMount , etc.) |
Performance | More optimized | Slightly slower |
Code Complexity | Simpler | More boilerplate |
Which One to Use?
- Use functional components for modern React development as they are lighter, easier to maintain, and fully support Hooks.
- Class components are still supported but considered legacy after React 16.8.
Conclusion: Functional components are now the preferred way to build React applications due to Hooks, better performance, and simpler code structure.
What are props in React? How do they differ from state? Can We Modify Props in React? What Happens If We Try?
Props in React
Props (short for “properties”) in React are used to pass data from a parent component to a child component. They are read-only and help make components reusable by allowing dynamic content.
Example of Props
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;
// Usage
<Greeting name="John" />
Here, name
is a prop passed from the parent component.
How Props Differ from State
Feature | Props | State |
Definition | Data passed from parent to child | Data managed within a component |
Mutability | Immutable (cannot be changed by the component receiving it) | Mutable (can be updated using setState or useState ) |
Usage | Used for passing dynamic data | Used for component behavior and interactivity |
Who Controls It? | Controlled by the parent component | Controlled by the component itself |
Example of State
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
return (
<button onClick={increment}>
Count: {count}
</button>
);
};
export default Counter;
Here, count
is a state variable that updates when the button is clicked.
Key Takeaway
- Use props when passing data between components.
- Use state when managing component behavior internally.
In short, props allow communication between components, while state enables dynamic updates within a component.
Can We Modify Props in React? What Happens If We Try?
No, props are immutable. If you try to modify them inside a component, React will throw an error.
Example of Incorrect Prop Modification:
const Greeting = ({ name }) => {
name = "Bob"; // ❌ Modifying props is not allowed
return <h2>Hello, {name}!</h2>;
};
This will cause unintended behavior or errors because props are meant to be controlled by the parent component.
How to Modify Data Passed as Props?
If you need to modify a value inside a child component, you should:
- Use State inside the child component.
- Pass a Function as a Prop to update state in the parent.
Example: Updating Parent State from Child Using Props
const Parent = () => {
const [name, setName] = useState("Alice");
return <Child name={name} changeName={() => setName("Bob")} />;
};
const Child = ({ name, changeName }) => (
<div>
<h2>Hello, {name}!</h2>
<button onClick={changeName}>Change Name</button>
</div>
);
Here, the Child
component doesn’t modify props directly but calls a function passed by the parent.
Conclusion
- Props are immutable and passed from parent to child.
- State is mutable and managed within a component.
- You cannot modify props directly inside a child component.
- To modify passed data, use state in the parent and pass an update function as a prop.
How does React handle state management within a component?
React manages component state using the useState Hook (for functional components) and this.state (for class components). The state represents data that can change over time and triggers a re-render when updated.
1. State in Functional Components (useState Hook)
The useState
Hook allows functional components to maintain local state.
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
};
useState(0)
initializescount
with0
.setCount(count + 1)
updates the state and re-renders the component.
2. State in Class Components
Class components use this.state
and this.setState()
.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => this.setState({ count: this.state.count + 1 });
render() {
return <button onClick={this.increment}>Count: {this.state.count}</button>;
}
}
this.state = { count: 0 }
initializes state.this.setState({ count: this.state.count + 1 })
updates state.
React’s State Update Mechanism
- State updates are asynchronous – React batches multiple updates for performance.
- Updating state triggers a re-render – React compares the Virtual DOM and updates only changed parts.
- State should not be mutated directly – Always use
setState
oruseState
to update values.
When to Use State Management Libraries?
For global state management, React provides:
- Context API – For prop drilling elimination.
- Redux / Zustand / Recoil – For complex application-wide state management.
Conclusion
React manages state efficiently using useState
in functional components and this.state
in class components. For larger applications, state management solutions like Context API or Redux may be needed.
What is the useState Hook? Can it be used in class components?
The useState
Hook is a built-in React function that allows functional components to manage local state. It was introduced in React 16.8 to replace the need for class components in stateful logic.
How useState
Works
It returns an array with two values:
- Th current state value
- A function to update the state
Example of useState
in a Functional Component
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0); // Initial state = 0
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
};
count
is the state variable.setCount
updates the state and triggers a re-render.
Can useState
Be Used in Class Components?
No, useState
cannot be used in class components. Class components use this.state
and this.setState()
instead.
State in Class Components (Alternative to useState
) Class components and their state management are considered legacy in modern React development. Hooks (useState
, useReducer
, etc.) are the preferred way to manage state in functional components. However, understanding class-based state is useful for maintaining older React codebases.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => this.setState({ count: this.state.count + 1 });
render() {
return <button onClick={this.increment}>Count: {this.state.count}</button>;
}
}
Key Differences
Feature | useState (Functional) |
this.state (Class) |
---|---|---|
Usage | Inside functional components | Inside class components |
Syntax | const [state, setState] = useState(initialValue) |
this.state = { key: value } + this.setState() |
Reusability | Easier with Hooks | Requires lifecycle methods |
Conclusion
useState
only works in functional components.- Class components use
this.state
andthis.setState()
instead. - Hooks (like
useState
) make functional components the preferred approach in modern React development.
What are controlled and uncontrolled components in React?
In React, form components (like <input>
, <textarea>
, <select>
) can be controlled or uncontrolled based on how they manage their value.
1. Controlled Components
A controlled component is a form element whose value is managed by React state.
- The component’s state determines the input value.
- Updates happen via the
onChange
event.
Example of a Controlled Component
const ControlledInput = () => {
const [text, setText] = React.useState("");
return (
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
);
};
Advantages:
- Predictable state management.
- Synchronizes UI with state.
- Easier validation and form handling.
2. Uncontrolled Components
An uncontrolled component manages its own state using the DOM, without React state.
- React does not control the value.
- Uses a ref to access the current value.
Example of an Uncontrolled Component
const UncontrolledInput = () => {
const inputRef = React.useRef(null);
const handleSubmit = () => {
alert(inputRef.current.value);
};
return (
<>
<input type="text" ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</>
);
};
Advantages:
- Useful for integrating with non-React code.
- Can handle large forms efficiently without frequent re-renders.
Key Differences
Feature | Controlled Components | Uncontrolled Components |
Value Managed By | React state (useState ) |
DOM (useRef ) |
Update Method | onChange handler |
Direct DOM access |
Use Case | Dynamic forms, validation | Integrating with third-party libraries, performance-sensitive forms |
Recommended? | Preferred for most cases | Use when necessary |
Conclusion
- Controlled components are recommended for most React applications as they ensure better control and synchronization.
- Uncontrolled components are useful when integrating with non-React libraries or for simple, uncontrolled user inputs.
How can you pass data between components in React?
In React, data is typically passed between components using props, context, or state management libraries.
1. Passing Data from Parent to Child (Props)
Props (properties
) allow unidirectional data flow from parent to child components.
const Child = ({ message }) => <h1>{message}</h1>;
const Parent = () => <Child message="Hello from Parent" />;
Best for: Static or read-only data transfer.
2. Passing Data from Child to Parent (Callback Functions)
A child component can send data to a parent using callback props.
const Child = ({ onMessage }) => {
return <button onClick={() => onMessage("Data from Child")}>Send</button>;
};
const Parent = () => {
const handleMessage = (data) => alert(data);
return <Child onMessage={handleMessage} />;
};
Best for: Allowing child components to update parent state.
3. Passing Data Between Sibling Components (Lifting State Up)
React promotes lifting state up by keeping shared state in a common parent.
const Parent = () => {
const [data, setData] = React.useState("");
return (
<>
<Child1 sendData={setData} />
<Child2 receivedData={data} />
</>
);
};
const Child1 = ({ sendData }) => (
<button onClick={() => sendData("Hello from Child1")}>Send</button>
);
const Child2 = ({ receivedData }) => <h1>{receivedData}</h1>;
Best for: Sharing state between sibling components.
4. Using Context API for Global State
Context API helps avoid prop drilling by sharing data across multiple components.
const DataContext = React.createContext();
const Parent = () => {
return (
<DataContext.Provider value="Shared Data">
<Child />
</DataContext.Provider>
);
};
const Child = () => {
const data = React.useContext(DataContext);
return <h1>{data}</h1>;
};
Best for: Global state or deeply nested components.
5. Using State Management Libraries (Redux, Recoil, Zustand)
For complex applications, Redux, Recoil, or Zustand can manage global state efficiently.
// Redux example
import { useSelector, useDispatch } from "react-redux";
const data = useSelector((state) => state.value);
const dispatch = useDispatch();
dispatch({ type: "UPDATE", payload: "New Data" });
Best for: Large-scale applications with shared state.
Conclusion
- Use props for parent-to-child communication.
- Use callback functions for child-to-parent communication.
- Use lifting state up for sibling communication.
- Use Context API or Redux for global state.
Each method depends on the complexity and needs of the application.
What is the purpose of the useEffect Hook, and how does it work?
The useEffect
Hook in React is used to handle side effects in functional components, such as data fetching, DOM manipulation, event listeners, or subscriptions. It replaces lifecycle methods like componentDidMount
, componentDidUpdate
, and
componentWillUnmount
in class components.
Basic Syntax
import { useEffect, useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Count updated: ${count}`);
}, [count]); // Runs when 'count' changes
return <button onClick={() => setCount(count + 1)}>Click Me</button>;
};
How useEffect
Works
- Runs after render – By default,
useEffect
runs after the component renders. - Dependency Array – Controls when the effect runs:
[]
(empty array) → Runs once (likecomponentDidMount
).[dependencies]
→ Runs when dependencies change (likecomponentDidUpdate
).- No array → Runs on every render.
- Cleanup Function – Prevents memory leaks by cleaning up effects before re-running.
Common Use Cases for Cleanup Functions
Example with Cleanup (Event Listener Removal)
useEffect(() => {
const handleResize = () => console.log("Window resized");
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
}; // Cleanup when the component unmounts
}, []);
Example with Cleanup (Removing Event Listeners)
useEffect(() => {
const handleResize = () => console.log("Window resized");
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize); // Cleanup
};
}, []);
Prevents multiple event listeners from stacking up when the component re-renders.
Example with Cleanup (Clearing Intervals or Timeouts)
useEffect(() => {
const interval = setInterval(() => console.log("Fetching data..."), 1000);
return () => {
clearInterval(interval); // Cleanup
};
}, []);
Ensures the interval stops running when the component unmounts.
Example with Cleanup (Aborting Fetch Requests). To prevent state updates on unmounted components
useEffect(() => {
const controller = new AbortController();
fetch("https://api.example.com/data", { signal: controller.signal });
return () => controller.abort(); // Cleanup
}, []);
Prevents fetching data for an unmounted component.
Common Use Cases
- Fetching data from an API
- Listening to events (resize, scroll, keypress, etc.)
- Setting up subscriptions
- Manipulating the DOM
Conclusion
useEffect
allows functional components to handle side effects efficiently, making React applications more interactive and dynamic.
What is the use of the key prop in React lists?
The key
prop in React is used to uniquely identify list items when rendering dynamic lists. It helps React track changes efficiently and improve performance by minimizing unnecessary re-renders.
Why is the key
Prop Important?
- Optimizes Rendering – Helps React efficiently update or re-order list items without re-rendering the entire list.
- Prevents UI Bugs – Without keys, React may reuse incorrect elements, causing unexpected behavior.
- Improves Performance – React uses reconciliation (diffing algorithm) to compare new and previous states and update only changed items.
Example of Using the key
Prop
const List = () => {
const items = ["Apple", "Banana", "Cherry"];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li> // Not recommended
))}
</ul>
);
};
Avoid using index
as a key unless items are static, as it may cause issues when list order changes.
Best Practice: Use a Unique Identifier
const List = () => {
const items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Cherry" },
];
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li> // Best practice
))}
</ul>
);
};
Conclusion
The key
prop helps React efficiently update lists by tracking individual items, reducing re-renders, and preventing UI inconsistencies. Always use a unique ID instead of the array index for dynamic lists.
How do you handle forms in React?
Forms in React are handled using controlled or uncontrolled components, with controlled components being the preferred approach.
1. Controlled Components (Recommended)
In controlled components, the form input’s value is managed by React state, ensuring synchronization between the UI and state.
import { useState } from "react";
const Form = () => {
const [name, setName] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
alert(`Submitted: ${name}`);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
};
Benefits:
- Full control over input values.
- Easier validation and manipulation.
2. Uncontrolled Components (Using Refs)
Uncontrolled components store values directly in the DOM instead of React state, accessed via useRef
.
import { useRef } from "react";
const Form = () => {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert(`Submitted: ${inputRef.current.value}`);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
};
Best for: Integrating with non-React libraries or handling file inputs.
3. Handling Multiple Inputs
For forms with multiple fields, use a single state object.
const [formData, setFormData] = useState({ name: "", email: "" });
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
4. Form Validation Example
const handleSubmit = (e) => {
e.preventDefault();
if (!name.trim()) alert("Name is required");
};
Conclusion
- Use controlled components for better control and validation.
- Use uncontrolled components when direct DOM access is needed.
- Use state objects for handling multiple fields efficiently.
Controlled components are the preferred way to manage forms in React due to their predictability and flexibility.
What is React Router, and why is it used?
React Router is a popular routing library for React applications that enables navigation between different views (pages) without a full page reload. It helps manage client-side routing in Single Page Applications (SPAs).
Why is React Router Used?
- Enables Client-Side Routing – Allows navigation between components without refreshing the page.
- Improves Performance – Only updates the necessary components instead of reloading the entire app.
- Supports Dynamic Routing – Routes can be parameterized (
/user/:id
) for dynamic content. - Nested Routing – Supports defining child routes within parent routes.
- Easy Navigation Management – Provides
Link
andNavLink
components for declarative navigation.
Basic Example of React Router
Install React Router:
npm install react-router-dom
Set up routes in App.js:
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
const Home = () => <h1>Home Page</h1>;
const About = () => <h1>About Page</h1>;
const App = () => {
return (
<Router>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
};
export default App;
Key Components of React Router
BrowserRouter
– Wraps the app and enables routing.Routes
– Defines multipleRoute
components.Route
– Maps URLs to specific components.Link
/NavLink
– Enables navigation without reloading the page.useParams
– Access dynamic route parameters (/user/:id
).useNavigate
– Programmatic navigation to other routes.
Conclusion
React Router is essential for building multi-page SPAs by enabling seamless navigation, improving performance, and allowing dynamic route handling.
What are higher-order components (HOCs) in React?
A Higher-Order Component (HOC) is a function that takes a component as an argument and returns a new enhanced component. HOCs enable code reusability, logic sharing, and component composition in React.
Why Use HOCs?
- Code Reusability – Share logic across multiple components.
- Separation of Concerns – Keep business logic separate from UI components.
- Enhance Components – Add additional functionality (e.g., authentication, logging, API handling).
Basic Example of a HOC
This HOC adds a loading state to any component.
const withLoading = (WrappedComponent) => {
return (props) => props.isLoading ? <p>Loading...</p> : <WrappedComponent {...props} />;
};
// Usage
const UserList = ({ users }) => (
<ul>{users.map((user) => <li key={user.id}>{user.name}</li>)}</ul>
);
const EnhancedUserList = withLoading(UserList);
// In App
<EnhancedUserList isLoading={false} users={[{ id: 1, name: "John" }]} />;
UserList
now automatically handles a loading state!
Common Use Cases for HOCs
- Authentication Handling – Restrict access to authenticated users.
- Logging & Analytics – Track user interactions.
- Data Fetching – Handle API calls and inject data into components.
- Theming – Apply different styles dynamically.
Key Considerations
- Avoid Overuse – HOCs can lead to deeply nested component trees (wrapper hell).
- Use Hooks Instead – Many use cases of HOCs are now handled with custom Hooks (
useContext
,useReducer
).
Conclusion
Higher-Order Components enhance reusability by wrapping components with additional logic, but modern React often prefers Hooks and Context API for similar use cases.
What is the difference between useEffect and lifecycle methods in class components?
In class components, React uses lifecycle methods (componentDidMount
,
componentDidUpdate
, componentWillUnmount
) to handle side effects. In functional components, the useEffect
Hook replaces these lifecycle methods.
1. Lifecycle Methods in Class Components
Class components use separate methods for different lifecycle stages:
class Example extends React.Component {
componentDidMount() {
console.log("Component mounted");
}
componentDidUpdate() {
console.log("Component updated");
}
componentWillUnmount() {
console.log("Component will unmount");
}
render() {
return <h1>Hello</h1>;
}
}
Downside: Requires multiple methods to handle different lifecycle stages.
2. useEffect
in Functional Components
The useEffect
Hook combines all lifecycle methods into one function.
import { useEffect } from "react";
const Example = () => {
useEffect(() => {
console.log("Component mounted or updated");
return () => console.log("Component will unmount"); // Cleanup function
}, []); // Dependency array
return <h1>Hello</h1>;
};
Simplifies logic by merging multiple lifecycle behaviors.
Key Differences
Feature | Lifecycle Methods (Class) | useEffect (Functional) |
Component Mounting | componentDidMount |
useEffect(() => {...}, []) |
Component Updating | componentDidUpdate |
useEffect(() => {...}, [dependencies]) |
Component Unmounting | componentWillUnmount |
useEffect(() => {... return cleanup }, []) |
Code Complexity | Separate methods for each phase | One function handles multiple phases |
Performance | Requires class instantiation | Lightweight and optimized |
Conclusion
- Class components use lifecycle methods, while functional components use
useEffect
. useEffect
simplifies side effect management by merging multiple lifecycle stages.- Modern React prefers functional components with Hooks over class components.
Explain how React Context API works and when to use it over props.
he React Context API is a built-in state management solution that allows data to be shared across components without prop drilling. It is useful for global state like themes, authentication, and user preferences.
How React Context API Works
- Create a Context – Defines a global store.
- Provide a Context – Wrap components with a Provider to pass data.
- Consume the Context – Use useContext to access data inside any component.
Example of React Context API
import React, { createContext, useContext, useState } from "react";
// Step 1: Create Context
const ThemeContext = createContext();
// Step 2: Provide Context
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Step 3: Consume Context
const ThemedComponent = () => {
const { theme, setTheme } = useContext(ThemeContext);
return (
<>
<p>Current Theme: {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
</>
);
};
// Usage
const App = () => (
<ThemeProvider>
<ThemedComponent />
</ThemeProvider>
);
When to Use Context API Over Props?
Scenario | Props | Context API |
Simple Parent-Child Communication | yes | no |
Avoid Prop Drilling (Deep Nesting) | no | yes |
Global State (Auth, Theme, User Data) | yes | yes |
Reusable Components (Without State Dependency) | no | no |
Props are preferred for local state and direct communication.
Context API is best for global state management and eliminating prop drilling.
Conclusion
React Context API improves state management by providing a direct way to share data between components without passing props manually. It should be used when multiple components need access to the same data, but for complex applications, Redux or Zustand may be better choices.
What are React Portals, and when should you use them?
React Portals allow rendering a component outside of its parent component’s DOM hierarchy while keeping it within the React tree. This is useful when you need to bypass CSS overflow or stacking context issues.
How React Portals Work
- Create a Portal container in
index.html
:
<div id="portal-root"></div>
2. Use ReactDOM.createPortal() to render the component:
import ReactDOM from "react-dom";
const Modal = ({ children }) => {
return ReactDOM.createPortal(
<div className="modal">{children}</div>,
document.getElementById("portal-root")
);
};
const App = () => (
<>
<h1>Main App</h1>
<Modal>Portal Content</Modal>
</>
);
When Should You Use React Portals?
- Modals & Dialogs – Ensures the modal appears above everything else, bypassing parent styles.
- Tooltips & Popovers – Prevents unwanted clipping inside containers with
overflow: hidden
. - Global Notifications – Displays alerts or toast messages without affecting existing layout structure.
Conclusion
React Portals help render components outside their parent DOM hierarchy while maintaining React’s state and event bubbling. They are essential for modals, popups, tooltips, and overlays that need to break out of the normal component tree.
How does React handle reconciliation and rendering optimization?
React reconciliation is the process of efficiently updating the DOM when component state or props change. It uses a diffing algorithm and Virtual DOM to optimize rendering.
1. How React Reconciliation Works
- Virtual DOM Update – React creates a new Virtual DOM tree when state or props change.
- Diffing Algorithm – React compares the new Virtual DOM with the previous one to identify changes.
- Efficient Updates (Reconciliation) – Only the changed elements are updated in the real DOM.
Example of efficient updates:
const [count, setCount] = useState(0);
return <h1>Count: {count}</h1>;
When setCount(1) runs, React only updates the <h1> instead of re-rendering the entire page.
2. Rendering Optimization Techniques
- Using
key
in Lists – Helps React track and update list items efficiently.
items.map((item) => <li key={item.id}>{item.name}</li>);
- Memoization (React.memo) – Prevents unnecessary re-renders of functional components.
const MemoizedComponent = React.memo(MyComponent);
- Using useCallback and useMemo – Optimizes function and computed values.
const memoizedValue = useMemo(() => computeExpensiveValue(data), [data]);
- Avoiding Unnecessary State Updates – Updating state only when necessary reduces re-renders.
if (prevValue !== newValue) setState(newValue);
- Lazy Loading with React.lazy – Loads components only when needed, improving performance.
const LazyComponent = React.lazy(() => import("./MyComponent"));
Conclusion
React optimizes rendering through reconciliation, Virtual DOM, and diffing algorithms. Using best practices like memoization, lazy loading, and efficient state updates improves performance and reduces unnecessary re-renders.
What are the benefits of using memoization (React.memo and
useMemo) in React?
Memoization in React helps optimize performance by caching the results of expensive computations or preventing unnecessary re-renders. React provides two key tools for memoization:
1. React.memo
– Optimizing Component Re-Renders
React.memo
is a higher-order component (HOC) that prevents a functional component from re-rendering if its props haven’t changed.
Example:
const MemoizedComponent = React.memo(({ name }) => {
console.log("Re-rendered!");
return <h1>Hello, {name}!</h1>;
});
Prevents unnecessary re-renders when name
stays the same.
2. useMemo
– Optimizing Expensive Calculations
useMemo
caches the result of computationally expensive functions to avoid recalculating on every render.
Example:
import { useMemo } from "react";
const ExpensiveComponent = ({ numbers }) => {
const sum = useMemo(() => numbers.reduce((a, b) => a + b, 0), [numbers]);
return <h1>Sum: {sum}</h1>;
};
Recalculates only when numbers
change, improving efficiency.
Benefits of Memoization in React
- Reduces Unnecessary Re-Renders – Improves performance by skipping updates.
- Optimizes Expensive Calculations – Prevents recalculating the same values repeatedly.
- Improves UI Responsiveness – Faster rendering leads to a smoother experience.
- Enhances Large Component Performance – Useful in lists, tables, and complex UIs.
When to Use Memoization?
- Use
React.memo
for pure functional components that receive the same props frequently. - Use
useMemo
when dealing with costly computations that don’t need to run on every render.
Conclusion
Memoization with React.memo
and useMemo
boosts performance by caching values and preventing redundant renders, making React applications more efficient.
What is useCallback, and when should it be used?
useCallback
is a React Hook that memoizes functions to prevent unnecessary re-creations on every render. It is useful for performance optimization in components that pass functions as props.
How useCallback
Works
It returns a memoized function that only changes if its dependencies change.
Example Without useCallback
(Inefficient)
const Parent = () => {
const handleClick = () => console.log("Clicked!");
return <Child onClick={handleClick} />;
};
Issue: handleClick
is recreated on every render, causing unnecessary re-renders in Child
.
Optimized with useCallback
import { useCallback } from "react";
const Parent = () => {
const handleClick = useCallback(() => console.log("Clicked!"), []);
return <Child onClick={handleClick} />;
};
Now handleClick
remains the same across renders, preventing unnecessary re-renders of Child
.
When Should useCallback
Be Used?
- Passing Functions as Props – Prevents child components from re-rendering unnecessarily.
- Performance Optimization – Useful in components with expensive computations.
- Avoiding Unnecessary State Updates – Helps with event listeners and handlers.
When NOT to Use useCallback
?
- On simple functions that don’t affect performance.
- If the function is already recreated infrequently (e.g., inside an event handler).
Conclusion
useCallback
memoizes functions to reduce unnecessary renders, making it useful in performance-critical scenarios, especially when passing callbacks to child components.
Explain lazy loading and how React’s React.lazy and Suspense help improve performance.
Lazy loading is a technique that delays loading non-essential components until they are needed. This improves performance by reducing the initial load time of an application.
How React.lazy
Works
React.lazy()
allows components to be loaded dynamically only when required.
Example of Lazy Loading
import React, { lazy, Suspense } from "react";
const LazyComponent = lazy(() => import("./MyComponent"));
const App = () => (
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
);
Here’s what happens:
LazyComponent
is not loaded initially.- When needed, React fetches the component asynchronously.
Suspense
displays a fallback UI (like a loading spinner) until loading is complete.
How Suspense
Helps
Suspense
acts as a wrapper that handles loading states while waiting for the lazy component to load.
- Improves performance – Loads only what’s needed instead of loading everything upfront.
- Enhances user experience – Shows a fallback UI while loading content.
- Optimizes large applications – Reduces JavaScript bundle size, speeding up initial page loads.
When to Use Lazy Loading?
- Large components like modals, charts, or dashboards.
- Routes in a single-page application (SPA).
- Third-party libraries that aren’t needed immediately.
Conclusion
Lazy loading with React.lazy
and Suspense
boosts performance by reducing initial bundle size and ensuring components are loaded only when necessary.
How do you optimize a React application for performance?
Optimizing a React application involves reducing unnecessary renders, minimizing JavaScript execution, and improving load times. Key strategies include:
1. Optimize Rendering
Use React.memo
to Prevent Unnecessary Re-renders
const MemoizedComponent = React.memo(MyComponent);
Use useCallback and useMemo for Function and Value Memoization
const memoizedValue = useMemo(() => computeExpensiveValue(data), [data]);
const memoizedFunction = useCallback(() => doSomething(), [dependencies]);
2. Optimize Component Structure
Split Code with Lazy Loading (React.lazy
& Suspense
)
const LazyComponent = React.lazy(() => import("./MyComponent"));
Keep Component State Local – Move unnecessary state out of high-level components to avoid re-renders.
3. Optimize Lists and Rendering
Use key
Props Efficiently in Lists
items.map((item) => <li key={item.id}>{item.name}</li>);
Avoid Inline Functions and Objects in JSX
// ❌ Creates a new function on every render
<button onClick={() => doSomething()} />
// ✅ Use useCallback
const handleClick = useCallback(() => doSomething(), []);
<button onClick={handleClick} />
Reduce JavaScript Bundle Size
Remove Unused Dependencies – Analyze bundle size with tools like webpack-bundle-analyzer
.
Use Production Build – Enable optimizations with:
npm run build
Use Tree Shaking – Import only needed functions.
// Imports everything
import * as lodash from "lodash";
// Imports only what’s needed
import { debounce } from "lodash";
5. Optimize API Calls and State Management
Use Pagination or Infinite Scrolling for Large Data Sets
Use Context API, Redux, or Zustand for Efficient State Management
Debounce Expensive Calls (e.g., Search Inputs)
const debouncedSearch = useCallback(debounce((query) => fetchData(query), 500), []);
Conclusion
Optimizing a React app involves reducing unnecessary renders, using memoization, optimizing lists, and minimizing bundle size to improve performance and user experience.
How does server-side rendering (SSR) work in React, and what are its advantages?
Server-Side Rendering (SSR) in React is a technique where a React application is rendered on the server instead of the client’s browser. This improves performance and SEO by sending a fully rendered HTML page to the client.
How SSR Works in React
- Client Request – The browser requests a React page.
- Server Renders the React Component – The server executes React code and generates an HTML response.
- Pre-rendered HTML Sent to Client – The user sees the initial content immediately.
- Hydration – React reattaches event listeners, making the app interactive.
Example of SSR Using Next.js (React Framework)
export async function getServerSideProps() {
const res = await fetch("https://api.example.com/data");
const data = await res.json();
return { props: { data } };
}
const Page = ({ data }) => <h1>{data.title}</h1>;
export default Page;
getServerSideProps
fetches data on the server before rendering the page.
Advantages of SSR
- Improved SEO – Search engines can index pre-rendered content, boosting visibility.
- Faster First Paint – Users see content sooner since the HTML is ready.
- Better Performance for Low-Powered Devices – Reduces client-side computation.
- Consistent Content – Ensures data consistency before hydration.
When to Use SSR?
- When SEO is crucial (e.g., blogs, e-commerce).
- When fast initial load times are needed.
- When pre-fetching dynamic data before rendering.
Conclusion
SSR renders React pages on the server, improving SEO, performance, and user experience. It’s commonly used with Next.js for dynamic, optimized applications.
What are the advantages and disadvantages of Redux compared to Context API?
Redux and Context API are both used for state management in React, but they serve different purposes and have their own pros and cons.
Advantages of Redux
- Predictable State Management – Uses a single source of truth (store) and follows strict rules (reducers, actions).
- Powerful Debugging Tools – Redux DevTools allow time-travel debugging.
- Scalability – Works well for large applications with complex state.
- Middleware Support – Integrates with Redux Thunk or Redux Saga for handling async actions (e.g., API calls).
Disadvantages of Redux
- Boilerplate Code – Requires actions, reducers, and dispatch functions, making it verbose.
- Complexity for Small Apps – Overkill for simple state management.
- Performance Overhead – Frequent re-renders if not optimized properly.
Advantages of Context API
- Built-In to React – No need for external libraries.
- Simpler State Management – Easier to use for small-to-medium applications.
- No Extra Boilerplate – Uses
useContext
withuseState
oruseReducer
. - Better Performance for Localized State – Efficient when state is needed in only a few components.
Disadvantages of Context API
- Performance Issues for Frequent Updates – If the context value changes frequently, all consuming components re-render.
- No Middleware Support – Lacks built-in async handling like Redux middleware.
- Not Ideal for Large-Scale State Management – Becomes harder to manage in complex applications.
When to Use Redux vs. Context API?
Feature | Redux | Context API |
App Size | Large apps | Small-to-medium apps |
State Complexity | Complex state (e.g., authentication, API caching) | Simple state (e.g., theme, user preferences) |
Performance Optimization | Better with selectors & memoization | Can cause unnecessary re-renders |
Middleware Support | Supports async middleware (Thunk, Saga) | No built-in middleware |
Ease of Use | More boilerplate, but powerful | Simpler, but less scalable |
Conclusion
- Use Redux for large-scale, complex applications requiring centralized state management and middleware.
- Use Context API for simpler, lightweight state sharing without external dependencies.
- For performance-sensitive applications, consider React Query, Zustand, or Recoil as alternatives.
What are the main differences between Redux Toolkit and traditional Redux?
Redux Toolkit (RTK) is the official, recommended way to use Redux. It simplifies state management by reducing boilerplate and improving efficiency compared to traditional Redux.
Feature | Traditional Redux | Redux Toolkit (RTK) |
Boilerplate Code | Requires actions, reducers, and constants separately | Uses createSlice() to reduce boilerplate |
State Mutability | Requires immutable updates with spread operators |
Uses Immer.js, allowing direct state mutation |
Setup Complexity | Manual store configuration (createStore ) |
Pre-configured with configureStore() |
Thunk Middleware | Must install and configure manually | Built-in support for async actions (createAsyncThunk ) |
Best Practices | Requires manual structuring | Enforces Redux best practices automatically |
Developer Experience | More code, prone to complexity | Simpler syntax, easier debugging |
Example: Traditional Redux vs. Redux Toolkit
1. Traditional Redux Setup
// Actions
const increment = () => ({ type: "INCREMENT" });
// Reducer
const counterReducer = (state = 0, action) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
default:
return state;
}
};
// Store
const store = createStore(counterReducer);
2. Redux Toolkit Setup (Simpler & Cleaner)
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
});
const store = configureStore({ reducer: counterSlice.reducer });
export const { increment } = counterSlice.actions;
Less code, better readability, and built-in best practices!
When to Use Redux Toolkit Over Traditional Redux?
- For new projects, always use RTK—it’s more efficient and less error-prone.
- For existing Redux apps, migrating to RTK improves maintainability and simplifies code.
- For complex state logic, RTK’s built-in tools (like
createAsyncThunk
) handle async operations more effectively.
Conclusion
Redux Toolkit eliminates unnecessary boilerplate, simplifies state management, and follows best practices. It is now the default and recommended approach for using Redux.
How would you implement global state management in a React application?
Global state management in React is used when multiple components need to share and update the same state. There are several approaches, depending on the complexity of the application.
1. Using React Context API (Best for Small to Medium Apps)
React’s built-in Context API allows global state sharing without prop drilling.
import { createContext, useContext, useState } from "react";
// Create Context
const GlobalContext = createContext();
// Provider Component
const GlobalProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
Best for: Small apps, themes, authentication, or user preferences.
2. Using Redux Toolkit (Best for Large-Scale Apps)
For complex state (e.g., API data, authentication), Redux Toolkit simplifies global state management
import { createSlice, configureStore } from "@reduxjs/toolkit";
import { Provider, useSelector, useDispatch } from "react-redux";
// Create Slice
const counterSlice = createSlice({
name: "counter",
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
});
Best for: Large apps needing predictable, centralized state management.
3. Using Zustand (Simpler Alternative to Redux)
Zustand provides lightweight global state without Context API overhead.
import create from "zustand";
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const Counter = () => {
const { count, increment } = useStore();
return <button onClick={increment}>Count: {count}</button>;
};
Best for: Simple, scalable global state management with minimal boilerplate.
Choosing the Right Approach
Approach | Best For |
Context API | Small-to-medium apps, simple state sharing |
Redux Toolkit | Large-scale apps, complex state management |
Zustand | Lightweight, scalable state management |
Conclusion
To implement global state in React:
- Use Context API for simple state sharing.
- Use Redux Toolkit for large-scale applications.
- Use Zustand for an efficient, minimal setup.
Choose the right tool based on your app size and complexity.
What is the purpose of useReducer, and how is it different from
useState?
The useReducer
Hook is used for managing complex state logic in React components. It is an alternative to useState
, especially when state updates involve multiple conditions or actions.How useReducer
Works
useReducer
takes two arguments:
- A reducer function – Defines how state updates based on actions.
- An initial state – The starting value of the state.
import { useReducer } from "react";
// Reducer function
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
};
// Component using useReducer
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
};
Here’s what happens:
dispatch({ type: "increment" })
triggers the reducer function.- The reducer updates the state based on the action type.
Differences Between useReducer
and useState
Feature | useState |
useReducer |
Best For | Simple state (e.g., toggles, form inputs) | Complex state (e.g., multiple state changes, logic-heavy updates) |
State Update | Directly modifies state (setState ) |
Uses an action-based approach (dispatch ) |
Performance | Simpler, but can cause frequent re-renders | Optimized for managing complex logic efficiently |
Scalability | Best for local component state | Better for larger, structured state updates |
When to Use useReducer
?
- When state logic depends on previous state values (e.g., counters, form validation).
- When multiple actions modify state (e.g., handling API responses).
- When using
useState
becomes hard to manage (e.g., nested state objects).
Conclusion
- Use
useState
for simple, independent state updates. - Use
useReducer
for structured, complex state updates with multiple conditions. - For global state, consider Context API +
useReducer
instead of Redux for lightweight state management.
How do you test React components using Jest and React Testing Library?
Testing React components ensures they work correctly and handle user interactions as expected. Jest is the default testing framework for React, and React Testing Library (RTL) provides utilities for testing UI behavior.
1. Setting Up Jest and React Testing Library
If not already installed, install react-testing-library
and jest-dom
:
npm install --save-dev @testing-library/react @testing-library/jest-dom
2. Writing a Basic Test for a React Component
Consider a simple Button
component:
const Button = ({ onClick, label }) => (
<button onClick={onClick}>{label}</button>
);
export default Button;
Now, test if the button renders and handles clicks:
import { render, screen, fireEvent } from "@testing-library/react";
import Button from "./Button";
test("renders button with correct label", () => {
render(<Button label="Click Me" />);
expect(screen.getByText("Click Me")).toBeInTheDocument();
});
test("calls onClick when button is clicked", () => {
const handleClick = jest.fn();
render(<Button label="Click Me" onClick={handleClick} />);
fireEvent.click(screen.getByText("Click Me"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
What This Test Does:
- Checks if the button renders correctly (
getByText
). - Simulates a click event (
fireEvent.click
). - Verifies if the
onClick
handler was called (jest.fn()
).
3. Testing Asynchronous Components
For async operations (e.g., API calls), use findBy
:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("displays message after async fetch", async () => {
render(<AsyncComponent />);
expect(await screen.findByText("Data Loaded")).toBeInTheDocument();
});
4. Snapshot Testing
Snapshot tests ensure UI consistency:
import { render } from "@testing-library/react";
import Button from "./Button";
test("matches snapshot", () => {
const { asFragment } = render(<Button label="Click Me" />);
expect(asFragment()).toMatchSnapshot();
});
5. Best Practices for Testing React Components
- Test Behavior, Not Implementation – Focus on UI interactions, not internal logic.
- Use
screen.getBy*
Methods – Improves test readability. - Mock API Calls – Prevents real network requests during testing.
Conclusion
Jest and React Testing Library enable efficient, behavior-driven testing by verifying component rendering, interactions, and async behavior.
What are the best practices for structuring a React.js project?
A well-structured React project improves maintainability, scalability, and readability. Here are key best practices:
1. Follow a Clear Folder Structure
A common structure for medium to large applications:
/src
/components → Reusable UI components
/pages → Page-specific components
/hooks → Custom React Hooks
/context → Context API state management
/services → API calls and external integrations
/utils → Helper functions and constants
/assets → Images, fonts, and styles
/tests → Unit and integration tests
/store → Redux or Zustand store (if used)
App.js → Root component
index.js → Entry point
2. Keep Components Small and Reusable
- Break down large components into smaller, single-responsibility components.
- Store reusable UI components inside
/components
.
// Example: Button.js
const Button = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
export default Button;
3. Use Context API or State Management Libraries Wisely
- Use
useState
for local component state. - Use Context API for sharing state across multiple components.
- Use Redux or Zustand for large-scale state management.
4. Separate API Calls Into a services/
Folder
// services/api.js
export const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
5. Use Absolute Imports Instead of Relative Imports
Instead of:
import Button from "../../../components/Button";
Use Absolute Paths (Configure jsconfig.json or tsconfig.json)
import Button from "components/Button";
6. Use Lazy Loading for Performance Optimization
import { lazy, Suspense } from "react";
const LazyComponent = lazy(() => import("./components/LazyComponent"));
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>;
7. Follow Naming Conventions
- Components: PascalCase (
MyComponent.js
) - Hooks: camelCase (
useAuth.js
) - Constants: UPPER_CASE (
config.js
) - Files: Meaningful names instead of
index.js
everywhere.
8. Write Tests for Components and Functions
- Unit Tests – For individual components.
- Integration Tests – For multiple component interactions.
- End-to-End Tests – For full user flows.
import { render, screen } from "@testing-library/react";
import Button from "components/Button";
test("renders button correctly", () => {
render(<Button label="Click Me" />);
expect(screen.getByText("Click Me")).toBeInTheDocument();
});
Conclusion
Following these best practices ensures clean, scalable, and maintainable React applications. A well-structured project improves collaboration, debugging, and performance over time.
React.js Interview Questions for Experienced Levels
How does React Fiber improve rendering performance compared to earlier versions?
React Fiber is a reconciliation algorithm introduced in React 16 to improve rendering performance. It replaces the older stack-based reconciliation with a more efficient incremental rendering system.
Key Performance Improvements in React Fiber
- Incremental Rendering (Time-Slicing)
- Fiber breaks rendering into small units and prioritizes updates, allowing React to pause and resume work efficiently.
- This prevents UI lag and keeps apps responsive.
- Prioritization of Updates
- React Fiber assigns different priority levels to updates (e.g., user interactions are prioritized over background tasks).
- This makes animations and input handling smoother.
- Concurrency Mode Support
- Enables non-blocking rendering, allowing React to process multiple updates asynchronously.
- Efficient Reconciliation (Diffing Algorithm)
- Faster re-render detection, reducing unnecessary updates.
- Uses a linked list structure instead of a recursive call stack for better memory management.
- Better Error Handling
- Introduces error boundaries to gracefully handle component failures without crashing the app.
Comparison: React Fiber vs. Older Versions
Feature | Older React (Stack Reconciliation) | React Fiber |
Rendering | Synchronous (blocks UI) | Asynchronous (time-slicing) |
Reconciliation | Recursive, slower updates | Linked-list based, optimized updates |
Prioritization | No prioritization | High-priority updates handled first |
Error Handling | No built-in error boundaries | Supports error boundaries |
Conclusion
React Fiber optimizes rendering performance by enabling incremental updates, prioritizing UI tasks, supporting concurrency, and improving memory efficiency. This results in smoother, faster, and more responsive React applications.
What are the limitations of the Context API, and when should you prefer Redux, Redux Toolkit or Zustand?
The Context API is a built-in React feature used for global state management. While useful, it has some limitations that make Redux, Redux Toolkit (RTK), or Zustand better choices in certain cases.
Limitations of the Context API
- Performance Issues – Every context update triggers a re-render of all consuming components, which can slow down large applications.
- Prop Drilling in Large Apps – Although it avoids prop drilling, managing deeply nested state across multiple contexts can become complex.
- No Built-in Middleware Support – Unlike Redux and RTK, Context API does not have middlewares for async logic like logging, caching, or API handling.
- Harder Debugging – Context API lacks advanced debugging tools like Redux DevTools.
- State Splitting Complexity – Managing multiple contexts for different state slices can be tedious and hard to maintain.
When to Prefer Redux, Redux Toolkit, or Zustand
Use Case | Context API | Redux | Redux Toolkit (RTK) | Zustand |
Small to Medium Apps | ✅ Best Choice | ❌ Overkill | ❌ Overkill | ✅ Good Choice |
Complex State Management | ❌ Hard to Scale | ✅ Handles Large State | ✅ Simplifies Redux | ✅ Lightweight but Scalable |
Performance Optimization | ❌ Re-renders on updates | ✅ Optimized with selectors | ✅ Efficient with createSlice |
✅ Optimized with slices |
Middleware Support | ❌ No built-in support | ✅ Supports Thunk/Saga | ✅ Built-in async handling (createAsyncThunk ) |
✅ Supports async logic |
Debugging Tools | ❌ Limited | ✅ Redux DevTools | ✅ Redux DevTools + Built-in best practices | ✅ Simple but effective |
Conclusion
- Use Context API for small apps with simple global state (e.g., themes, authentication).
- Use Redux for large-scale applications that require strict state management and debugging tools.
- Use Redux Toolkit (RTK) when you need Redux but with less boilerplate, built-in middleware, and async support.
- Use Zustand for lightweight, efficient state management without Redux’s complexity.
How does useReducer compare to Redux for state management?
Both useReducer
and Redux help manage complex state in React applications, but they serve different purposes.
useReducer
(Local State Management)
useReducer
is a built-in React Hook for handling component-level state with an action-based approach.
Example: Counter with useReducer
const reducer = (state, action) => {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "decrement": return { count: state.count - 1 };
default: return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return <button onClick={() => dispatch({ type: "increment" })}>Count: {state.count}</button>;
};
Best For:
- Component-specific state
- Simple applications
- Avoiding unnecessary external dependencies
Redux (Global State Management)
Redux manages application-wide state with a centralized store, making it suitable for complex apps.
When to Use Redux Instead of useReducer
Feature | useReducer
|
Redux |
Scope | Local component state | Global application state |
Boilerplate | Minimal setup | More setup (actions, reducers, store) |
Middleware Support | ❌ No middleware | ✅ Supports Thunk, Saga, etc. |
Debugging Tools | ❌ Limited | ✅ Redux DevTools |
Performance Optimization | ❌ Can cause unnecessary re-renders | ✅ Optimized with selectors |
Conclusion
- Use
useReducer
for local state management in small-to-medium components. - Use Redux for large-scale applications needing global state, middleware, and debugging tools.
- If Redux is overkill but global state is needed, consider Zustand or Context API +
useReducer
.
What are the best practices for structuring large-scale React applications?
Best Practices for Structuring Large-Scale React Applications
A well-structured React application improves maintainability, scalability, and performance. Below are key best practices for organizing large-scale projects.
1. Follow a Scalable Folder Structure
A modular approach keeps components and logic well-organized:
/src
/components → Reusable UI components
/pages → Page-specific components
/hooks → Custom Hooks
/context → Context API for global state
/store → Redux/Zustand store (if used)
/services → API calls and external integrations
/utils → Helper functions/constants
/assets → Images, fonts, and styles
/tests → Unit and integration tests
App.js → Root component
index.js → Entry point
2. Keep Components Small and Reusable
- Follow the Single Responsibility Principle (SRP) – Each component should have one clear function.
- Extract reusable UI elements (buttons, forms) into a
/components
folder.
// Example: Reusable Button Component
const Button = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
export default Button;
3. Use Centralized State Management Wisely
- Use
useState
for local component state. - Use Context API for small global states (e.g., theme, authentication).
- Use Redux/Zustand for large, complex applications.
4. Separate Business Logic from UI
- Keep API calls in a
services/
folder.
// Example: API Service
export const fetchUsers = async () => {
const response = await fetch("/api/users");
return response.json();
};
5. Optimize Performance
- Use
React.memo
anduseCallback
to prevent unnecessary re-renders. - Use Lazy Loading (
React.lazy
) for components that aren’t needed immediately. - Use Code Splitting to reduce bundle size.
const LazyComponent = React.lazy(() => import("./MyComponent"));
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>;
6. Use Absolute Imports
Avoid deep relative paths by configuring jsconfig.json
:
{
"compilerOptions": {
"baseUrl": "src"
}
}
Now, instead of:
import Button from "../../../components/Button";
Use:
import Button from "components/Button";
7. Write Unit and Integration Tests
- Use Jest & React Testing Library for testing components.
- Mock API requests in tests.
import { render, screen } from "@testing-library/react";
import Button from "components/Button";
test("renders button correctly", () => {
render(<Button label="Click Me" />);
expect(screen.getByText("Click Me")).toBeInTheDocument();
});
Conclusion
By following these best practices—clear folder structure, reusable components, state management, optimized performance, and testing—you can build scalable, maintainable, and efficient React applications.
How does React handle reconciliation, and what are some ways to optimize re-renders?
Reconciliation is React’s process of updating the DOM efficiently when state or props change. It uses the Virtual DOM and a diffing algorithm to identify what has changed and updates only those parts instead of re-rendering the entire UI.
How React Handles Reconciliation
- Virtual DOM Comparison – React creates a new Virtual DOM tree and compares it with the previous one.
- Diffing Algorithm – React finds differences (changes, additions, or deletions).
- Efficient Updates – Only modified elements are updated in the real DOM using a process called reconciliation.
Example: If a component’s state changes, React only updates the affected part instead of reloading the entire page.
Ways to Optimize Re-Renders
1. Use React.memo
to Prevent Unnecessary Renders
const MemoizedComponent = React.memo(({ name }) => <h1>{name}</h1>);
Prevents re-renders if name
prop hasn’t changed.
2. Use useCallback
for Memoizing Functions
const handleClick = useCallback(() => console.log("Clicked"), []);
Prevents function recreation on every render.
3. Use useMemo
for Expensive Computations
const calculatedValue = useMemo(() => expensiveFunction(data), [data]);
Only recalculates when data
changes.
4. Optimize Context API Usage
Avoid unnecessary re-renders by structuring Context providers efficiently or using selectors in global state management in tools like Redux.
5. Use Efficient List Rendering with key
Props
Only recalculates when data
changes.
4. Optimize Context API Usage
Avoid unnecessary re-renders by structuring Context providers efficiently or using selectors in global state management in tools like Redux.
5. Use Efficient List Rendering with key
Props
items.map((item) => <li key={item.id}>{item.name}</li>);
Helps React track list items efficiently.
6. Split Components to Minimize Updates
Move stateful logic to a separate component so only necessary parts re-render.
Conclusion
React optimizes rendering with reconciliation, and performance can be further improved using memoization (React.memo
, useMemo
, useCallback
), optimized list rendering, and proper state management.
What is the difference between React.memo, useMemo, and
useCallback, and when should you use each?
Difference Between React.memo
, useMemo
, and useCallback
and When to Use Them
All three are performance optimization tools in React, but they serve different purposes.
1. React.memo
– Prevents Unnecessary Re-Renders of Components
React.memo
memoizes a functional component and prevents it from re-rendering unless its props change.
Example:
const MemoizedComponent = React.memo(({ name }) => <h1>{name}</h1>);
Use React.memo
when:
- The component is pure and renders the same output for the same props.
- The component re-renders frequently without prop changes.
2. useMemo
– Memoizes Computed Values
useMemo
caches expensive computations to avoid recalculating them on every render.
Example:
const filteredData = useMemo(() => data.filter(item => item.active), [data]);
Use useMemo
when:
- A function returns a computed value (e.g., filtering, sorting, heavy calculations).
- The computation is expensive and should only run when dependencies change.
3. useCallback
– Memoizes Functions
useCallback
returns a memoized function, ensuring the function reference doesn’t change unless dependencies update.
Example:
const handleClick = useCallback(() => console.log("Clicked!"), []);
Use useCallback
when:
- Passing functions as props to child components to prevent unnecessary renders.
- Preventing function recreation on every render.
Key Differences
Use useCallback
when:
- Passing functions as props to child components to prevent unnecessary renders.
- Preventing function recreation on every render.
Key Differences
Use useCallback
when:
- Passing functions as props to child components to prevent unnecessary renders.
- Preventing function recreation on every render.
Key Differences
Feature | React.memo |
useMemo |
useCallback |
What it Memoizes | Whole component | Computed value | Function reference |
Purpose | Prevents re-rendering | Avoids recalculating values | Prevents function recreation |
When to Use | Memoize UI components | Optimize expensive calculations | Optimize function props |
Conclusion
- Use
React.memo
to prevent unnecessary re-renders of components. - Use
useMemo
to cache expensive calculations and avoid re-computing values. - Use
useCallback
to memoize functions, especially when passing them as props.
Using these optimizations correctly can improve React app performance and avoid unnecessary renders.
How do you handle expensive calculations in React components without affecting performance?
Expensive calculations can slow down React applications if they run on every render. To optimize performance, use memoization, background processing, and efficient state management.
1. Use useMemo
to Cache Expensive Computations
useMemo
caches computed values and only recalculates them when dependencies change.
Example:
const filteredData = useMemo(() => {
return data.filter(item => item.active);
}, [data]);
Use when:
- A function performs heavy calculations (e.g., filtering, sorting).
- The result doesn’t change frequently.
2. Use useCallback
to Prevent Function Recreation
useCallback
memoizes functions so they are not recreated on every render.
Example:
const computeValue = useCallback(() => {
return expensiveCalculation(data);
}, [data]);
Use when:
- Passing functions as props to child components.
3. Perform Expensive Calculations Outside the UI Thread (Web Workers)
For extremely heavy computations, offload work to Web Workers.
Example:
const worker = new Worker("worker.js");
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);
Use when:
- The calculation blocks the UI and affects responsiveness.
4. Optimize Rendering with React.memo
Use React.memo
to prevent unnecessary re-renders of components.
Example:
const MemoizedComponent = React.memo(MyComponent);
Use when:
- A component renders frequently with the same props.
5. Debounce Expensive Calls (e.g., Search Input)
Use debounce
to limit function calls and reduce unnecessary computations.
Example:
const debouncedSearch = useCallback(debounce((query) => fetchData(query), 500), []);
Use when:
- Handling real-time user input (e.g., search, autocomplete).
Conclusion
To handle expensive calculations efficiently in React:
- Use
useMemo
for caching computed values. - Use
useCallback
to optimize function references. - Offload heavy tasks to Web Workers if needed.
- Use
React.memo
to reduce unnecessary renders. - Apply debouncing for frequent event-based computations.
These techniques help keep React apps fast and responsive.
How does lazy loading with React.lazy and Suspense improve performance?
Lazy loading delays the loading of components until they are needed, reducing the initial JavaScript bundle size and improving performance.
How React.lazy
Works
React.lazy
dynamically imports components only when they are rendered, preventing unnecessary code from loading upfront.
Example:
import { lazy, Suspense } from "react";
const LazyComponent = lazy(() => import("./HeavyComponent"));
const App = () => (
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
);
How This Improves Performance:
- Reduces Initial Load Time – Only necessary components are loaded at first.
- Optimizes Large Applications – Splits code into smaller chunks, improving speed.
- Enhances User Experience – Displays a fallback UI (
Suspense
) while loading.
How Suspense
Works
Suspense
wraps lazy-loaded components and provides a fallback UI while loading.- Prevents UI flickering and keeps users informed during loading.
When to Use Lazy Loading?
- Large components that aren’t needed immediately (e.g., dashboards, charts).
- Route-based splitting in single-page applications (SPAs).
- Third-party libraries that don’t need to load upfront.
Conclusion
React.lazy
and Suspense
improve performance by delaying component loading, reducing bundle size, and improving initial load speed, making React apps faster and more efficient.
How does useRef differ from useState, and when should you use it?
Both useRef
and useState
store values in React, but they serve different purposes.
Key Differences
Feature | useRef |
useState |
Triggers Re-Renders | ❌ No | ✅ Yes |
Stores Mutable Values | ✅ Yes | ❌ No (state is immutable) |
Accesses DOM Elements | ✅ Yes | ❌ No |
Use Case | Persist values without re-renders | Store component state that affects rendering |
When to Use useRef
- Persist Values Without Causing Re-Renders
const countRef = useRef(0);
countRef.current += 1; // Updates without triggering a re-render
Best for: Storing previous values, timers, or caching data.
- Access and Manipulate DOM Elements
const inputRef = useRef(null);
<input ref={inputRef} />;
Best for: Focusing input fields or interacting with the DOM.
When to Use useState
- Trigger Re-Renders When State Changes
const [count, setCount] = useState(0);
setCount(count + 1); // Triggers a re-render
Best for: Managing UI-related data that changes over time.
Conclusion
- Use
useRef
when you need to store values without causing re-renders or access DOM elements. - Use
useState
when the value should trigger a re-render (e.g., UI updates).
What are the use cases for useLayoutEffect over useEffect?
Both useEffect
and useLayoutEffect
run after rendering, but useLayoutEffect
runs synchronously before the browser paints the screen, while useEffect
runs asynchronously after rendering.
When to Use useLayoutEffect
1. Measuring DOM Elements Before Paint
Use when you need to calculate layout (e.g., element size, position) before the browser updates the screen.
useLayoutEffect(() => {
const height = divRef.current.clientHeight;
console.log("Height before paint:", height);
}, []);
Best for: Dynamic layouts, animations, tooltips.
2. Preventing UI Flickering
If an effect modifies the DOM before it appears, useLayoutEffect
ensures changes happen before the paint.
useLayoutEffect(() => {
if (modalOpen) document.body.style.overflow = "hidden";
return () => document.body.style.overflow = "auto";
}, [modalOpen]);
Best for: Preventing flashes in modal dialogs or dynamic UI changes.
3. Synchronizing Scroll Position or Focus
useLayoutEffect(() => {
window.scrollTo(0, 0);
}, []);
Best for: Restoring scroll position when navigating pages.
When to Use useEffect
Instead
- For data fetching, event listeners, and updates that don’t block rendering.
- If DOM measurement or immediate updates aren’t required.
Conclusion
- Use
useLayoutEffect
when UI updates must happen before paint to prevent flickering. - Use
useEffect
for asynchronous tasks like API calls or event listeners.
How does the dependency array in useEffect work, and what are common mistakes when using it?
The dependency array in useEffect
controls when the effect runs by tracking specific values.
How the Dependency Array Works
useEffect(() => {
console.log("Effect runs when count changes");
}, [count]); // Runs only when `count` changes
Dependency Array | Behavior |
[] (empty) |
Runs only once after mount (like componentDidMount ). |
[var] |
Runs when var changes (like componentDidUpdate ). |
No array | Runs on every render. |
Common Mistakes When Using useEffect
1. Forgetting Dependencies (Causing Stale State)
useEffect(() => {
fetchData(); // Fetches old data if dependencies are missing
}, []); // ❌ Missing dependencies
Fix: Always list required dependencies:
useEffect(() => {
fetchData(query);
}, [query]); // ✅ Runs when `query` changes
2. Using Objects or Arrays as Dependencies
Objects and arrays are reference types, so React sees them as “new” on every render.
useEffect(() => {
console.log("Effect runs every time");
}, [{}]); // ❌ Always runs because `{}` is a new object
Fix: Use useMemo or primitive values instead.
const memoizedObj = useMemo(() => ({ key: value }), [value]);
useEffect(() => {}, [memoizedObj]); // ✅ Runs only when `value` changes
3. Causing Infinite Loops with State Updates
Updating state inside useEffect
without proper conditions causes infinite loops.
useEffect(() => {
setCount(count + 1); // ❌ Triggers a re-render on every update
}, [count]);
Fix: Use functional updates to prevent unnecessary re-renders
useEffect(() => {
setCount(prevCount => prevCount + 1);
}, []); // ✅ Runs only once
Conclusion
- Use the dependency array correctly to prevent unnecessary renders.
- Always list state or props that affect the effect to avoid stale data.
- Be careful with objects, arrays, and state updates to prevent infinite loops.
How would you create a custom React Hook for fetching data?
A custom Hook in React allows reusable logic, making code cleaner and more maintainable. A data-fetching Hook simplifies API requests across multiple components.
Steps to Create a Custom Hook for Fetching Data
- Use
useState
to manage loading, error, and data states. - Use
useEffect
to fetch data when the component mounts or dependencies change.
Example: useFetch
Hook
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch data");
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]); // Runs when `url` changes
return { data, loading, error };
};
export default useFetch;
Using the useFetch Hook
const Users = () => {
const { data, loading, error } = useFetch("https://api.example.com/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
};
Why Use a Custom Hook for Fetching Data?
- Reusability – Can be used in multiple components.
- Cleaner Code – Removes redundant fetch logic from components.
- Better State Management – Handles loading and error states efficiently.
Conclusion
Creating a useFetch
Hook makes API calls easier to manage, improves code reusability, and keeps components clean and focused on UI logic.
How do you manage complex state updates using useReducer?
useReducer
is a React Hook used for managing complex state logic in a structured way. It is an alternative to useState
, especially when multiple related state updates need to be handled.
How useReducer
Works
- Takes a reducer function that determines how state changes based on actions.
- Uses a dispatch function to trigger state updates.
- Returns the updated state.
Example: Managing a Counter with useReducer
import { useReducer } from "react";
// Reducer function
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
return state;
}
};
// Component using useReducer
const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
};
When to Use useReducer Instead of useState?
Feature | useState | useReducer |
Best for | Simple state updates | Complex state logic |
State structure | Single values | Objects with multiple fields |
State updates | Directly with setState |
Uses an action-based approach |
Performance | Can cause unnecessary re-renders | Optimized for structured state updates |
Use useReducer
when:
- The state has multiple related values (e.g., form handling).
- The state transitions are complex (e.g., authentication flow).
- The next state depends on the previous state.
Conclusion
useReducer
simplifies managing complex state logic by using a structured action-based approach, improving code organization and maintainability.
How does React Router handle dynamic routes, and how do you extract URL parameters?
React Router allows dynamic routing by defining route parameters (:id
) in the URL. These parameters can be accessed in components using the useParams
Hook.
1. Defining a Dynamic Route
Dynamic segments (:id
) allow URLs to change based on parameters.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UserProfile from "./UserProfile";
const App = () => (
<Router>
<Routes>
<Route path="/user/:id" element={<UserProfile />} />
</Routes>
</Router>
);
Example URLs: /user/1
, /user/42
2. Extracting URL Parameters Using useParams
The useParams
Hook retrieves parameters from the URL.
import { useParams } from "react-router-dom";
const UserProfile = () => {
const { id } = useParams(); // Extracts "id" from URL
return <h1>User ID: {id}</h1>;
};
Use when: You need to display or fetch data based on dynamic routes.
3. Navigating to a Dynamic Route
Use the useNavigate
Hook to programmatically navigate with dynamic parameters.
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
return <button onClick={() => navigate("/user/10")}>Go to User 10</button>;
};
Conclusion
React Router supports dynamic routes using :id
, and you can extract URL parameters using useParams()
. This is useful for user profiles, product pages, and other dynamic content-driven applications.
What is the difference between useNavigate and useHistory in React Router?
Difference Between useNavigate
and useHistory
in React Router
In React Router v6, useHistory
was replaced by useNavigate
for navigation within an app.
1. useHistory
(React Router v5 – Deprecated)
- Used to navigate programmatically.
- Provided a
history
object withpush
,replace
, andgoBack
methods.
Example (React Router v5)
import { useHistory } from "react-router-dom";
const Component = () => {
const history = useHistory();
return <button onClick={() => history.push("/dashboard")}>Go to Dashboard</button>;
};
Deprecated in React Router v6
2. useNavigate
(React Router v6 – Current)
- Replaces
useHistory
with a simplernavigate
function. - Supports relative and absolute paths.
Example (React Router v6)
import { useNavigate } from "react-router-dom";
const Component = () => {
const navigate = useNavigate();
return <button onClick={() => navigate("/dashboard")}>Go to Dashboard</button>;
};
Advantages of useNavigate
:
- Simpler syntax (
navigate(path)
) compared tohistory.push(path)
. - Supports
replace
andstate
(likehistory.replace
).
Example: Replacing History Entry
navigate("/login", { replace: true }); // Prevents going back to the previous page
Key Differences
Feature | useHistory (v5) |
useNavigate (v6) |
Status | Deprecated | Current |
Navigation | history.push("/path") |
navigate("/path") |
Replace Current Page | history.replace("/path") |
navigate("/path", { replace: true }) |
Go Back | history.goBack() |
navigate(-1) |
Conclusion
- Use
useNavigate
in React Router v6, asuseHistory
is deprecated. - It provides a simpler, more flexible way to navigate between pages.
How does server-side rendering (SSR) work with React Router?
Server-Side Rendering (SSR) in React generates the HTML on the server before sending it to the client. This improves performance, SEO, and initial page load speed. React Router supports SSR by handling routing on the server side.
How SSR Works with React Router
- Client Requests a Page – The browser requests a URL (e.g.,
/dashboard
). - Server Renders the React Component – The server pre-renders the matching route into HTML.
- HTML Sent to Client – The user sees the initial content immediately.
- Hydration – React attaches event listeners and makes the page interactive.
Example: SSR with React Router in Next.js
Next.js is a popular React framework with built-in SSR support.
1. Creating an SSR Page in Next.js
export async function getServerSideProps() {
const res = await fetch("https://api.example.com/data");
const data = await res.json();
return { props: { data } };
}
const Page = ({ data }) => <h1>{data.title}</h1>;
export default Page;
getServerSideProps
fetches data on the server before rendering.
Advantages of SSR with React Router
- Better SEO – Search engines can index pre-rendered content.
- Faster Initial Load – The user sees content immediately, even before React hydrates.
- Improved Performance – Reduces JavaScript execution time on the client side.
When to Use SSR?
- SEO-heavy pages (blogs, e-commerce).
- Dynamic content that requires fresh data on each request.
- Performance-sensitive applications.
Conclusion
React Router supports SSR by pre-rendering pages on the server, improving SEO and performance. Next.js is the preferred way to implement SSR in React applications.
What are the key differences between HashRouter, BrowserRouter, and MemoryRouter?
Key Differences Between HashRouter
, BrowserRouter
, and MemoryRouter
React Router provides different routing strategies based on the application’s environment and requirements.
1. BrowserRouter
(Default for Web Apps)
- Uses the HTML5 History API (
pushState
andreplaceState
). - Clean, user-friendly URLs (
/about
instead of#/about
). - Requires server-side configuration to handle direct page reloads.
Example:
import { BrowserRouter, Route, Routes } from "react-router-dom";
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</BrowserRouter>;
Best for: Standard web apps with a backend that supports route handling.
2. HashRouter
(Uses URL Hash for Routing)
- Uses the URL hash (
#
) to store routes (/#/about
instead of/about
). - No server configuration required (works in static file hosting).
- Less SEO-friendly than
BrowserRouter
.
Example:
import { HashRouter } from "react-router-dom";
<HashRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</HashRouter>;
Best for: Static websites (e.g., GitHub Pages) where the server doesn’t support route handling.
3. MemoryRouter
(Stores Routes in Memory)
- Keeps navigation history in memory, not in the browser’s URL.
- Doesn’t update the browser URL.
- Useful for testing and non-browser environments (e.g., React Native).
Example:
import { MemoryRouter } from "react-router-dom";
<MemoryRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</MemoryRouter>;
Best for: Unit tests and React Native apps.
Comparison Table
Feature | BrowserRouter |
HashRouter |
MemoryRouter |
URL Format | /about |
/#/about |
No URL change |
Requires Server Setup? | ✅ Yes | ❌ No | ❌ No |
SEO-Friendly? | ✅ Yes | ❌ No | ❌ No |
Use Case | Web apps | Static sites | Testing, React Native |
Conclusion
- Use
BrowserRouter
for most web apps with a server. - Use
HashRouter
for static sites where server configuration isn’t possible. - Use
MemoryRouter
for testing and environments where no browser history is needed.
How do you test React components that rely on async data fetching?
Testing components that fetch data asynchronously requires handling delays, promises, and UI state updates. React Testing Library (RTL) and Jest provide tools for simulating async behavior.
1. Example Component with Async Data Fetching
import { useState, useEffect } from "react";
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
2. Writing an Async Test with Jest & React Testing Library
import { render, screen, waitFor } from "@testing-library/react";
import UserList from "./UserList";
// Mock fetch API
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{ id: 1, name: "John Doe" }]),
})
);
test("renders user list after fetching data", async () => {
render(<UserList />);
// Check for loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for users to appear in the DOM
await waitFor(() => expect(screen.getByText("John Doe")).toBeInTheDocument());
});
What This Test Does:
- Mocks the
fetch
API to return fake data. - Asserts that the loading state appears first.
- Waits for the data to be rendered before testing.
3. Best Practices for Testing Async Components
- Use
waitFor()
– Waits for the component to update after async calls. - Mock API Requests – Prevents real network calls, making tests fast and reliable.
- Check Loading and Error States – Ensure the component handles different states correctly.
Conclusion
To test async components:
- Mock API calls using Jest.
- Use
waitFor
to wait for UI updates. - Test loading and final state to ensure correct behavior.
This ensures reliable and efficient testing for data-fetching React components.
What are the best practices for testing React components using Jest and React Testing Library?
React Testing Library (RTL) and Jest provide a robust way to test React components by focusing on user interactions and UI behavior rather than implementation details.
1. Test Components Like a User Would
Use screen.getByText
and screen.getByRole
instead of querying by class names or IDs.
render(<button>Click Me</button>);
expect(screen.getByText("Click Me")).toBeInTheDocument();
Why? Ensures tests are resilient to implementation changes.
2. Use waitFor
for Asynchronous Tests
Handle async data fetching with waitFor
.
await waitFor(() => expect(screen.getByText("User Loaded")).toBeInTheDocument());
Why? Prevents tests from failing due to delayed state updates.
3. Avoid Testing Implementation Details
Bad (Testing Internal State)
expect(component.state.count).toBe(1); // ❌ Avoid this
Good (Testing User Behavior)
fireEvent.click(screen.getByText("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
Why? Tests should focus on how the UI behaves, not how it’s implemented.
4. Mock API Calls Instead of Making Real Requests
Use jest.fn()
or msw
to mock API responses.
global.fetch = jest.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ name: "John Doe" }) })
);
Why? Avoids real API calls, making tests fast and reliable.
5. Use act()
for State Updates in Tests using React Testing Library
Ensure updates happen synchronously using act()
.
import { act } from "@testing-library/react";
act(() => {
setState(newState);
});
Why? Helps prevent warnings when React state updates asynchronously.
6. Clean Up After Tests using React Testing Library
Use afterEach(cleanup)
to reset the DOM between tests.
import { cleanup } from "@testing-library/react";
afterEach(() => {
cleanup();
});
Why? Prevents test pollution and inconsistent results.
Conclusion
To write reliable and maintainable tests:
- Test UI behavior, not implementation details.
- Mock APIs for predictable results.
- Use
waitFor
for async updates andact
for state changes. - Ensure accessibility by using
getByRole
andgetByText
.
Following these best practices improves test reliability, maintainability, and performance in React applications.
How do you handle testing for React components that use the Context API?
When testing React components that use the Context API, you need to wrap the component inside the provider and ensure the correct context values are passed.
1. Example: Context API Component
Let’s assume we have an AuthContext
that provides user authentication data.
import { createContext, useContext } from "react";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => (
<AuthContext.Provider value={{ user: { name: "John Doe" } }}>
{children}
</AuthContext.Provider>
);
export const useAuth = () => useContext(AuthContext);
A component consuming this context:
const UserProfile = () => {
const { user } = useAuth();
return <h1>{user ? `Welcome, ${user.name}` : "Guest"}</h1>;
};
export default UserProfile;
2. Testing a Context Consumer
To test UserProfile
, we must wrap it with AuthProvider
in the test.
import { render, screen } from "@testing-library/react";
import { AuthProvider } from "./AuthContext";
import UserProfile from "./UserProfile";
test("displays the logged-in user", () => {
render(
<AuthProvider>
<UserProfile />
</AuthProvider>
);
expect(screen.getByText("Welcome, John Doe")).toBeInTheDocument();
});
Why? This ensures the component correctly receives context values.
3. Mocking Context for Different Scenarios
Instead of using AuthProvider
, we can manually mock the context to test different cases.
import { render, screen } from "@testing-library/react";
import { AuthContext } from "./AuthContext";
import UserProfile from "./UserProfile";
test("displays guest message when user is not logged in", () => {
render(
<AuthContext.Provider value={{ user: null }}>
<UserProfile />
</AuthContext.Provider>
);
expect(screen.getByText("Guest")).toBeInTheDocument();
});
Why? This allows us to test multiple context states (e.g., logged-in vs. guest).
Conclusion
- Wrap components with the Context Provider when testing context-dependent components.
- Mock the Context Provider to test different scenarios.
- Use
screen.getByText
to verify UI behavior.
Following these steps ensures reliable and isolated tests for Context API components.
What is the difference between unit testing and integration testing in React applications?
Unit and integration testing ensure React applications work correctly, but they focus on different aspects of the application.
1. Unit Testing (Isolated Component Testing)
- Tests individual components or functions in isolation.
- Ensures that a specific function or UI element behaves as expected.
- Uses Jest and React Testing Library to verify component rendering and logic.
Example: Unit Testing a Button Component
import { render, screen } from "@testing-library/react";
import Button from "./Button";
test("renders button with correct label", () => {
render(<Button label="Click Me" />);
expect(screen.getByText("Click Me")).toBeInTheDocument();
});
Best for: Testing pure functions, UI elements, and hooks.
2. Integration Testing (Multiple Components Working Together)
- Tests how multiple components interact with each other.
- Ensures that a feature works as a whole (e.g., form submission, API requests).
- Often includes mocking API calls and state management tests.
Example: Integration Testing a Form Submission
import { render, screen, fireEvent } from "@testing-library/react";
import LoginForm from "./LoginForm";
test("submits form with correct input", () => {
render(<LoginForm />);
fireEvent.change(screen.getByLabelText("Username"), { target: { value: "user1" } });
fireEvent.click(screen.getByText("Submit"));
expect(screen.getByText("Login successful")).toBeInTheDocument();
});
Best for: Forms, API calls, and state management testing.
Key Differences
Feature | Unit Testing | Integration Testing |
Scope | Individual component/function | Multiple components working together |
Focus | Internal logic, rendering | Data flow, state updates, API calls |
Tools | Jest, React Testing Library | Jest, React Testing Library, MSW (Mock API) |
Use Case | UI elements, functions, hooks | Forms, API calls, authentication |
Conclusion
- Use Unit Testing for isolated component behavior.
- Use Integration Testing to verify how components work together.
- Both are essential for building a reliable React application.
How does React integrate with GraphQL for data fetching?
React integrates with GraphQL using libraries like Apollo Client and Relay, allowing efficient data fetching, caching, and state management.
1. Setting Up Apollo Client in React
Apollo Client is the most popular GraphQL client for React applications.
Install Apollo Client
npm install @apollo/client graphql
Configure Apollo Provider
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
const client = new ApolloClient({
uri: "https://api.example.com/graphql",
cache: new InMemoryCache(),
});
const App = () => (
<ApolloProvider client={client}>
<MyComponent />
</ApolloProvider>
);
2. Fetching Data with useQuery
Hook
GraphQL queries are used to fetch only the required data.
Example: Fetching Users
import { gql, useQuery } from "@apollo/client";
const GET_USERS = gql`
query {
users {
id
name
email
}
}
`;
const UserList = () => {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error fetching data</p>;
return (
<ul>
{data.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Advantages:
- Fetches only required fields (reduces over-fetching).
- Handles loading and error states automatically.
3. Mutations: Sending Data to GraphQL
Mutations allow modifying data on the server.
Example: Adding a New User
import { gql, useMutation } from "@apollo/client";
const ADD_USER = gql`
mutation AddUser($name: String!, $email: String!) {
addUser(name: $name, email: $email) {
id
name
}
}
`;
const AddUserForm = () => {
const [addUser] = useMutation(ADD_USER);
const handleSubmit = () => {
addUser({ variables: { name: "John Doe", email: "john@example.com" } });
};
return <button onClick={handleSubmit}>Add User</button>;
};
Conclusion
- Use Apollo Client for easy GraphQL integration in React.
- Use
useQuery
for fetching data anduseMutation
for modifying data. - GraphQL helps reduce over-fetching and provides a more efficient data structure.
This approach makes React applications faster, scalable, and more efficient for data-driven apps.
What are the benefits of using TypeScript with React, and how does it improve component development?
TypeScript enhances React development by adding static typing, improving code quality, maintainability, and developer experience.
1. Type Safety and Early Error Detection
- Prevents runtime errors by catching issues at compile time.
- Helps developers avoid common mistakes, such as passing incorrect props.
type ButtonProps = { label: string };
const Button: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>;
Ensures label
is always a string, reducing bugs.
2. Better Developer Experience (Intellisense & Autocomplete)
- Provides autocompletion, type hints, and documentation in IDEs.
- Makes it easier to understand function signatures and props.
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {};
Prevents incorrect event handling by enforcing proper event types.
3. Strongly Typed Props and State
- Helps document component props and state structure explicitly.
type User = { id: number; name: string };
const [user, setUser] = useState<User | null>(null);
Prevents incorrect state assignments and improves maintainability.
4. Improved Code Maintainability & Scalability
- Enforces consistent types across a project.
- Makes refactoring safer and easier to manage in large applications.
interface User {
id: number;
name: string;
}
const fetchUser = async (): Promise<User> => {
return { id: 1, name: "John Doe" };
};
Ensures API responses match expected types.
5. Better Control with Redux and Context API
- Defines action types, reducers, and store structure explicitly.
- Prevents type mismatches in state management.
interface AuthState {
user: User | null;
}
const authReducer = (state: AuthState, action: { type: string; payload?: User }) => {
switch (action.type) {
case "LOGIN":
return { ...state, user: action.payload };
default:
return state;
}
};
Conclusion
Using TypeScript with React provides:
- Type safety, reducing runtime errors.
- Better developer experience with auto-completion and hints.
- Stronger code maintainability for large applications.
It makes React apps more reliable, scalable, and easier to debug.
How would you handle WebSockets or real-time updates in a React application?
WebSockets enable real-time communication between the client and server, making them ideal for live chat, notifications, and real-time dashboards in React applications.
1. Using Native WebSockets in React
React’s useEffect
and useState
can manage WebSocket connections efficiently.
Example: WebSocket Connection in a Component
import { useEffect, useState } from "react";
const WebSocketComponent = () => {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket("wss://example.com/socket");
socket.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => socket.close(); // Cleanup on unmount
}, []);
return (
<ul>
{messages.map((msg, index) => (
<li key={index}>{msg}</li>
))}
</ul>
);
};
export default WebSocketComponent;
Why?
- Opens a persistent connection with the server.
- Listens for incoming messages and updates state.
- Closes connection when the component unmounts.
2. Using socket.io
for WebSockets
For better event handling and reconnection, use Socket.IO.
Installation
npm install socket.io-client
Example: Using Socket.IO in React
import { useEffect, useState } from "react";
import { io } from "socket.io-client";
const socket = io("https://example.com");
const ChatComponent = () => {
const [messages, setMessages] = useState([]);
useEffect(() => {
socket.on("message", (msg) => {
setMessages(prev => [...prev, msg]);
});
return () => socket.off("message");
}, []);
return (
<ul>
{messages.map((msg, index) => (
<li key={index}>{msg}</li>
))}
</ul>
);
};
Why?
- Automatic reconnection if connection drops.
- Supports rooms, namespaces, and event-based messaging.
3. Optimizing Performance for Real-Time Data
- Debounce state updates to prevent excessive re-renders.
- Use Web Workers for processing real-time data.
- Use Redux or Zustand for managing live updates efficiently.
Conclusion
To handle WebSockets in React:
- Use native WebSockets for basic real-time communication.
- Use Socket.IO for advanced event handling and reconnection.
- Optimize state updates to improve performance.
This approach ensures fast and reliable real-time updates in React applications.
How does React interact with Web Workers for performance-heavy tasks?
Web Workers allow React applications to run CPU-intensive tasks (e.g., data processing, image manipulation) in a separate thread, preventing UI freezes and improving performance.
1. Creating a Web Worker in React
A Web Worker runs JavaScript code in the background, separate from the main thread.
Example: Web Worker File (worker.js
)
self.onmessage = (event) => {
const result = event.data * 2; // Simulating heavy computation
postMessage(result);
};
2. Using Web Workers in a React Component
React communicates with the Web Worker via postMessage
and onmessage
.
import { useState, useEffect } from "react";
const HeavyComputation = () => {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(new URL("./worker.js", import.meta.url));
worker.postMessage(10); // Send data to worker
worker.onmessage = (event) => setResult(event.data); // Receive processed data
return () => worker.terminate(); // Cleanup on unmount
}, []);
return <p>Computed Result: {result}</p>;
};
export default HeavyComputation;
Why Use Web Workers?
- Offloads heavy computations from the main thread.
- Prevents UI blocking and lag in React apps.
- Ideal for large data processing, video encoding, and AI computations.
3. Using Web Workers with comlink
for Simpler Integration
comlink
abstracts Web Worker communication, making it easier to use.
npm install comlink
Example Using comlink
import { wrap } from "comlink";
const worker = new Worker(new URL("./worker.js", import.meta.url));
const workerAPI = wrap(worker);
const result = await workerAPI.heavyTask(10);
Why? Reduces complexity by eliminating postMessage
boilerplate.
Conclusion
- Use Web Workers to run heavy computations without blocking the UI.
- Communicate using
postMessage
or libraries likecomlink
. - Always terminate workers when not needed to free resources.
This ensures smooth and responsive React applications for performance-intensive tasks.
What are the key considerations when migrating a legacy React codebase to Next.js?
Migrating a React app to Next.js improves performance, SEO, and scalability, but requires careful planning.
1. Understanding Next.js Features
- Server-Side Rendering (SSR) – Pre-renders pages for better performance & SEO.
- Static Site Generation (SSG) – Builds static pages at compile time.
- API Routes – Backend functionality inside Next.js (
/pages/api
).
2. Restructuring the Project
Next.js follows a file-based routing system, so you’ll need to move components accordingly:
Convert src/components
to components/
(No major changes needed).
Move src/pages
to /app
(Next.js app router handles routing automatically).
Replace react-router-dom
with Next.js routing (next/link
, next/router
).
3. Updating Routing Logic
React Router (Legacy Code)
import { BrowserRouter, Route } from "react-router-dom";
<BrowserRouter>
<Route path="/about" component={About} />
</BrowserRouter>;
Next.js Routing (New Code)
import Link from "next/link";
<Link href="/about">Go to About</Link>;
Next.js Dynamic Routing
// pages/user/[id].js
import { useRouter } from "next/router";
const User = () => {
const { query } = useRouter();
return <h1>User ID: {query.id}</h1>;
};
export default User;
4. Handling Data Fetching
- Replace
useEffect
API calls with Next.js data-fetching methods if you don’t need client-side rendering:getServerSideProps()
(SSR)getStaticProps()
(SSG)
export async function getServerSideProps() {
const res = await fetch("https://api.example.com/data");
const data = await res.json();
return { props: { data } };
}
5. Managing Static Assets and Styles
Move /public
assets (images, icons) to Next.js /public/
folder.
Use CSS modules (.module.css
) or styled-components instead of global CSS.
6. Updating API Calls (Optional)
Next.js has built-in API routes, so you can move API logic from the frontend to /app/api
.
// app/api/{somepath}/router.js
export async function GET(request) {
return new Response(JSON.stringify({ message: "Hello from Next.js API!" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
7. SEO and Performance Optimization
Use next/head
for SEO metadata.
Use next/image
for optimized image loading.
Enable Automatic Static Optimization for faster page loads.
Conclusion
Migrating to Next.js involves:
- Restructuring the project for file-based routing.
- Updating navigation & API calls for SSR/SSG.
- Optimizing SEO & performance using Next.js features.
This transition enhances performance, scalability, and developer experience while keeping the core React logic intact.
React.js Coding tasks
Counter with Undo/Redo (useState)
- Implement a counter with increment, decrement, reset, and undo/redo functionality.
Here is the implementation of a Counter Component with Undo/Redo functionality using useState
:
Features:
- Increment, Decrement, and Reset counter.
- Undo/Redo functionality to revert or restore previous values.
- Prevents unnecessary state updates when there’s nothing to undo or redo.
Counter with Undo/Redo (useState
)
import { useState } from "react";
const CounterWithUndoRedo = () => {
const [count, setCount] = useState(0);
const [history, setHistory] = useState([0]); // Stores past counter values
const [index, setIndex] = useState(0); // Tracks current position in history
const updateCount = (newCount) => {
const updatedHistory = history.slice(0, index + 1); // Trim future states
setHistory([...updatedHistory, newCount]);
setIndex(updatedHistory.length);
setCount(newCount);
};
const increment = () => updateCount(count + 1);
const decrement = () => updateCount(count > 0 ? count - 1 : 0);
const reset = () => updateCount(0);
const undo = () => {
if (index > 0) {
setIndex(index - 1);
setCount(history[index - 1]);
}
};
const redo = () => {
if (index < history.length - 1) {
setIndex(index + 1);
setCount(history[index + 1]);
}
};
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>Counter: {count}</h2>
<button onClick={increment}>Increment</button>
<button onClick={decrement} disabled={count === 0}>Decrement</button>
<button onClick={reset}>Reset</button>
<button onClick={undo} disabled={index === 0}>Undo</button>
<button onClick={redo} disabled={index === history.length - 1}>Redo</button>
</div>
);
};
export default CounterWithUndoRedo;
How It Works:
- State Tracking:
count
: Stores the current counter value.history
: Maintains a list of past counter values.index
: Tracks the current position in history.
- Updating State:
- When
increment
,decrement
, orreset
is triggered, it updates the counter and adds the new value to thehistory
. undo
moves back in the history array without modifying it.redo
moves forward in the history if there are available future values.
- When
- Button Disabling:
Decrement
is disabled if count is0
.Undo
is disabled if there’s no previous state.Redo
is disabled if there’s no future state.
Why This Approach?
- Ensures a clear undo/redo history with efficient state updates.
- Prevents unnecessary updates by trimming future states when new actions are taken.
- Maintains a simple and readable structure while implementing full undo/redo functionality.
- 2. Toggle Theme (
useState
& CSS Variables)2. Implement the Theme Toggle Component
Debounced Search (useState & useEffect)
- Implement a search input that updates results only after the user stops typing (debouncing).
This component implements a search input with debounce functionality, ensuring that API calls or expensive computations only trigger after the user stops typing.
- Uses
useState
for managing search input. - Uses
useEffect
withsetTimeout
for debouncing input changes. - Prevents unnecessary re-renders and API calls on every keystroke.
1. Implement the Debounced Search Component
import { useState, useEffect } from "react";
const DebouncedSearch = () => {
const [searchTerm, setSearchTerm] = useState(""); // Stores user input
const [debouncedTerm, setDebouncedTerm] = useState(""); // Stores debounced value
useEffect(() => {
// Set a delay before updating the debounced value
const timer = setTimeout(() => {
setDebouncedTerm(searchTerm);
}, 500); // Adjust debounce delay (500ms)
return () => clearTimeout(timer); // Cleanup timeout on each input change
}, [searchTerm]);
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>Debounced Search</h2>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<p>Debounced Value: {debouncedTerm}</p>
</div>
);
};
export default DebouncedSearch;
How It Works:
- User types in the input field →
searchTerm
updates immediately. - Debounced effect triggers after 500ms of inactivity →
debouncedTerm
updates. - If the user types before 500ms, the timeout resets, preventing unnecessary updates.
- Debounced value is used for API calls or expensive computations instead of updating on every keystroke.
Why This Approach?
- Reduces API calls by ensuring requests only happen after a delay.
- Improves performance by preventing unnecessary re-renders.
- Easily configurable → Adjust
setTimeout
delay as needed.
Multi-Step Form (useState)
- Create a 3-step form (e.g., personal info, address, confirmation).
- Allow navigation between steps.
Multi-Step Form (useState
)
This component implements a 3-step form that allows users to:
- Enter personal information (Step 1).
- Enter address details (Step 2).
- Review and submit the form (Step 3).
- Navigate between steps using Next & Back buttons.
1. Implement the Multi-Step Form Component
import { useState } from "react";
const MultiStepForm = () => {
const [step, setStep] = useState(1); // Track current step
const [formData, setFormData] = useState({
name: "",
email: "",
address: "",
city: "",
});
// Handle input changes
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// Move to the next step
const nextStep = () => setStep((prev) => prev + 1);
// Move to the previous step
const prevStep = () => setStep((prev) => prev - 1);
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
alert("Form submitted successfully!");
console.log("Submitted Data:", formData);
};
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Multi-Step Form</h2>
{/* Step 1: Personal Info */}
{step === 1 && (
<div>
<h3>Step 1: Personal Info</h3>
<input
type="text"
name="name"
placeholder="Full Name"
value={formData.name}
onChange={handleChange}
/>
<br />
<input
type="email"
name="email"
placeholder="Email"
value={formData.email}
onChange={handleChange}
/>
<br />
<button onClick={nextStep} disabled={!formData.name || !formData.email}>
Next
</button>
</div>
)}
{/* Step 2: Address */}
{step === 2 && (
<div>
<h3>Step 2: Address</h3>
<input
type="text"
name="address"
placeholder="Street Address"
value={formData.address}
onChange={handleChange}
/>
<br />
<input
type="text"
name="city"
placeholder="City"
value={formData.city}
onChange={handleChange}
/>
<br />
<button onClick={prevStep}>Back</button>
<button onClick={nextStep} disabled={!formData.address || !formData.city}>
Next
</button>
</div>
)}
{/* Step 3: Confirmation */}
{step === 3 && (
<div>
<h3>Step 3: Confirmation</h3>
<p><strong>Name:</strong> {formData.name}</p>
<p><strong>Email:</strong> {formData.email}</p>
<p><strong>Address:</strong> {formData.address}, {formData.city}</p>
<button onClick={prevStep}>Back</button>
<button onClick={handleSubmit}>Submit</button>
</div>
)}
</div>
);
};
export default MultiStepForm;
How It Works:
step
state controls navigation between Step 1, Step 2, and Step 3.formData
stores user input, updating as users type.- Next button is disabled if required fields are empty.
- Submit button displays form data and logs it to the console.
Why This Approach?
- Simple state-based navigation using
useState
. - Prevents incomplete submissions with validation.
- Encapsulated steps for easy expansion (e.g., adding more fields).
Countdown Timer (useEffect & useRef)
- Build a timer that counts down from a given time and stops at zero.
- Provide start, pause, and reset buttons.
Countdown Timer (useEffect
& useRef
)
This component implements a countdown timer that:
- Starts counting down from a given time.
- Stops at zero when the countdown completes.
- Provides Start, Pause, and Reset buttons.
- Uses
useEffect
for handling side effects. - Uses
useRef
to store the timer reference and prevent unnecessary re-renders.
1. Implement the Countdown Timer Component
import { useState, useEffect, useRef } from "react";
const CountdownTimer = () => {
const [time, setTime] = useState(60); // Initial countdown time (in seconds)
const [isRunning, setIsRunning] = useState(false);
const timerRef = useRef(null); // Store interval reference
// Effect to handle countdown
useEffect(() => {
if (isRunning && time > 0) {
timerRef.current = setInterval(() => {
setTime((prevTime) => prevTime - 1);
}, 1000);
} else {
clearInterval(timerRef.current);
}
return () => clearInterval(timerRef.current); // Cleanup on unmount
}, [isRunning, time]);
// Start countdown
const startTimer = () => {
if (time > 0) setIsRunning(true);
};
// Pause countdown
const pauseTimer = () => {
setIsRunning(false);
};
// Reset countdown
const resetTimer = () => {
setIsRunning(false);
setTime(60); // Reset time to initial value
};
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>Countdown Timer</h2>
<h1>{time}s</h1>
<button onClick={startTimer} disabled={isRunning || time === 0}>Start</button>
<button onClick={pauseTimer} disabled={!isRunning}>Pause</button>
<button onClick={resetTimer}>Reset</button>
</div>
);
};
export default CountdownTimer;
How It Works:
useState
for time tracking → Stores the countdown value and running state.useEffect
for timer control → Runs whenisRunning
changes.useRef
for interval management → Ensures the interval clears when needed.- Prevents negative countdown → Stops at zero automatically.
- Disables inappropriate buttons → Prevents invalid actions (e.g., “Start” when running).
Why This Approach?
- Prevents memory leaks using
useRef
to manage intervals. - Prevents re-renders when toggling start/stop.
- User-friendly UI with disabled button states.
Fetch & Display Data (useEffect)
- Fetch data from an API (
https://jsonplaceholder.typicode.com/users
). - Display a loading indicator and handle errors.
Fetch & Display Data (useEffect
)
This component fetches user data from an API, handles loading and errors, and displays the results in a list.
- Uses
useEffect
to fetch data when the component mounts. - Handles loading and errors gracefully.
- Displays fetched data dynamically.
1. Implement the Fetch & Display Component
import { useState, useEffect } from "react";
const UserList = () => {
const [users, setUsers] = useState([]); // Store user data
const [loading, setLoading] = useState(true); // Track loading state
const [error, setError] = useState(null); // Store error messages
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false); // Ensure loading stops after fetch completes
}
};
fetchUsers();
}, []); // Empty dependency array ensures it runs once on mount
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
};
export default UserList;
How It Works:
useState
manages data, loading, and error states.useEffect
triggers the API call on mount.- Handles API errors with a
try-catch
block. - Prevents re-fetching by using an empty dependency array (
[]
). - Displays different UI states:
"Loading users..."
while fetching."Error: message"
if the fetch fails.- List of users once data is loaded.
Why This Approach?
- Prevents multiple API calls (fetches once on mount).
- Handles network errors properly.
- User-friendly experience with a loading indicator.
Infinite Scroll (useEffect
& Intersection Observer)
Infinite Scroll (useEffect
& Intersection Observer)
This component implements infinite scrolling, where more items load dynamically as the user scrolls down.
- Uses
useEffect
to fetch paginated data. - Uses Intersection Observer API to detect when the user reaches the bottom.
- Prevents unnecessary API calls by tracking loading state.
Implement the Infinite Scroll Component
- Fetch paginated data and load more items when the user scrolls down.
import {useState, useEffect, useRef} from "react";
const InfiniteScroll = () => {
const [items, setItems] = useState([]); // Stores fetched items
const [page, setPage] = useState(1); // Current page
const [loading, setLoading] = useState(false); // Loading state
const [hasMore, setHasMore] = useState(true); // Whether more data is available
const observerRef = useRef(null); // Reference for the last element
const loadingRef = useRef(loading);
// Keep the loadingRef updated
useEffect(() => {
loadingRef.current = loading;
}, [loading]);
// Fetch data whenever the page changes
useEffect(() => {
const fetchItems = async () => {
setLoading(true);
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`
);
const data = await response.json();
if (data.length === 0) {
// No more data available, stop further fetches
setHasMore(false);
} else {
setItems((prevItems) => [...prevItems, ...data]);
}
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
if (hasMore) {
fetchItems();
}
}, [page, hasMore]);
// Set up the observer only once
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
// If the observed element is in view, not loading, and more data is available, load next page
if (entry.isIntersecting && !loadingRef.current && hasMore) {
setPage((prevPage) => prevPage + 1);
}
},
{
// Adjust threshold as needed (lower threshold triggers earlier)
threshold: 0.5,
}
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, [hasMore]);
return (
<div style={{
textAlign: "center",
maxWidth: "600px",
margin: "auto",
}}>
<h2>Infinite Scroll</h2>
<ul>
{items.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
<div ref={observerRef} style={{height: "20px"}}></div>
{loading && <p>Loading more items...</p>}
{!hasMore && <p>No more items to load.</p>}
</div>
);
};
export default InfiniteScroll;
How It Works:
useState
stores items, page number, and loading state.useEffect
fetches paginated data wheneverpage
updates.- Intersection Observer detects when the user reaches the bottom.
- When the last item enters the viewport,
page
increments, triggering a new fetch.
- When the last item enters the viewport,
- New data is appended to the existing list without losing previous items.
Why This Approach?
- Efficient scrolling with no manual button clicks.
- Prevents unnecessary re-renders by tracking the last item.
- Scales well with large datasets.
Autocomplete Search (useState & API)
- Implement an autocomplete search that fetches and displays matching results dynamically.
Autocomplete Search (useState
& API)
This component implements an autocomplete search that fetches and displays matching results dynamically as the user types.
- Uses
useState
for managing input and results. - Uses
useEffect
for fetching API data dynamically. - Implements debouncing to prevent excessive API requests.
- Handles loading and error states for better UX.
1. Implement the Autocomplete Search Component
import { useState, useEffect } from "react";
const AutocompleteSearch = () => {
const [query, setQuery] = useState(""); // Stores user input
const [results, setResults] = useState([]); // Stores fetched results
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [debouncedQuery, setDebouncedQuery] = useState("");
// Debounce effect to reduce API calls
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 500); // Adjust debounce delay
return () => clearTimeout(timer);
}, [query]);
// Fetch results when debounced query updates
useEffect(() => {
if (!debouncedQuery) {
setResults([]); // Clear results if input is empty
return;
}
const fetchResults = async () => {
setLoading(true);
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users?q=${debouncedQuery}`
);
if (!response.ok) throw new Error("Failed to fetch data");
const data = await response.json();
setResults(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchResults();
}, [debouncedQuery]); // Runs when debounced input updates
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Autocomplete Search</h2>
<input
type="text"
placeholder="Search users..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
<ul>
{results.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default AutocompleteSearch;
How It Works:
- Debouncing:
setTimeout
waits 500ms before updating thedebouncedQuery
state.- If the user types again before 500ms, the timer resets.
- This prevents excessive API requests.
- Fetching Data:
- Runs when
debouncedQuery
updates. - Fetches results from an API based on user input.
- Clears results if input is empty.
- Runs when
- Error & Loading Handling:
- Displays “Loading…” while fetching.
- Shows error messages if the API request fails.
Why This Approach?
- Efficient API usage with debounced requests.
- Prevents unnecessary re-fetching on every keystroke.
- Improves user experience with error handling & loading states
Cache API Responses (useState & useEffect)
- Cache API responses using local state to prevent redundant network requests.
Cache API Responses (useState
& useEffect
)
This component caches API responses in local state, preventing redundant network requests when fetching the same data.
- Uses
useState
to store API responses and manage state. - Uses
useEffect
to trigger API calls when needed. - Stores previously fetched results in local state cache.
- Displays loading and error states for better user experience.
1. Implement the Cached API Fetching Component
import { useState, useEffect } from "react";
const CachedFetch = () => {
const [query, setQuery] = useState(""); // User input for search
const [results, setResults] = useState([]); // Stores API results
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [cache, setCache] = useState({}); // Local state cache
useEffect(() => {
if (!query) {
setResults([]);
return;
}
// Check if query exists in cache
if (cache[query]) {
setResults(cache[query]); // Use cached data
return;
}
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users?q=${query}`
);
if (!response.ok) throw new Error("Failed to fetch data");
const data = await response.json();
setResults(data);
setCache((prevCache) => ({ ...prevCache, [query]: data })); // Store in cache
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [query]); // Runs when query changes
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Cached API Fetch</h2>
<input
type="text"
placeholder="Search users..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{loading && <p>Loading...</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
<ul>
{results.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default CachedFetch;
How It Works:
- Checks Cache Before Fetching:
- If the query exists in
cache
, it returns stored results instead of fetching.
- If the query exists in
- Fetches Data Only When Needed:
- If no cache is found, performs API request.
- Stores response in
cache
for future use.
- Efficient State Handling:
- Displays loading while fetching.
- Handles errors gracefully.
- Clears results when input is empty.
Why This Approach?
- Reduces redundant API calls by caching data locally.
- Improves performance with instant cached results.
- Provides a smooth user experience with loading/error handling.
Protected Routes (React Router)
- Implement a protected route component that restricts access to authenticated users.
Protected Routes (React Router)
This implementation ensures that only authenticated users can access certain routes in a React app using React Router.
- Uses React Router v6 for routing.
- Uses
useContext
to manage authentication state. - Redirects unauthenticated users to the login page.
1. Set Up Authentication Context
Create an AuthContext
to manage authentication state.
import { createContext, useContext, useState } from "react";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => setIsAuthenticated(true);
const logout = () => setIsAuthenticated(false);
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
2. Create a Protected Route Component
Redirects users to login if they are not authenticated.
import { Navigate } from "react-router-dom";
import { useAuth } from "./AuthContext";
const ProtectedRoute = ({ element }) => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? element : <Navigate to="/login" />;
};
export default ProtectedRoute;
3. Define Routes in App.js
Wrap the protected page with ProtectedRoute
.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import LoginPage from "./LoginPage";
import Dashboard from "./Dashboard";
import ProtectedRoute from "./ProtectedRoute";
const App = () => (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} />} />
</Routes>
</Router>
</AuthProvider>
);
export default App;
4. Implement Login & Logout
Simulate user authentication with a Login button.
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";
const LoginPage = () => {
const { login } = useAuth();
const navigate = useNavigate();
const handleLogin = () => {
login();
navigate("/dashboard"); // Redirect after login
};
return (
<div style={{ textAlign: "center" }}>
<h2>Login Page</h2>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default LoginPage;
5. Dashboard (Protected Page)
This page is only accessible when logged in.
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";
const Dashboard = () => {
const { logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/login");
};
return (
<div style={{ textAlign: "center" }}>
<h2>Welcome to Dashboard</h2>
<button onClick={handleLogout}>Logout</button>
</div>
);
};
export default Dashboard;
How It Works:
AuthContext
stores the authentication state.ProtectedRoute
checks authentication before rendering the protected page.- Unauthenticated users are redirected to the login page.
- Login/Logout buttons update the auth state and navigate accordingly.
Why This Approach?
- Reusable ProtectedRoute component for secure pages.
- Scalable Authentication Context for managing state globally.
- Smooth user navigation with
useNavigate()
.
Dynamic Routing (React Router)
- Create a user profile page that dynamically loads data based on the URL parameter (
/user/:id
).
Protected Routes (React Router)
This implementation ensures that only authenticated users can access certain routes in a React app using React Router.
- Uses React Router v6 for routing.
- Uses
useContext
to manage authentication state. - Redirects unauthenticated users to the login page.
1. Set Up Authentication Context
Create an AuthContext
to manage authentication state.
import { createContext, useContext, useState } from "react";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => setIsAuthenticated(true);
const logout = () => setIsAuthenticated(false);
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
2. Create a Protected Route Component
Redirects users to login if they are not authenticated.
import { Navigate } from "react-router-dom";
import { useAuth } from "./AuthContext";
const ProtectedRoute = ({ element }) => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? element : <Navigate to="/login" />;
};
export default ProtectedRoute;
3. Define Routes in App.js
Wrap the protected page with ProtectedRoute
.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import LoginPage from "./LoginPage";
import Dashboard from "./Dashboard";
import ProtectedRoute from "./ProtectedRoute";
const App = () => (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} />} />
</Routes>
</Router>
</AuthProvider>
);
export default App;
4. Implement Login & Logout
Simulate user authentication with a Login button.
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";
const LoginPage = () => {
const { login } = useAuth();
const navigate = useNavigate();
const handleLogin = () => {
login();
navigate("/dashboard"); // Redirect after login
};
return (
<div style={{ textAlign: "center" }}>
<h2>Login Page</h2>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default LoginPage;
5. Dashboard (Protected Page)
This page is only accessible when logged in.
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";
const Dashboard = () => {
const { logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/login");
};
return (
<div style={{ textAlign: "center" }}>
<h2>Welcome to Dashboard</h2>
<button onClick={handleLogout}>Logout</button>
</div>
);
};
export default Dashboard;
How It Works:
AuthContext
stores the authentication state.ProtectedRoute
checks authentication before rendering the protected page.- Unauthenticated users are redirected to the login page.
- Login/Logout buttons update the auth state and navigate accordingly.
Why This Approach?
- Reusable ProtectedRoute component for secure pages.
- Scalable Authentication Context for managing state globally.
- Smooth user navigation with
useNavigate()
.
Breadcrumb Navigation (useLocation)
- Build a breadcrumb navigation system based on the current route.
Breadcrumb Navigation (useLocation
)
This component dynamically generates breadcrumb navigation based on the current route using React Router’s useLocation
.
- Uses
useLocation
to get the current path. - Dynamically creates breadcrumb links based on the path.
- Uses React Router’s
Link
for navigation.
1. Implement the Breadcrumb Component (this doesn’t handle cases with query parameters, path parameters, etc.)
import { Link, useLocation } from "react-router-dom";
const Breadcrumbs = () => {
const location = useLocation();
const pathnames = location.pathname.split("/").filter((path) => path);
return (
<nav style={{ padding: "10px" }}>
<Link to="/">Home</Link>
{pathnames.map((path, index) => {
const routeTo = `/${pathnames.slice(0, index + 1).join("/")}`;
return (
<span key={routeTo}>
{" > "}
<Link to={routeTo}>{path}</Link>
</span>
);
})}
</nav>
);
};
export default Breadcrumbs;
2. Use Breadcrumbs in Your App
Place the <Breadcrumbs />
component inside the main layout.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Breadcrumbs from "./Breadcrumbs";
import HomePage from "./HomePage";
import Products from "./Products";
import ProductDetails from "./ProductDetails";
const App = () => {
return (
<Router>
<Breadcrumbs />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetails />} />
</Routes>
</Router>
);
};
export default App;
3. Example Pages for Navigation
HomePage.js
const HomePage = () => <h2>Home Page</h2>;
export default HomePage;
Products.js
import { Link } from "react-router-dom";
const Products = () => (
<div>
<h2>Products Page</h2>
<Link to="/products/1">Go to Product 1</Link>
</div>
);
export default Products;
ProductDetails.js
import { useParams } from "react-router-dom";
const ProductDetails = () => {
const { id } = useParams();
return <h2>Product Details for Product {id}</h2>;
};
export default ProductDetails;
How It Works:
useLocation
extracts the current path.- Splits the path into segments, creating breadcrumb links dynamically.
- Each segment is clickable, allowing navigation to previous pages.
- Automatically updates when the user navigates.
Why This Approach?
- Dynamic Breadcrumbs – Works for any route structure.
- Reusable Component – Can be used across the app.
- Enhances UX – Provides clear navigation history.
Scroll Restoration on Route Change (useEffect)
- Ensure the page scrolls to the top when navigating between routes.
Scroll Restoration on Route Change (useEffect
)
This implementation ensures that the page automatically scrolls to the top when the user navigates between different routes using React Router.
- Uses
useEffect
to listen for route changes. - Uses
useLocation
from React Router to detect navigation. - Improves user experience by preventing unwanted scroll positions.
1. Implement the Scroll Restoration Component
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
const ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0); // Scroll to top on route change
}, [pathname]);
return null; // This component doesn't render anything
};
export default ScrollToTop;
2. Use the Component in App.js
Place <ScrollToTop />
inside <Router>
so it runs on every route change.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ScrollToTop from "./ScrollToTop";
import HomePage from "./HomePage";
import AboutPage from "./AboutPage";
import ContactPage from "./ContactPage";
const App = () => {
return (
<Router>
<ScrollToTop /> {/* Ensures scroll resets on route change */}
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</Router>
);
};
export default App;
3. Example Pages for Navigation
HomePage.js
const HomePage = () => (
<div>
<h2>Home Page</h2>
<p>Scroll down, then navigate to another page.</p>
</div>
);
export default HomePage;
AboutPage.js
const AboutPage = () => (
<div>
<h2>About Page</h2>
<p>This page should always start from the top when loaded.</p>
</div>
);
export default AboutPage;
ContactPage.js
const ContactPage = () => (
<div>
<h2>Contact Page</h2>
<p>Ensure the page starts from the top on navigation.</p>
</div>
);
export default ContactPage;
How It Works:
useLocation
detects route changes.useEffect
triggerswindow.scrollTo(0, 0)
whenever the route changes.- Ensures a smooth user experience, preventing unwanted scroll positions.
Why This Approach?
- Lightweight & efficient – Uses built-in browser scrolling.
- Reusable Component – Works across the entire app.
- Improves UX – Avoids unwanted scroll positions when navigating.
Sidebar Navigation with Active Links
Sidebar Navigation with Active Links (Using useLocation
)
This implementation creates a sidebar navigation where the active link is highlighted based on the current route using React Router’s useLocation
.
- Uses
useLocation
to detect the active route dynamically. - Highlights the active link by comparing the current pathname.
- Uses CSS styles to differentiate active and inactive links.
1. Implement the Sidebar Component
import { Link, useLocation } from "react-router-dom";
import "./Sidebar.css"; // Import styles
const Sidebar = () => {
const location = useLocation(); // Get current route
return (
<nav className="sidebar">
<Link to="/" className={location.pathname === "/" ? "active-link" : ""}>
Home
</Link>
<Link to="/about" className={location.pathname === "/about" ? "active-link" : ""}>
About
</Link>
<Link to="/services" className={location.pathname === "/services" ? "active-link" : ""}>
Services
</Link>
<Link to="/contact" className={location.pathname === "/contact" ? "active-link" : ""}>
Contact
</Link>
</nav>
);
};
export default Sidebar;
2. Sidebar Styles (Sidebar.css)
.sidebar {
width: 200px;
height: 100vh;
background: #f4f4f4;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar a {
text-decoration: none;
color: #333;
padding: 10px;
display: block;
border-radius: 5px;
transition: background 0.3s ease;
}
.sidebar a:hover {
background: #ddd;
}
.active-link {
background: #007bff;
color: white;
font-weight: bold;
}
3. Use the Sidebar in App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Sidebar from "./Sidebar";
import Home from "./Home";
import About from "./About";
import Services from "./Services";
import Contact from "./Contact";
const App = () => {
return (
<Router>
<div style={{ display: "flex" }}>
<Sidebar />
<div style={{ padding: "20px", flex: 1 }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/services" element={<Services />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
</div>
</Router>
);
};
export default App;
4. Example Pages
Home.js
const Home = () => <h2>Home Page</h2>;
export default Home;
About.js
const About = () => <h2>About Page</h2>;
export default About;
Services.js
const Services = () => <h2>Services Page</h2>;
export default Services;
Contact.js
const Contact = () => <h2>Contact Page</h2>;
export default Contact;
How It Works:
useLocation
retrieves the current pathname, allowing us to dynamically check which link should be highlighted.Link
elements use conditional class assignment, applying"active-link"
only when the pathname matches the link’s destination.- CSS highlights the active link, ensuring a visual indication of the current page.
Why This Approach?
- Directly uses
useLocation
to detect active routes, ensuring accuracy. - More control over styling and logic, compared to
NavLink
. - Easy to extend by adding more links, icons, or animations.
Reusable Form Input Component (useState)
- Create a reusable input component that supports validation and custom error messages.
Reusable Form Input Component (useState
)
This implementation creates a reusable input component that:
-
- Supports custom validation and error messages.
- Uses
useState
to manage input state. - Works with different input types (text, email, password, etc.).
1. Implement the Reusable Input Component
import { useState } from "react";
const InputField = ({ label, type, name, value, onChange, validate, errorMessage }) => {
const [error, setError] = useState("");
const handleBlur = () => {
if (validate && !validate(value)) {
setError(errorMessage);
} else {
setError("");
}
};
return (
<div style={{ marginBottom: "15px" }}>
<label>{label}</label>
<input
type={type}
name={name}
value={value}
onChange={onChange}
onBlur={handleBlur}
style={{ display: "block", padding: "8px", width: "100%" }}
/>
{error && <p style={{ color: "red", fontSize: "12px" }}>{error}</p>}
</div>
);
};
export default InputField;
2. Use the Reusable Input Component in a Form
import { useState } from "react";
import InputField from "./InputField";
const validateEmail = (email) => /\S+@\S+\.\S+/.test(email);
const validatePassword = (password) => password.length >= 6;
const FormExample = () => {
const [formData, setFormData] = useState({ email: "", password: "" });
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
alert("Form submitted successfully!");
};
return (
<form onSubmit={handleSubmit} style={{ maxWidth: "400px", margin: "auto" }}>
<h2>Reusable Input Form</h2>
<InputField
label="Email"
type="email"
name="email"
value={formData.email}
onChange={handleChange}
validate={validateEmail}
errorMessage="Invalid email format"
/>
<InputField
label="Password"
type="password"
name="password"
value={formData.password}
onChange={handleChange}
validate={validatePassword}
errorMessage="Password must be at least 6 characters"
/>
<button type="submit">Submit</button>
</form>
);
};
export default FormExample;
How It Works:
- Reusability –
InputField
works for any input type with validation. - Validation on Blur – Shows an error only when the user moves away from the field.
- Dynamic Error Messages – Displays custom error messages based on validation rules.
- Easy Form Integration – Can be used in any form with different input fields.
Why This Approach?
- Encapsulates validation logic inside a reusable component.
- Supports different validation rules (email, password, etc.).
- Keeps the form clean by avoiding duplicate validation logic.
File Upload with Preview
- Allow users to upload an image file and display a preview before submission.
File Upload with Preview
This implementation allows users to upload an image file and preview it before submission.
- Uses
useState
to manage file and preview state. - Supports image validation (only allows images).
- Displays image preview before submission.
1. Implement the File Upload Component
import { useState } from "react";
const FileUpload = () => {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const [error, setError] = useState("");
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
if (selectedFile) {
if (!selectedFile.type.startsWith("image/")) {
setError("Only image files are allowed");
setFile(null);
setPreview(null);
return;
}
setError("");
setFile(selectedFile);
setPreview(URL.createObjectURL(selectedFile));
}
};
const handleSubmit = (event) => {
event.preventDefault();
if (!file) {
setError("Please upload an image before submitting.");
return;
}
alert("File uploaded successfully!");
};
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>File Upload with Preview</h2>
<input type="file" accept="image/*" onChange={handleFileChange} />
{error && <p style={{ color: "red" }}>{error}</p>}
{preview && (
<div style={{ marginTop: "15px" }}>
<img
src={preview}
alt="Preview"
style={{ width: "100%", maxWidth: "300px", borderRadius: "5px" }}
/>
</div>
)}
<button onClick={handleSubmit} style={{ marginTop: "10px" }}>
Upload
</button>
</div>
);
};
export default FileUpload;
How It Works:
- User selects a file – Updates
file
and generates a preview URL. - File validation – Ensures only image files are allowed.
- Displays image preview – Shows the uploaded file before submission.
- Handles errors – Alerts if no file is selected or an invalid file is chosen.
Why This Approach?
- Prevents non-image uploads with validation.
- Provides instant feedback with a preview before submission.
- Keeps the form interactive and user-friendly.
Controlled vs. Uncontrolled Forms
- Build a form with both controlled and uncontrolled inputs and compare their behavior.
Controlled vs. Uncontrolled Forms in React
This implementation demonstrates the difference between controlled and uncontrolled form inputs by creating a simple form with both types of inputs.
- Controlled inputs – Managed by React using
useState
. - Uncontrolled inputs – Uses Refs to access DOM values directly.
- Comparison – Shows how both approaches handle form data.
1. Implement the Controlled vs. Uncontrolled Form Component
import { useState, useRef } from "react";
const ControlledVsUncontrolled = () => {
// Controlled Input (Managed by React State)
const [controlledValue, setControlledValue] = useState("");
// Uncontrolled Input (Uses Ref)
const uncontrolledRef = useRef(null);
// Handle Controlled Input Change
const handleControlledChange = (event) => {
setControlledValue(event.target.value);
};
// Handle Form Submission
const handleSubmit = (event) => {
event.preventDefault();
alert(
`Controlled Input: ${controlledValue}\nUncontrolled Input: ${uncontrolledRef.current.value}`
);
};
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Controlled vs. Uncontrolled Inputs</h2>
<form onSubmit={handleSubmit}>
{/* Controlled Input */}
<div>
<label>Controlled Input: </label>
<input
type="text"
value={controlledValue}
onChange={handleControlledChange}
/>
</div>
{/* Uncontrolled Input */}
<div style={{ marginTop: "10px" }}>
<label>Uncontrolled Input: </label>
<input type="text" ref={uncontrolledRef} />
</div>
<button type="submit" style={{ marginTop: "15px" }}>Submit</button>
</form>
</div>
);
};
export default ControlledVsUncontrolled;
How It Works:
- Controlled Input
- Uses
useState
to manage the value. - React controls the input’s value.
- Uses
- Uncontrolled Input
- Uses
useRef
to access the value directly from the DOM. - The browser manages the input instead of React.
- Uses
- Form Submission
- Displays both controlled and uncontrolled input values on submit.
Comparison Table
Feature | Controlled Input (useState ) |
Uncontrolled Input (useRef ) |
Managed By | React State | DOM (Direct Access via Ref) |
Re-rendering | Triggers re-renders on change | Does not trigger re-renders |
Validation | Can be validated in real-time | Requires manual validation |
Use Case | Forms that need validation & dynamic state updates | Simple forms with minimal interaction |
Why This Approach?
- Demonstrates both approaches side by side for easy comparison.
- Shows pros & cons of each method with practical use cases.
- Helps developers decide which approach suits their needs.
Multi-Select Dropdown (useState)
- Implement a multi-select dropdown where users can select multiple items.
Multi-Select Dropdown (useState
)
This implementation creates a multi-select dropdown where users can select multiple items from a list.
- Uses
useState
to manage selected options. - Allows multiple selections and displays selected items.
- Supports deselection by clicking on selected options.
1. Implement the Multi-Select Dropdown Component
import { useState } from "react";
const MultiSelectDropdown = () => {
const options = ["Apple", "Banana", "Cherry", "Mango", "Orange", "Grapes"];
const [selectedItems, setSelectedItems] = useState([]);
// Handle selection & deselection
const handleSelect = (item) => {
if (selectedItems.includes(item)) {
setSelectedItems(selectedItems.filter((selected) => selected !== item)); // Remove item
} else {
setSelectedItems([...selectedItems, item]); // Add item
}
};
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Multi-Select Dropdown</h2>
<div style={{ border: "1px solid #ccc", padding: "10px", borderRadius: "5px" }}>
<p>Select Fruits:</p>
{options.map((item) => (
<div key={item}>
<label>
<input
type="checkbox"
checked={selectedItems.includes(item)}
onChange={() => handleSelect(item)}
/>
{item}
</label>
</div>
))}
</div>
{/* Display selected items */}
<div style={{ marginTop: "10px" }}>
<strong>Selected:</strong> {selectedItems.length > 0 ? selectedItems.join(", ") : "None"}
</div>
</div>
);
};
export default MultiSelectDropdown;
How It Works:
- State Management (
useState
)selectedItems
stores the selected options.- Clicking an item adds or removes it from the selection.
- Dynamic Selection & Deselection
- Uses checkboxes to allow multiple selections.
- Clicking a selected item unchecks it (removes it).
- Displaying Selected Items
- Shows the selected items as a comma-separated list.
Why This Approach?
- Simple and scalable – Easy to expand with more items.
- Allows real-time updates – Users see changes instantly.
- Enhances usability – Provides a clear, interactive UI.
Optimize a Search Component (useMemo, useCallback,
useDeferred)
Optimize a Search Component (useMemo
, useCallback
, useDeferredValue
)
This implementation improves the performance of a search component by:
- Using
useMemo
to prevent unnecessary filtering re-renders. - Using
useCallback
to memoize event handlers. - Using
useDeferredValue
to avoid blocking UI updates during typing.
1. Implement the Optimized Search Component
import { useState, useMemo, useCallback, useDeferredValue } from "react";
// Sample data for searching
const items = [
"Apple", "Banana", "Cherry", "Mango", "Orange", "Grapes",
"Pineapple", "Strawberry", "Blueberry", "Peach", "Watermelon"
];
const SearchComponent = () => {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query); // Defers search input processing
// Memoize filtered results
const filteredItems = useMemo(() => {
return items.filter((item) =>
item.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [deferredQuery]);
// Memoize the input change handler
const handleChange = useCallback((event) => {
setQuery(event.target.value);
}, []);
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Optimized Search</h2>
<input
type="text"
placeholder="Search..."
value={query}
onChange={handleChange}
/>
<ul>
{filteredItems.length > 0 ? (
filteredItems.map((item, index) => <li key={index}>{item}</li>)
) : (
<li>No results found</li>
)}
</ul>
</div>
);
};
export default SearchComponent;
How It Works:
useMemo
Optimizes Filtering- Prevents unnecessary recalculations when
query
hasn’t changed. - Improves performance, especially for large datasets.
- Prevents unnecessary recalculations when
useCallback
Optimizes Event Handler- Prevents re-creating the
handleChange
function on every render. - Useful in components that receive functions as props.
- Prevents re-creating the
useDeferredValue
Improves UI Responsiveness- Allows the UI (e.g., typing in the search box) to remain smooth.
- Delays filtering computation to prevent blocking the main thread.
Why This Approach?
- Prevents unnecessary re-renders and computations.
- Ensures smooth UI interactions while searching.
- Scales well for large lists and real-time filtering.
Prevent Unnecessary Re-Renders (React.memo)
- Create a component that only re-renders when necessary, using
React.memo
.
Prevent Unnecessary Re-Renders (React.memo
)
This implementation optimizes a React component by using React.memo
to prevent unnecessary re-renders.
- Uses
React.memo
to prevent re-renders if props don’t change. - Demonstrates unnecessary vs optimized renders.
- Improves performance when dealing with frequently updated states.
1. Implement the Optimized Component Using React.memo
import { useState, memo } from "react";
// Child Component (Optimized with React.memo)
const ChildComponent = memo(({ count }) => {
console.log("ChildComponent Rendered");
return <h3>Count: {count}</h3>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [randomValue, setRandomValue] = useState(0); // Irrelevant state
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>Prevent Unnecessary Re-Renders</h2>
<ChildComponent count={count} /> {/* Only re-renders when 'count' changes */}
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<button onClick={() => setRandomValue(Math.random())}>Update Random Value</button>
</div>
);
};
export default ParentComponent;
How It Works:
React.memo(ChildComponent)
- Prevents
ChildComponent
from re-rendering unless its props change. - If only
randomValue
updates,ChildComponent
does not re-render.
- Prevents
- Parent Updates Irrelevant State (
randomValue
)- Updates that don’t affect the
ChildComponent
won’t cause a re-render. - Console logs confirm that
ChildComponent
only renders whencount
changes.
- Updates that don’t affect the
Why This Approach?
- Optimizes performance by avoiding unnecessary updates.
- Ensures components re-render only when needed.
- Useful for complex UI elements (e.g., large lists, expensive calculations).
Lazy Loading Components (React.lazy & Suspense)
- Implement lazy loading for a heavy component to improve performance.
Lazy Loading Components (React.lazy
& Suspense
)
This implementation uses React.lazy
and Suspense
to dynamically load a heavy component, improving performance by reducing the initial bundle size.
- Uses
React.lazy
to defer loading until needed. - Wraps in
Suspense
to display a fallback while loading. - Improves performance by loading components only when necessary.
1. Create a Heavy Component (HeavyComponent.js
)
This simulates a large component that should be loaded lazily.
const HeavyComponent = () => {
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>Heavy Component Loaded</h2>
<p>This component was lazily loaded to improve performance.</p>
</div>
);
};
export default HeavyComponent;
2. Lazy Load the Component in App.js
import React, { lazy, Suspense, useState } from "react";
// Lazy load the HeavyComponent
const HeavyComponent = lazy(() => import("./HeavyComponent"));
const App = () => {
const [showComponent, setShowComponent] = useState(false);
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>Lazy Loading Example</h2>
<button onClick={() => setShowComponent(true)}>Load Component</button>
{/* Show the lazy-loaded component only when needed */}
{showComponent && (
<Suspense fallback={<p>Loading Component...</p>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
};
export default App;
How It Works:
- Lazy Loading (
React.lazy
)- Defers loading
HeavyComponent
until needed.
- Defers loading
- Fallback Handling (
Suspense
)- Displays
"Loading Component..."
while waiting for lazy-loaded content.
- Displays
- Loads Component on Button Click
- The component loads only when the user triggers it, reducing initial load time.
Why This Approach?
- Reduces initial bundle size and speeds up app load time.
- Efficiently loads large components only when needed.
- Improves user experience by displaying a loading message.
WebSocket Integration (useEffect)
- Implement a real-time chat using WebSockets (
wss://echo.websocket.org
).
WebSocket Integration (useEffect
) – Real-Time Chat
This implementation sets up a real-time chat system using WebSockets with wss://echo.websocket.org
.
- Uses WebSocket API for real-time messaging.
- Uses
useState
to manage messages. - Uses
useEffect
to maintain WebSocket connection. - Supports sending and receiving messages dynamically.
1. Implement the WebSocket Chat Component
import { useState, useEffect, useRef } from "react";
const WebSocketChat = () => {
const [messages, setMessages] = useState([]); // Stores chat messages
const [input, setInput] = useState(""); // Stores user input
const [status, setStatus] = useState("Connecting...");
const ws = useRef(null); // WebSocket reference
useEffect(() => {
// Initialize WebSocket connection
ws.current = new WebSocket("wss://echo.websocket.org");
ws.current.onopen = () => setStatus("Connected");
ws.current.onclose = () => setStatus("Disconnected");
ws.current.onerror = () => setStatus("Error occurred");
ws.current.onmessage = (event) => {
setMessages((prev) => [...prev, { text: event.data, sender: "Server" }]);
};
return () => {
ws.current.close(); // Cleanup WebSocket on component unmount
};
}, []);
const sendMessage = () => {
if (input.trim() && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(input);
setMessages((prev) => [...prev, { text: input, sender: "You" }]);
setInput(""); // Clear input field
}
};
return (
<div style={{ textAlign: "center", maxWidth: "400px", margin: "auto" }}>
<h2>WebSocket Chat</h2>
<p>Status: {status}</p>
<div style={{
border: "1px solid #ccc",
padding: "10px",
height: "200px",
overflowY: "auto",
marginBottom: "10px",
}}>
{messages.map((msg, index) => (
<p key={index} style={{ textAlign: msg.sender === "You" ? "right" : "left" }}>
<strong>{msg.sender}:</strong> {msg.text}
</p>
))}
</div>
<input
type="text"
value={input}
placeholder="Type a message..."
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
<button onClick={sendMessage} style={{ marginLeft: "5px" }}>Send</button>
</div>
);
};
export default WebSocketChat;
How It Works:
useRef
stores WebSocket connection to persist across renders.useEffect
initializes the WebSocket and cleans up on unmount.- Handles WebSocket events (
onopen
,onclose
,onmessage
). - Stores messages in state and updates dynamically.
- Users can send messages, and the server echoes them back in real-time.
Why This Approach?
- Provides real-time communication with WebSockets.
- Manages connection state dynamically (
Connected
,Disconnected
). - Interactive UI with chat history and input field.
Optimized Image Gallery (Intersection Observer)
- Load images only when they are visible using the Intersection Observer API.
Optimized Image Gallery (Intersection Observer
)
This implementation loads images only when they are visible using the Intersection Observer API, improving performance by reducing unnecessary image loading.
- Uses
useState
to store image data. - Uses
useEffect
andIntersection Observer
to detect when an image is in view. - Prevents unnecessary network requests by loading images only when needed.
1. Implement the Optimized Image Gallery Component
import { useState, useEffect, useRef } from "react";
// Sample image data
const imageUrls = [
"https://via.placeholder.com/300?text=Image+1",
"https://via.placeholder.com/300?text=Image+2",
"https://via.placeholder.com/300?text=Image+3",
"https://via.placeholder.com/300?text=Image+4",
"https://via.placeholder.com/300?text=Image+5",
"https://via.placeholder.com/300?text=Image+6",
"https://via.placeholder.com/300?text=Image+7",
"https://via.placeholder.com/300?text=Image+8",
"https://via.placeholder.com/300?text=Image+9",
];
const LazyImage = ({ src, alt }) => {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setIsVisible(true);
observer.disconnect(); // Stop observing once the image is loaded
}
},
{ threshold: 0.5 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} style={{ minHeight: "300px", display: "flex", alignItems: "center", justifyContent: "center" }}>
{isVisible ? <img src={src} alt={alt} style={{ width: "100%", height: "auto" }} /> : <p>Loading...</p>}
</div>
);
};
const ImageGallery = () => {
return (
<div style={{ textAlign: "center", maxWidth: "600px", margin: "auto" }}>
<h2>Optimized Image Gallery</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px" }}>
{imageUrls.map((url, index) => (
<LazyImage key={index} src={url} alt={`Image ${index + 1}`} />
))}
</div>
</div>
);
};
export default ImageGallery;
How It Works:
- Intersection Observer watches images
- When an image enters the viewport, it loads dynamically.
observer.disconnect()
stops observing after loading.
- Prevents Unnecessary Image Loads
- Only loads images when they become visible instead of loading all at once.
- Fallback UI for Loading State
- Shows “Loading…” placeholder before the image appears.
Why This Approach?
- Improves performance by reducing unnecessary image requests.
- Reduces memory usage by loading only visible images.
- Enhances UX with smooth lazy-loading.
User Authentication (using Context API)
- Use the Context API to manage global user authentication state.
Global State Management (Context API) – User Authentication
This implementation manages global user authentication state using React Context API.
- Uses
createContext
for authentication state. - Uses
useContext
to access authentication data anywhere. - Supports Login / Logout functionality across the app.
- Prevents prop drilling by providing global state access.
1. Create the AuthContext
for Authentication State
import { createContext, useContext, useState } from "react";
// Create AuthContext
const AuthContext = createContext();
// Provide Auth State to the App
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => setIsAuthenticated(true);
const logout = () => setIsAuthenticated(false);
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// Custom Hook to Access Auth Context
export const useAuth = () => useContext(AuthContext);
2. Implement Protected Routes Using Context
This component restricts access to authenticated users.
import { Navigate } from "react-router-dom";
import { useAuth } from "./AuthContext";
const ProtectedRoute = ({ element }) => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? element : <Navigate to="/login" />;
};
export default ProtectedRoute;
3. Implement the Login Page
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";
const LoginPage = () => {
const { login } = useAuth();
const navigate = useNavigate();
const handleLogin = () => {
login();
navigate("/dashboard"); // Redirect after login
};
return (
<div style={{ textAlign: "center" }}>
<h2>Login Page</h2>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default LoginPage;
4. Implement the Dashboard Page (Protected Page)
import { useAuth } from "./AuthContext";
import { useNavigate } from "react-router-dom";
const Dashboard = () => {
const { logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/login");
};
return (
<div style={{ textAlign: "center" }}>
<h2>Welcome to Dashboard</h2>
<button onClick={handleLogout}>Logout</button>
</div>
);
};
export default Dashboard;
5. Set Up Routes and Wrap the App with AuthProvider
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import LoginPage from "./LoginPage";
import Dashboard from "./Dashboard";
import ProtectedRoute from "./ProtectedRoute";
const App = () => {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} />} />
</Routes>
</Router>
</AuthProvider>
);
};
export default App;
How It Works:
AuthProvider
manages the authentication state globally.useAuth
provides access to auth functions (login
,logout
).ProtectedRoute
restricts access to logged-in users only.- Login updates global state, allowing navigation to the dashboard.
- Logout resets authentication state and redirects back to login.
Why This Approach?
- Removes prop drilling by using a global authentication state.
- Easily accessible authentication functions (
login/logout
) across the app. - Prevents unauthorized access with protected routes.
Undo/Redo with useReducer
- Implement an undo/redo feature for a simple text editor using
useReducer
.
Undo/Redo with useReducer
– Simple Text Editor
This implementation creates a text editor with Undo/Redo functionality using
useReducer
to manage state history efficiently.
- Uses
useReducer
to track past, present, and future states. - Implements Undo / Redo functionality to revert and restore changes.
- Prevents unnecessary re-renders by handling state updates efficiently.
1. Implement the useReducer
Logic for Undo/Redo
import { useReducer } from "react";
// Define initial state
const initialState = {
past: [],
present: "",
future: [],
};
// Reducer function to handle state changes
const reducer = (state, action) => {
switch (action.type) {
case "TYPING":
return {
past: [...state.past, state.present],
present: action.payload,
future: [],
};
case "UNDO":
if (state.past.length === 0) return state; // Prevent undo if no history
return {
past: state.past.slice(0, -1),
present: state.past[state.past.length - 1],
future: [state.present, ...state.future],
};
case "REDO":
if (state.future.length === 0) return state; // Prevent redo if no future states
return {
past: [...state.past, state.present],
present: state.future[0],
future: state.future.slice(1),
};
case "RESET":
return initialState;
default:
return state;
}
};
2. Implement the Undo/Redo Text Editor Component
const UndoRedoTextEditor = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div style={{ textAlign: "center", maxWidth: "500px", margin: "auto" }}>
<h2>Undo/Redo Text Editor</h2>
<textarea
rows="5"
cols="40"
value={state.present}
onChange={(e) => dispatch({ type: "TYPING", payload: e.target.value })}
placeholder="Type something..."
/>
<div style={{ marginTop: "10px" }}>
<button onClick={() => dispatch({ type: "UNDO" })} disabled={state.past.length === 0}>
Undo
</button>
<button onClick={() => dispatch({ type: "REDO" })} disabled={state.future.length === 0}>
Redo
</button>
<button onClick={() => dispatch({ type: "RESET" })}>
Reset
</button>
</div>
</div>
);
};
export default UndoRedoTextEditor;
How It Works:
useReducer
stores three states:past
– Stores previous values for undo.present
– Current text content.future
– Stores values for redo.
- Typing (
TYPING
action)- Saves the previous value to
past[]
. - Updates
present
with new text. - Clears the
future[]
to prevent invalid redos.
- Saves the previous value to
- Undo (
UNDO
action)- Moves the last value from
past[]
topresent
. - Moves the current
present
tofuture[]
for redo.
- Moves the last value from
- Redo (
REDO
action)- Moves the first value from
future[]
back topresent
. - Restores the state for seamless redo operations.
- Moves the first value from
Why This Approach?
- Optimized state management using
useReducer
instead of multipleuseState
calls. - Efficient undo/redo functionality with separate history tracking.
- Scalable – Can be extended to store multiple field histories.
Centralized Notifications System
- Create a global notifications system using Context API and
useReducer
.
Centralized Notifications System (Context API & useReducer
)
This implementation creates a global notifications system using Context API and
useReducer
, allowing any component to trigger and display notifications.
- Uses
useContext
for global access to notifications. - Uses
useReducer
for efficient state management. - Supports adding & removing notifications dynamically.
- Ensures notifications auto-dismiss after a timeout.
1. Create the NotificationContext
Manages notifications globally.
import { createContext, useContext, useReducer } from "react";
// Initial state
const initialState = [];
// Reducer function
const notificationReducer = (state, action) => {
switch (action.type) {
case "ADD_NOTIFICATION":
return [...state, action.payload];
case "REMOVE_NOTIFICATION":
return state.filter((notification) => notification.id !== action.payload);
default:
return state;
}
};
// Create context
const NotificationContext = createContext();
// Notification Provider
export const NotificationProvider = ({ children }) => {
const [notifications, dispatch] = useReducer(notificationReducer, initialState);
const addNotification = (message, type = "info", duration = 3000) => {
const id = Date.now();
dispatch({ type: "ADD_NOTIFICATION", payload: { id, message, type } });
// Auto-remove notification after duration
setTimeout(() => {
dispatch({ type: "REMOVE_NOTIFICATION", payload: id });
}, duration);
};
return (
<NotificationContext.Provider value={{ notifications, addNotification }}>
{children}
</NotificationContext.Provider>
);
};
// Custom Hook to Use Notifications
export const useNotifications = () => useContext(NotificationContext);
2. Implement the Notification Display Component
import { useNotifications } from "./NotificationContext";
const Notifications = () => {
const { notifications } = useNotifications();
return (
<div style={{
position: "fixed", top: "10px", right: "10px", width: "300px", zIndex: 1000
}}>
{notifications.map(({ id, message, type }) => (
<div key={id} style={{
marginBottom: "10px",
padding: "10px",
borderRadius: "5px",
background: type === "error" ? "#ff4d4d" : type === "success" ? "#4caf50" : "#2196f3",
color: "white"
}}>
{message}
</div>
))}
</div>
);
};
export default Notifications;
3. Implement a Component to Trigger Notifications
import { useNotifications } from "./NotificationContext";
const TestComponent = () => {
const { addNotification } = useNotifications();
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h2>Global Notifications System</h2>
<button onClick={() => addNotification("Info Notification", "info")}>
Show Info
</button>
<button onClick={() => addNotification("Success Notification", "success")}>
Show Success
</button>
<button onClick={() => addNotification("Error Notification", "error")}>
Show Error
</button>
</div>
);
};
export default TestComponent;
4. Integrate Everything in App.js
import { BrowserRouter as Router } from "react-router-dom";
import { NotificationProvider } from "./NotificationContext";
import Notifications from "./Notifications";
import TestComponent from "./TestComponent";
const App = () => {
return (
<NotificationProvider>
<Router>
<Notifications />
<TestComponent />
</Router>
</NotificationProvider>
);
};
export default App;
How It Works:
NotificationContext
manages notifications globally usinguseReducer
.- Any component can trigger notifications using
useNotifications()
. - Notifications auto-dismiss after a set duration.
- Notifications appear in a fixed corner for better UX.
Why This Approach?
- Provides a centralized notification system without prop drilling.
- Efficient state updates using
useReducer
. - Customizable notification types and duration.
Theme Provider with Context API
- Implement a global theme provider that supports light/dark mode.
Theme Provider with Context API (Light/Dark Mode)
This implementation manages global theme state using the Context API, allowing any component to toggle between Light and Dark mode.
- Uses Context API to provide theme state globally.
- Uses
useReducer
for efficient state management. - Stores theme preference in
localStorage
for persistence. - Applies CSS variables to dynamically change styles.
1. Create the ThemeContext
Manages theme state globally and persists it in localStorage
.
import { createContext, useContext, useReducer, useEffect } from "react";
// Define theme actions
const TOGGLE_THEME = "TOGGLE_THEME";
// Define the initial theme state
const initialState = {
theme: localStorage.getItem("theme") || "light",
};
// Reducer function for theme toggling
const themeReducer = (state, action) => {
switch (action.type) {
case TOGGLE_THEME:
const newTheme = state.theme === "light" ? "dark" : "light";
localStorage.setItem("theme", newTheme);
return { theme: newTheme };
default:
return state;
}
};
// Create context
const ThemeContext = createContext();
// Theme Provider
export const ThemeProvider = ({ children }) => {
const [state, dispatch] = useReducer(themeReducer, initialState);
useEffect(() => {
document.documentElement.setAttribute("data-theme", state.theme);
}, [state.theme]);
return (
<ThemeContext.Provider value={{ theme: state.theme, toggleTheme: () => dispatch({ type: TOGGLE_THEME }) }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook to use theme context
export const useTheme = () => useContext(ThemeContext);
2. Apply Theme Styles Using CSS Variables
Define CSS variables for light and dark themes.
/* styles.css */
:root {
--bg-color: #ffffff;
--text-color: #000000;
}
[data-theme="dark"] {
--bg-color: #181818;
--text-color: #ffffff;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
3. Implement a Theme Toggle Button
import { useTheme } from "./ThemeContext";
const ThemeToggleButton = () => {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme} style={{ margin: "20px", padding: "10px" }}>
{theme === "light" ? "🌙 Switch to Dark Mode" : "☀️ Switch to Light Mode"}
</button>
);
};
export default ThemeToggleButton;
4. Integrate Everything in App.js
import { ThemeProvider } from "./ThemeContext";
import ThemeToggleButton from "./ThemeToggleButton";
import "./styles.css";
const App = () => {
return (
<ThemeProvider>
<div style={{ textAlign: "center", padding: "20px" }}>
<h2>Global Theme Provider</h2>
<ThemeToggleButton />
<p>The theme will persist after page reloads.</p>
</div>
</ThemeProvider>
);
};
export default App;
How It Works:
- Global Theme Management with
Context API
- The theme state is available throughout the app.
- No prop drilling needed.
- Efficient State Updates with
useReducer
- Uses an action-based reducer to toggle the theme.
- Persisting Theme in
localStorage
- Ensures the theme remains the same after refresh.
- CSS Variables for Styling
- Styles are automatically updated without modifying components.
Why This Approach?
- Global theme state with easy access anywhere in the app.
- Smooth user experience with theme persistence.
- Performance-friendly using
useReducer
and CSS variables.
React Developer hiring resources
Our clients
Popular React Development questions
How does React handle form validation? – Lemon.io
React supports form validation by providing the developer with a means to manage the state of the forms and their respective validation logic within the components. Using controlled components, it becomes pretty straightforward to validate input fields in real time since all values of the inputs of the form will be managed by React state. For more advanced form management and validation, libraries such as Formik or React Hook Form can be utilized, allowing for the handling of complex forms with less boilerplate code.
What are the best practices for managing state in React applications? – Lemon.io
React applications can further be optimized with respect to the management of state by making use of best techniques like raising the state up the component tree to the nearest common ancestor of components that need access to it. Use Redux or Context API for complex state management. Minimize the states store only what is necessary and this will minimize the number of rerenders. Also, instead of useState, when there is more complex state logic, useReducer will lead to cleaner and maintainable code.
How does React improve the performance of web applications? – Lemon.io
React optimizes the performance of web applications by making use of a virtual DOM; hence, it reduces the amount of changes it makes on the actual DOM. It minimizes time spent rendering and resources utilized in rendering updates since React will only update those components that have changed. This leads to faster and more responsive user interfaces. Besides that, it allows developers to load parts of an application that are in use at a certain moment by splitting the code and making lazy loading work.
What are the advantages of using React for web development? – Lemon.io
React has a lot of advantages in web development; above all, it’s component-based architecture. The whole thing allows the developer to easily create reusable UI components. Next, it features a Virtual DOM, enhancing its performance through its great ability to update and render components at incredible speed. React has this one-way flow of data in user interface components, making it very easy to debug and test. Last but not least, React’s growing ecosystem—which also includes, among others, tools like Redux and React Router—already sports robust solutions for state management and routing. That is why React would still manage to hold its position as one of the leading choices in designing scalable and maintainable web applications.
Is React used by Full-Stack Developers? – Lemon.io
Yes, a Full-Stack Developer uses React for the web app Front-End. They create dynamic and responsive user interfaces and then manage the Back-End via Node.js, Express, or other technologies for server-side frameworks. Among those reasons is that React can be connected with many different Back-end technologies, hence helping one effectively develop end-to-end applications.
What is React Development? – Lemon.io
React Development refers to the process of building a user interface, primarily single-page applications. A developer can use it to create reusable UI elements, update, and render for performance and maintainability efficiently. The tool is geared toward creating independent components handling their state but possibly combined to form complex and highly interactive user interfaces. Accordingly, React Development has become a well-known aspect in the development of fast, dynamic, and scalable web applications.
Interview Questions by role
Interview Questions by skill
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions