logo
React Hooks Interview Questions and Answers
React Hooks are a feature introduced in React 16.8 that allow developers to use state and other React features in functional components, without needing to write class components. They’re essentially functions that "hook into" React’s internals, enabling you to manage state, side effects, and other lifecycle-like behaviors in a more concise and reusable way.

Before Hooks, state and lifecycle methods (like componentDidMount or setState) were tied to class components, which could get clunky and hard to maintain, especially with complex logic. Hooks solve this by letting you write everything as functions, making code cleaner and easier to share between components.

Here’s a quick rundown of the main ones:

useState : Lets you add state to a functional component. You call it with an initial value, and it returns an array with the current state and a function to update it. Example :
import React, { useState } from 'react';
function Counter() {
  const [count, setCount] = useState(0);
  return  setCount(count + 1)}>{count};
}?

 

useEffect: Handles side effects—like data fetching, subscriptions, or DOM updates—similar to lifecycle methods in classes. It runs after every render by default, but you can control it with a dependency array. Example :
import React, { useEffect } from 'react';
function Example() {
  useEffect(() => {
    document.title = 'Hello, world!';
  }, []); // Empty array means it runs once on mount
  return <div>Check the title!</div>;
}

useContext
: Lets you access React’s context (global-ish state) without prop drilling. Useful for themes, user data, etc.
const value = useContext(MyContext);?

There are others too, like useReducer (for complex state logic), useCallback (memoizes functions), and useMemo (memoizes values), plus you can create custom Hooks to reuse logic across components.

 

 

 

Hooks were introduced in React 16.8 to solve several issues with class components and to make state management and side effects easier in functional components. Here are the main reasons:

1. Simplifying Component Logic
  • Before Hooks, stateful logic was only possible in class components, making React code more complex and harder to reuse.
  • Hooks allow state and side effects in functional components, making them more powerful and concise.
2. Reusing Stateful Logic
  • In class components, logic reuse was done via Higher-Order Components (HOCs) and Render Props, which often led to "wrapper hell" (deeply nested components).
  • Hooks provide a better way to share logic across components through custom Hooks.
3. Avoiding Complex Class Components
  • Classes come with complexities like this binding, lifecycle methods, and boilerplate code.
  • Hooks eliminate the need for class components in most cases, making React code more readable and maintainable.
4. Better Handling of Side Effects
  • Class components use lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount), which often result in duplicated or scattered logic.
  • Hooks like useEffect allow handling side effects in a more organized way.
5. Improved Performance and Optimization
  • Hooks like useMemo and useCallback help optimize performance by reducing unnecessary re-renders.
  • Functional components with Hooks are generally lighter than class components.
6. Future-Proofing React
  • Hooks enable new features and improvements in React without breaking existing code.
  • They align well with concurrent rendering and React’s future optimizations.

Hooks in React follow a few fundamental rules to ensure they work correctly. These rules are enforced by React's linter plugin (eslint-plugin-react-hooks).

Rules of Hooks
  1. Only Call Hooks at the Top Level
    • Do not call Hooks inside loops, conditions, or nested functions.
    • Always call Hooks at the top level of a functional component or within another Hook.
    • This ensures Hooks run in the same order on every render.
    Incorrect :
    if (someCondition) {
      const [state, setState] = useState(0); //  Hook inside condition
    }
    
    Correct :
    const [state, setState] = useState(0); //  Always at the top level
    if (someCondition) {
      console.log(state);
    }
    
  2. Only Call Hooks from React Functions
    • You can use Hooks inside:
      • React functional components
      • Custom Hooks
    • Do not use Hooks inside:
      • Regular JavaScript functions
      • Class components
    Incorrect :
    function someUtilityFunction() {
      const [count, setCount] = useState(0); //  Hooks inside a regular function
    }
    
    Correct :
    function MyComponent() {
      const [count, setCount] = useState(0); //  Hooks inside a functional component
    }
    
  3. Use Hooks in the Same Order Every Time
    • Hooks rely on the order they are called in.
    • If you put them inside conditions or loops, their order may change between renders, causing unexpected bugs.
  4. Always Use the use Prefix for Custom Hooks
    • If you create a custom Hook, name it with the use prefix (e.g., useMyHook).
    • This helps React detect Hooks and apply its rules.
    Correct :
    function useCustomHook() {
      const [value, setValue] = useState(0);
      return [value, setValue];
    }
    
    Incorrect :
    function customHook() { //  No "use" prefix
      const [value, setValue] = useState(0);
      return [value, setValue];
    }
    

How to Enforce Hook Rules?

React provides an ESLint plugin:

npm install eslint-plugin-react-hooks --save-dev

This will warn you if you break any Hook rules.

The purpose of useCallback Hooks is used to memoize functions, and prevent unnecessary re-rendering of child components that rely on those components. The useCallback function in React is mainly used to keep a reference to a function constant across multiple re-renders. This feature becomes useful when we want to prevent the unnecessary re-creation of functions, especially when we need to pass them as dependencies to other hooks such as useMemo or useEffect.
Here's a difference of useMemo and useCallback int tabular form :

useMemo useCallback
Memoizes a value (result of a function) Memoizes a function
Memoized value Memoized function reference
When we need to memoize a calculated value When we need to memoize a function
Recalculates when any dependency changes Recreates the function only when any dependency changes
Example: Memoizing the result of expensive computations Example: Memoizing event handler functions to prevent re-renders
useState useReducer
Handles state with a single value Handles state with more complex logic and multiple values
Simple to use, suitable for basic state needs More complex, suitable for managing complex state logic
Simple state updates, like toggles or counters Managing state with complex transitions and logic
Directly updates state with a new value Updates state based on dispatched actions and logic
Not used Requires a reducer function to determine state changes
Logic is dispersed where state is used Logic is centralized within the reducer function
The useRef is used in React functional components when we need to keep a mutable value around across renders without triggering a re-render. It's commonly used for accessing DOM elements, caching values, or storing mutable variables. we can use useRef to manage focus within wer components, such as focusing on a specific input element when a condition is met without triggering re-renders.
The useLawetEffect is similar to useEffect but fires synchronously after all DOM mutations. It's useful for reading from the DOM or performing animations before the browser paints. Due to its synchronous nature, excessive usage of useLawetEffect may potentially impact performance, especially if the logic within it is computationally intensive or blocking. It's essential to use useLawetEffect judiciously and consider performance implications carefully.
Custom hooks in React are like wer own personal tools that we can create to make building components easier. They're functions we write to do specific tasks, and then we can reuse them in different components. They're handy because they let we keep wer code organized and share logic between different parts of wer application without repeating werself.
Here is to creating a custom Hooks step-by-step.

* Create a function : Start by defining a regular JavaScript function. It can have any name, but it's a convention to prefix it with "use" to indicate that it's a hook.

* Write the logic : Inside wer custom hook function, write the logic we want to encapsulate and reuse. we can use existing hooks like useState , useEffect , or other custom hooks if needed.

* Return values : Ensure wer custom hook returns whatever values or functions we want to expose to components that use it.

* Use the hook : we can now use wer custom hook in any functional component by calling it. React will recognize it as a hook because it starts with "use".
Yes, custom hooks in React can indeed accept parameters. By accepting parameters, custom hooks become more flexible and can adapt their behavior based on the specific needs of different components.
import { useEffect } from 'react';

function useDocumentTitle(title) {
    useEffect(() => {
        document.title = title;
    }, [title]);
}

// Usage:
function MyComponent() {
    useDocumentTitle('Hello FTL!');
    return <div>FreeTimeLearning content...</div>;
}?

According to React's documentation and best practices, you cannot directly use React Hooks inside class components. Here's a breakdown:

  • Hooks and Function Components :

    • Hooks were introduced in React 16.8 to enable function components to have state and other React features that were previously only available in class components.
    • Hooks are designed to work within the context of function components.
  • Why Not in Class Components?

    • The internal workings of Hooks rely on the order in which they are called within a function component. This order is guaranteed by the predictable execution of function components.
    • Class components have a different execution model, making it impossible to maintain the necessary order and consistency for Hooks to function correctly.
  • Mixing Components :

    • While you can't use Hooks within class components, you can freely mix class components and function components (with Hooks) within the same React application.
    • This allows you to gradually adopt Hooks in new components while maintaining existing class components.
  • Workarounds :

    • There are some workarounds, involving wrapping class components in functional components, and then passing props down, to allow the functionality of hooks to be utilized. However, directly inside of a class component, hooks will not function.
    • The general recommendation is to, when possible, refactor class components to function components to take full advantage of React Hooks.

In essence, React Hooks are specifically tied to function components, and attempting to use them within class components will lead to errors.

React provides several built-in Hooks to manage state, lifecycle, and other side effects in functional components. Here are some of the most commonly used ones:

1. State Management Hooks

* useState

  • Manages local component state.
  • Returns a state variable and a function to update it.
Example :
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

2. Side Effects and Lifecycle Hooks

* useEffect

  • Handles side effects (e.g., fetching data, subscriptions, DOM updates).
  • Runs after the component renders.
Example :
import { useState, useEffect } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval); // Cleanup on unmount
  }, []); // Empty dependency array → runs once

  return <p>Timer: {seconds}s</p>;
}

3. Performance Optimization Hooks

* useMemo

  • Memoizes expensive calculations to avoid unnecessary re-computation.
Example :
import { useMemo, useState } from "react";

function ExpensiveCalculation({ num }) {
  const squaredNumber = useMemo(() => {
    console.log("Calculating...");
    return num * num;
  }, [num]);

  return <p>Squared: {squaredNumber}</p>;
}

* useCallback

  • Memoizes functions to prevent unnecessary re-renders.
Example :
import { useCallback, useState } from "react";

function ButtonComponent({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <ButtonComponent onClick={handleClick} />
    </div>
  );
}

4. Reference and DOM Manipulation Hooks

* useRef

  • Stores a mutable reference that persists across renders.
  • Commonly used to reference DOM elements.
Example :
import { useRef, useEffect } from "react";

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

* useImperativeHandle

  • Customizes instance values exposed when using React.forwardRef.
Example :
import { useImperativeHandle, useRef, forwardRef } from "react";

const CustomInput = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

  const inputRef = useRef();
  return <input ref={inputRef} />;
});

function Parent() {
  const inputRef = useRef();
  
  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus Input</button>
    </div>
  );
}

5. Context and Reducer Hooks

* useContext

  • Consumes values from React’s Context API without using <Consumer>.
Example :
import { createContext, useContext } from "react";

const ThemeContext = createContext("light");

function ThemeComponent() {
  const theme = useContext(ThemeContext);
  return <p>Current theme: {theme}</p>;
}

* useReducer

  • Alternative to useState, useful for complex state logic.
Example :
import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
    default: throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </div>
  );
}

6. Additional Hooks

* useLayoutEffect

  • Like useEffect, but runs synchronously after DOM mutations.
Example :
import { useLayoutEffect, useRef } from "react";

function LayoutEffectExample() {
  const divRef = useRef();

  useLayoutEffect(() => {
    divRef.current.style.color = "red";
  });

  return <div ref={divRef}>This text will be red immediately</div>;
}

* useDebugValue

  • Used for debugging custom Hooks.
Example :
import { useDebugValue, useState } from "react";

function useCustomHook(value) {
  useDebugValue(value > 5 ? "Large" : "Small");
  return useState(value);
}

Summary Table
Hook Purpose
useState Manages local state
useEffect Handles side effects
useMemo Memoizes expensive computations
useCallback Memoizes functions
useRef Creates mutable references
useImperativeHandle Customizes ref exposure
useContext Accesses Context API values
useReducer Manages complex state logic
useLayoutEffect Runs synchronously after render
useDebugValue Adds debug info for custom hooks
* useContext Hook in React

The useContext Hook is used to access values from React’s Context API without needing to wrap components in Consumer components. It helps manage global state efficiently by avoiding prop drilling.


Why Use useContext?
  • Eliminates prop drilling (passing props through multiple components).
  • Makes state accessible globally without lifting state up.
  • Improves code readability by reducing unnecessary wrapper components.

How to Use useContext?
1. Create a Context

First, create a context using createContext().

import { createContext } from "react";

const ThemeContext = createContext("light"); // Default value

2. Provide a Value with Provider

Wrap your components inside a Provider to supply the context value.

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ChildComponent />
    </ThemeContext.Provider>
  );
}

3. Consume Context with useContext

Instead of using <ThemeContext.Consumer>, we use useContext().

import { useContext } from "react";

function ChildComponent() {
  const theme = useContext(ThemeContext); // Access context value

  return <p>Current Theme: {theme}</p>;
}

Full Example
import React, { useContext, createContext } from "react";

// 1 Create Context
const ThemeContext = createContext("light");

function ChildComponent() {
  // 2 Use useContext to get the value
  const theme = useContext(ThemeContext);
  return <p>Current Theme: {theme}</p>;
}

function App() {
  return (
    // 2 Provide context value
    <ThemeContext.Provider value="dark">
      <ChildComponent />
    </ThemeContext.Provider>
  );
}

export default App;

* Output: Current Theme: dark


Summary
Feature useContext
Purpose Access global state without prop drilling
Works with React Context API
Alternative to Prop drilling, Redux (for simple cases)
Best for Theme, Auth, Global Settings
Not ideal for Frequently changing state (use useState or useReducer instead)

The useContext hook in React is a powerful tool that significantly alleviates the problem of "prop drilling." Here's how it works:

Understanding Prop Drilling :

  • Prop drilling occurs when you need to pass data from a parent component to a deeply nested child component.
  • This often involves passing the data through several intermediate components that don't actually use the data themselves.
  • This can lead to:
    • Verbose and cluttered code.
    • Reduced maintainability.
    • Increased difficulty in refactoring.


How useContext Solves This :

  • Creating a Context:
    • You use React.createContext() to create a context object. This object holds the data you want to share.
  • Providing the Context:
    • You wrap a part of your component tree with a Context.Provider component.
    • The Provider component takes a value prop, which holds the data you want to make available to descendant components.
  • Consuming the Context:
    • Any component within the Provider's tree can access the context value using the useContext hook.
    • The useContext hook takes the context object as an argument and returns the current context value.  


In essence :

  • useContext allows you to establish a "global" data source within a portion of your component tree.
  • Components that need the data can directly access it, regardless of their position in the tree, without relying on props being passed down through intermediate components.


Key Benefits :

  • Eliminates Prop Drilling: Reduces the need to pass props through multiple layers of components.
  • Improved Code Readability: Makes code cleaner and easier to understand.
  • Enhanced Maintainability: Simplifies refactoring and code modifications.

Therefore, useContext is a very useful tool for managing state that needs to be accessed by many deeply nested components.

Yes, you can use useContext without a Provider, but the result will be that you receive the default value that was defined when you created the context. Here's a breakdown:

  • React.createContext(defaultValue) :

    • When you create a context using React.createContext(), you can provide an optional defaultValue.
    • This defaultValue serves as a fallback value that useContext will return if no matching Provider is found in the component tree above the component calling useContext.
  • useContext(MyContext) :

    • When you call useContext(MyContext), React searches upwards in the component tree for the nearest MyContext.Provider.
    • If a Provider is found, useContext returns the value prop of that Provider.
    • If no Provider is found, useContext returns the defaultValue that was passed to React.createContext().


Therefore :

  • If you use useContext without a surrounding Provider, you won't get an error. Instead, you'll simply get the default value.
  • This behavior can be useful in certain scenarios, such as:
    • Providing default configurations.
    • Simplifying testing by avoiding the need to wrap components in Providers for every test.

In essence, the Provider's role is to override the default context value, and if it is not present, the default value is what is returned.

While useContext is excellent for simplifying prop drilling, improper use can lead to performance issues, especially in large applications. Here's how you can optimize performance when using useContext:

1. Minimize Context Value Changes :

  • Immutable Updates:
    • Ensure that the context value is updated immutably. Avoid directly mutating objects or arrays. Instead, create new objects or arrays with the updated values. This allows React to efficiently detect changes.
  • Memoize Context Values:
    • If the context value is derived from other state or props, use useMemo to memoize the value. This prevents unnecessary re-renders when the derived value hasn't actually changed.


2. Scope Context Providers Appropriately :

  • Avoid Over-Providing:
    • Don't wrap your entire application with a single Provider unless absolutely necessary. This can lead to unnecessary re-renders of all components that consume the context, even if they don't depend on the changed values.
    • Wrap only the parts of your component tree that actually need access to the context.
  • Multiple Contexts:
    • Use multiple, smaller contexts instead of a single large context. This allows you to isolate changes and minimize re-renders.


3. Optimize Consumer Components :

  • Memoize Consumer Components:
    • Use React.memo to memoize components that consume the context. This prevents re-renders if the component's props and the context value haven't changed.
  • Selective Context Consumption:
    • If a component only needs a specific part of the context value, consider restructuring your context to provide smaller, more specific values.
  • Avoid Inline Object Creation:
    • Avoid creating new objects or arrays inline within the providers value prop. This will cause the context to change on every render, and therefore all consuming components to rerender.


4. Consider Alternatives for Frequent Updates :

  • useReducer with Context:
    • For complex state management with frequent updates, combine useContext with useReducer. This allows you to centralize state logic and optimize updates.
  • External State Management Libraries:
    • For very large and complex applications, consider using external state management libraries like Redux, Zustand, or Jotai. These libraries offer more advanced features for optimizing performance and managing state.


Example of Memoization :

import React, { createContext, useContext, useState, useMemo, memo } from 'react';

const MyContext = createContext();

const MyProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const contextValue = useMemo(() => ({
    count,
    increment: () => setCount(count + 1),
  }), [count]); // Memoize the context value

  return (
    <MyContext.Provider value={contextValue}>
      {children}
    </MyContext.Provider>
  );
};

const MyConsumer = memo(() => { // Memoize the consumer
  const { count, increment } = useContext(MyContext);
  console.log("MyConsumer rendered");

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
});

export default function App() {
  return (
    <MyProvider>
      <MyConsumer />
    </MyProvider>
  );
}

By following these optimization techniques, you can effectively use useContext without sacrificing performance in your React applications.

 

While useContext is a great tool for managing state, especially for avoiding prop drilling, it might not be the best fit for all situations. Here are some alternatives for state management in React, ranging from simpler to more complex solutions:

1. useReducer :

  • When to Use:
    • For complex state logic, especially when the next state depends on the previous state.
    • When you need to manage multiple related state values.
    • When you want to centralize state update logic.
  • How it Works:
    • useReducer takes a reducer function and an initial state as arguments.
    • The reducer function specifies how the state should be updated based on actions.
    • It returns the current state and a dispatch function to trigger state updates.
  • Advantages:
    • Predictable state updates.
    • Centralized state logic.
    • Easier to test.

2. Prop Drilling (For Simple Cases) :

  • When to Use:
    • For small applications or components with shallow component trees.
    • When the data being passed down is simple and doesn't change frequently.
  • How it Works:
    • Passing data as props from parent to child components.
  • Advantages:
    • Simple and straightforward.
    • No additional dependencies.
  • Disadvantages:
    • Can become cumbersome and difficult to maintain as the component tree grows.

3. External State Management Libraries :

  • Redux:
    • A predictable state container for JavaScript applications.
    • Centralized store, actions, and reducers.
    • Best for large, complex applications with global state.
  • Zustand:
    • A small, fast, and scalable bearbones state-management solution.
    • Uses a simpler API than Redux.
    • Good for both small and large applications.
  • Jotai:
    • Primitive and flexible state management for React.
    • Atom-based approach.
    • Good for dynamic and frequently changing states.
  • Recoil:
    • Developed by Facebook, Recoil solves many problems associated with standard React state.
    • Uses atoms and selectors.
    • Good for complex state dependencies.
  • Advantages:
    • Centralized state management.
    • Improved performance.
    • Better organization and maintainability.
    • Time travel debugging (Redux).
  • Disadvantages:
    • Increased complexity.
    • Requires learning a new API.
    • Can add overhead to smaller projects.

4. Component Composition :

  • When to Use:
    • When you want to encapsulate state and logic within a reusable component.
    • When you can compose components to pass data.
  • How it Works:
    • Creating reusable components that manage their own state and logic.
    • Passing data and callbacks between components using props.
  • Advantages:
    • Encapsulation and reusability.
    • Improved code organization.
  • Disadvantages:
    • Can become complex if not managed properly.

Choosing the Right Approach :

  • For simple state management within a single component, useState is sufficient.
  • For complex state logic within a component, useReducer is a good choice.
  • For avoiding prop drilling in small to medium-sized applications, useContext is effective.
  • For large, complex applications with global state, external state management libraries like Redux, Zustand, Jotai, or Recoil are recommended.
  • For reusable components, component composition is ideal.

Consider the complexity of your application, the size of your team, and your personal preferences when choosing a state management solution.

In React, useState is a Hook that allows you to add state to functional components. Essentially, it's a way to make functional components "remember" values between renders. Here's a breakdown :

What is State?

  • In React, "state" refers to data that can change over time within a component.
  • When state changes, React re-renders the component to reflect those changes in the UI.

How useState Works:

  1. Declaration:

    • You import useState from the react library.
    • Inside your functional component, you call useState with an initial value.
    • useState returns an array with two elements:
      • The current state value.
      • A function that allows you to update the state value.
  2. Using the State:

    • You can use the current state value in your component's JSX to display dynamic data.
    • When you need to update the state, you call the update function provided by useState.
  3. Re-renders:

    • When you call the update function, React schedules a re-render of the component.
    • During the re-render, React updates the state value and updates the UI accordingly.

Key Concepts:

  • Initial State:
    • The argument you pass to useState is the initial value of the state.
    • This value is only used during the first render of the component.
  • State Update Function:
    • The function returned by useState is used to update the state.
    • When updating state, it's best practice to use functional updates, especially when the new state depends on the previous state.
  • Re-renders:
    • React re-renders the component whenever the state is updated.
    • This ensures that the UI always reflects the current state.

Example :

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default Counter;

In this example :  

* useState(0) initializes the count state to 0.
* setCount is the function used to update the count value.
* When the button is clicked, setCount(count + 1) increments the count and triggers a re-render.

useState is a fundamental Hook in React that simplifies state management in functional components.

When working with useState in React, updating the state correctly is crucial for ensuring your components behave as expected. Here's a breakdown of how to update state, along with important considerations:

1. Using the Setter Function:

  • useState returns an array containing the current state value and a setter function. This setter function is how you update the state.
  • Example :
  • const [count, setCount] = useState(0);
    setCount(count + 1); // Updates the count

2. Updating Based on the Previous State (Functional Updates) :

  • When the new state depends on the previous state, it's essential to use a functional update. This involves passing a function to the setter function.
  • This approach prevents issues caused by asynchronous state updates and ensures you're working with the most up-to-date state.
  • Example :
  • setCount((prevCount) => prevCount + 1);

    In this example, prevCount represents the previous state value.


3. Updating Objects and Arrays :

  • useState doesn't automatically merge objects or arrays. Therefore, when updating these types of state, you need to create new copies of the data.
  • Updating Objects :
  • const [user, setUser] = useState({ name: 'John', age: 30 });
    setUser((prevUser) => ({
      ...prevUser, // Create a copy of the previous object
      age: prevUser.age + 1, // Update the specific property
    }));
  • Updating Arrays :
  •  const [items, setItems] = useState([1, 2, 3]);
    setItems((prevItems) => [...prevItems, 4]); // Create a new array with the added item

Key Considerations :

  • Immutability: Always treat state as immutable. Create new copies of objects and arrays instead of modifying the existing ones.
  • Asynchronous Updates: State updates can be asynchronous, so relying on the current state value immediately after calling the setter function might lead to unexpected results. Functional updates are the best way to avoid this.
  • Batching: React may batch multiple state updates for performance reasons. This means that multiple calls to the setter function within the same event handler might be combined into a single re-render.

By following these guidelines, you can ensure that your state updates are reliable and predictable.

When you update state in React using useState with the same value as the current state, React performs an optimization that prevents unnecessary re-renders. Here's a more detailed explanation:

React's Optimization :

  • Object.is Comparison:
    • React uses the Object.is comparison algorithm to determine if the new state value is the same as the current state value.
    • If Object.is determines that the values are identical, React will typically skip re-rendering the component and its children.
  • Preventing Unnecessary Re-renders:
    • This optimization is crucial for performance, as it avoids unnecessary updates to the DOM.
    • Re-rendering components can be computationally expensive, so skipping re-renders when the state hasn't actually changed helps to improve the efficiency of your application.

Key Points :

  • Component Evaluation:
    • It's important to note that even if React skips updating the DOM, it might still need to evaluate the component function itself. This is because React needs to determine whether any child components or other parts of the component's logic need to be updated.
    • However, React will avoid "going deeper" into the component tree if it detects that the state hasn't changed.
  • Reference Types (Objects and Arrays):
    • When dealing with objects and arrays, the Object.is comparison checks for reference equality. This means that if you create a new object or array with the same values as the previous one, React will consider them to be different, even if their contents are the same.
    • Therefore, when updating objects and arrays, it is very important to make sure to create new object and array references.
  • Functional Updates:
    • When using functional updates, if the function returns the exact same value as the previous state, React will also bail out of the re-render.

While useState is excellent for simple state management, handling complex state can become challenging. Here's how you can effectively manage complex state with useState, along with considerations for when to use alternatives:

Techniques for Managing Complex State with useState:

  • Using Objects for Structured Data :
    • When dealing with multiple related state values, group them into an object.
    • Remember to create new object copies when updating to maintain immutability.
    • Example :
    • const [formData, setFormData] = useState({
        name: '',
        email: '',
        address: {
          street: '',
          city: '',
        },
      });
      
      const handleInputChange = (e) => {
        const { name, value } = e.target;
        setFormData((prevData) => ({
          ...prevData,
          [name]: value,
        }));
      };
      
      const handleAddressChange = (e) => {
        const { name, value } = e.target;
        setFormData((prevData) => ({
          ...prevData,
          address: {
            ...prevData.address,
            [name]: value,
          },
        }));
      };
  • Using Functional Updates:
    • Always use functional updates when the new state depends on the previous state. This is especially important with complex objects and arrays.
    • Functional updates ensure you're working with the most up-to-date state.
  • Splitting State into Multiple useState Calls:
    • If your state is very complex and independent parts of it change frequently, consider splitting it into multiple useState calls.
    • This can improve performance by reducing unnecessary re-renders.
  • Combining useState with Custom Hooks:
    • For reusable state logic, create custom hooks that encapsulate useState and related functions.
    • This promotes code reusability and organization.


When to Consider Alternatives :

  • Deeply Nested State:
    • If your state has deeply nested objects or arrays, updating it can become cumbersome and error-prone.
  • Complex State Transitions:
    • When your state transitions involve complex logic or multiple related updates, useReducer is a better choice.
  • Global State Management:
    • If your state needs to be shared across multiple components, especially in a large application, consider using useContext or an external state management library like Redux, Zustand, or Recoil.
  • Large and frequent state updates:
    • When an application has many state updates happening very often, then useReducer is likely a better option.


Key Considerations :

  • Immutability:
    • Maintaining immutability is crucial for predictable state updates. Always create new copies of objects and arrays.
  • Code Organization:
    • As your state becomes more complex, prioritize code organization and maintainability.
  • Performance:
    • Be mindful of performance implications, especially when dealing with large objects and arrays.

useEffect is a React Hook that lets you perform side effects in functional components.

What are Side Effects?

Side effects are actions that affect things outside of the component's render output. Examples include:

  • Data fetching: Making API calls to retrieve data.
  • DOM manipulation: Directly changing the DOM (though React typically handles this).
  • Setting up subscriptions: Listening to events or data streams.
  • Timers: Using setTimeout or setInterval.
  • Logging: Outputting data to the console.

Why Use useEffect?

In functional components, you can't directly perform these side effects within the main body of the component. This is because:

  • React needs to maintain a predictable rendering process.
  • Side effects can cause unexpected behavior if they're not managed properly.

useEffect provides a way to:

  • Run side effects after the component renders.
  • Control when side effects are executed.
  • Clean up side effects to prevent memory leaks or other issues.

How useEffect Works:

useEffect takes two arguments:

  1. A callback function: This function contains the code for your side effect.
  2. An optional dependency array: This array specifies the values that the effect depends on.

Basic Usage :

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // This effect runs after every render
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

 

Dependency Array:

  • If you provide a dependency array, the effect will only run when the values in the array change.
  • An empty dependency array ([]) means the effect will only run once after the initial render.
  • Omitting the dependency array means the effect will run after every render.

Cleanup Function:

  • If your effect sets up a subscription or timer, you should provide a cleanup function to unsubscribe or clear the timer when the component unmounts or the effect runs again.
  • The cleanup function is returned by the effect callback.
  • useEffect(() => {
      const timerID = setInterval(() => {
        console.log('Tick');
      }, 1000);
    
      return () => {
        clearInterval(timerID); // Cleanup function
      };
    }, []); // Runs only once

In essence :

useEffect allows you to synchronize your component with external systems or perform actions that are not directly related to rendering, while also providing a mechanism for cleaning up those actions.

 

useEffect is a versatile Hook in React that allows you to manage side effects in various ways. Here's a breakdown of the different ways you can use it:

1. Running Effects After Every Render (No Dependency Array):

  • This is the simplest form of useEffect.

  • The effect callback function will run after every render of the component.

  • Use this when you need to perform an action on every render, regardless of changes in props or state.

  • Example :

    useEffect(() => {
      console.log('Component rendered');
    });

     

2. Running Effects Only Once (Empty Dependency Array):

  • By providing an empty dependency array ([]), you tell React to run the effect only once after the initial render.

  • This is useful for actions that should only happen once, such as fetching data on component mount or setting up an initial subscription.

  • Example :

    useEffect(() => {
      fetchData(); // Fetch data on component mount
    }, []);

     

3. Running Effects Only When Dependencies Change (Dependency Array):

  • This is the most common and powerful way to use useEffect.

  • You provide a dependency array containing the values that the effect depends on (props or state).

  • The effect will only run when any of the values in the dependency array change.

  • This helps to optimize performance and prevent unnecessary re-renders.

  • Example :

    useEffect(() => {
      document.title = `Count: ${count}`;
    }, [count]); // Run effect only when 'count' changes

     

4. Cleaning Up Effects:

  • If your effect sets up a subscription, timer, or other resource that needs to be cleaned up, you can return a cleanup function from the effect callback.

  • The cleanup function will be called:

    • Before the component unmounts.
    • Before the effect runs again (if the dependencies change).
  • This helps to prevent memory leaks and other issues.

  • Example :

    useEffect(() => {
      const timerID = setInterval(() => {
        console.log('Tick');
      }, 1000);
    
      return () => {
        clearInterval(timerID); // Cleanup function
      };
    }, []);

     

5. Using Multiple useEffect Hooks:

  • You can use multiple useEffect hooks in a single component.

  • This allows you to separate different side effects and manage them independently.

  • This can improve code organization and readability.

  • Example :

    useEffect(() => {
      // Effect 1: Update document title
    }, [count]);
    
    useEffect(() => {
      // Effect 2: Fetch data
    }, []);

     

Key Considerations :

  • Dependency Array: Pay close attention to the dependency array. Incorrect dependencies can lead to bugs or performance issues.
  • Cleanup Functions: Always provide cleanup functions for effects that require them.
  • Separation of Concerns: Use multiple useEffect hooks to separate different side effects.

By understanding these different ways to use useEffect, you can effectively manage side effects in your React components.

The dependency array in useEffect is a crucial part of controlling when and how your side effects run. Here's a detailed explanation of how it works:

Purpose of the Dependency Array :

  • The dependency array is an optional second argument to the useEffect Hook.
  • It tells React which values the effect depends on.
  • React uses this array to determine whether the effect needs to be re-run.


How React Uses the Dependency Array :

  1. Initial Render :

    • On the initial render of the component, React always executes the effect callback function.
  2. Subsequent Renders :

    • On subsequent renders, React compares the current values of the dependencies in the array with their previous values.
    • It uses the Object.is comparison algorithm to check if any of the dependencies have changed.
  3. Effect Execution :

    • If any of the dependencies have changed, React executes the effect callback function again.
    • If none of the dependencies have changed, React skips executing the effect callback function.


Different Scenarios :

  • Empty Dependency Array ([]) :
    • When you provide an empty dependency array, React treats it as if there are no dependencies.
    • This means the effect will only run once after the initial render of the component.
    • This is useful for effects that should only happen once, such as fetching data on component mount.
  • Dependency Array with Values ([dep1, dep2, ...]) :
    • When you provide a dependency array with values (props or state variables), React monitors those specific values for changes.
    • The effect will re-run whenever any of the values in the array change.
    • This is the most common use case for useEffect, as it allows you to control when the effect runs based on specific dependencies.
  • No Dependency Array (Omitted) :
    • If you omit the dependency array, React will execute the effect callback function after every render of the component.
    • This can lead to performance issues if the effect is expensive or if it causes unnecessary re-renders.
    • It is generally recommended to always provide a dependency array to avoid these issues.


Key Considerations :

  • Include All Dependencies :
    • It's essential to include all values that the effect depends on in the dependency array.
    • If you omit a dependency, the effect might not behave as expected, and you could encounter bugs.
    • Modern linters for react will usually warn you if you have incorrectly set up your dependancy array.
  • Object and Array Dependencies :
    • When dealing with objects and arrays as dependencies, React compares them by reference.
    • This means that if you create a new object or array with the same values as the previous one, React will consider them to be different, even if their contents are the same.
    • To avoid unnecessary re-renders, you can memoize objects and arrays using useMemo or useCallback.
  • Cleanup Functions and Dependencies :
    • If your effect has a cleanup function, that function will also be rerun when the dependencies change, right before the next effect runs. This is important to remember when structuring your side effects.

In summary, the dependency array in useEffect allows you to precisely control when your side effects run, which is essential for optimizing performance and preventing bugs.

When you don't provide a dependency array to useEffect, the effect callback function will run after every render of the component. This has significant implications:

Here's what happens :

  • Execution After Every Render :

    • Regardless of whether the component's props or state have actually changed, the effect will execute.
    • This means that any side effect within the useEffect callback will be performed on each and every re-render.
  • Potential Performance Issues :

    • If the side effect is computationally expensive or involves network requests, this can lead to significant performance problems.
    • Unnecessary side effects can slow down your application and create a poor user experience.
  • Risk of Infinite Loops :

    • If the effect updates state, and that state update triggers a re-render, you can easily create an infinite loop. The effect updates state, which re-renders the component, which runs the effect again, and so on.
  • Unintended Side Effects :

    • Side effects that should only happen under specific conditions will occur every time the component renders, which can lead to unexpected behavior.


In essence :

  • Omitting the dependency array is equivalent to telling React: "Run this effect after every single render, no matter what."


When to (Rarely) Use It :

  • There are very few legitimate use cases for omitting the dependency array.
  • One potential scenario is when you need to perform a side effect on every single render, regardless of changes. However, this is generally discouraged due to the potential performance implications.
  • It is much better practice to create a dependancy array, even if it is an empty array.


Key Takeaway :

  • It's strongly recommended to always provide a dependency array to useEffect. This allows you to control when the effect runs and prevent unnecessary re-renders.
  • Modern react linters will often give warnings when a dependancy array is missing.

Cleaning up side effects in useEffect is crucial for preventing memory leaks and ensuring your components behave correctly. Here's how you do it:

The Cleanup Function :

  • Within the useEffect callback, you can return a function.
  • This returned function is known as the "cleanup function."
  • React calls this cleanup function at specific times.


When the Cleanup Function Is Called :

  • Component Unmounting :
    • When the component is removed from the DOM (unmounted), React calls the cleanup function. This allows you to cancel any ongoing operations or remove event listeners.
  • Before the Next Effect Run (Dependencies Change):
    • If the useEffect Hook has a dependency array, and the values in that array change, React will:
      • Call the cleanup function from the previous effect.
      • Then, run the new effect.
    • This ensures that any previous side effects are properly cleaned up before a new one is started.


Common Scenarios for Cleanup :

  • Timers :
    • If you use setTimeout or setInterval in your effect, you need to clear the timers in the cleanup function.
  • Event Listeners :
    • If you add event listeners to the DOM, you need to remove them in the cleanup function.
  • Subscriptions :
    • If you subscribe to data streams or other sources, you need to unsubscribe in the cleanup function.
  • Fetch aborts :
    • If you have a fetch request running, when dependencies change, or the component unmounts, you may wish to abort the fetch.


Example: Cleaning Up a Timer :

import React, { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timerID = setInterval(() => {
      setSeconds((prevSeconds) => prevSeconds + 1);
    }, 1000);

    return () => {
      clearInterval(timerID); // Cleanup function
    };
  }, []);

  return <div>Seconds: {seconds}</div>;
}

export default Timer;


In this example :

  • The setInterval function sets up a timer.
  • The cleanup function, clearInterval(timerID), is returned from the useEffect callback.
  • When the component unmounts, React calls the cleanup function, clearing the timer.

Key Points :

  • Always provide a cleanup function for effects that require it.
  • Cleanup functions are essential for preventing memory leaks and ensuring your components are well-behaved.
  • Thinking of the cleanup function as the reverse of what you did in the effect is a good way to structure it.

By using cleanup functions, you can keep your React applications clean and efficient.

 



useMemo is a React Hook that plays a crucial role in performance optimization by memoizing the result of a computation. Here's a breakdown of how it works and how it helps:

What is Memoization?

  • Memoization is a technique that stores the result of a function call and returns the cached result when the same inputs occur again.
  • This avoids redundant computations, which can be especially beneficial for expensive or time-consuming operations.


How useMemo Works :

  1. Arguments :

    • useMemo takes two arguments:
      • A function that performs the computation.
      • A dependency array.
  2. Caching the Result :

    • On the initial render, useMemo executes the provided function and stores the result.
    • On subsequent renders, useMemo checks the dependency array.
    • If the values in the dependency array have not changed, useMemo returns the cached result from the previous render, without re-executing the computation function.
    • If the values in the dependency array have changed, useMemo re-executes the computation function, stores the new result, and returns it.


How useMemo Optimizes Performance :

  • Avoiding Expensive Computations :

    • If you have a function that performs a complex calculation or data transformation, useMemo can prevent it from being re-executed unnecessarily.
    • This is particularly useful when the computation depends on props or state that don't change frequently.
  • Preventing Unnecessary Re-renders of Child Components :

    • When you pass a computed value as a prop to a child component, the child component might re-render even if the value hasn't actually changed.
    • By using useMemo to memoize the computed value, you can ensure that the child component only re-renders when the value changes.
    • This is especially helpful when combined with React.memo, which memoizes the entire child component.
  • Reference Equality :

    • useMemo returns a memoized value. This is very useful when passing down values that rely on referential equality to prevent unneeded rerenders. For Example, if you are passing down an object, or an array as a prop. Without useMemo, a new object or array will be generated on every render, even if the contents are the same. This will cause the child component to rerender.

Example :

import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ a, b }) {
  const result = useMemo(() => {
    console.log('Calculating...');
    // Simulate an expensive calculation
    let sum = 0;
    for (let i = 0; i < 100000000; i++) {
      sum += a + b;
    }
    return sum;
  }, [a, b]);

  return <div>Result: {result}</div>;
}

function App() {
  const [num1, setNum1] = useState(1);
  const [num2, setNum2] = useState(2);

  return (
    <div>
      <input type="number" value={num1} onChange={(e) => setNum1(parseInt(e.target.value))} />
      <input type="number" value={num2} onChange={(e) => setNum2(parseInt(e.target.value))} />
      <ExpensiveCalculation a={num1} b={num2} />
    </div>
  );
}

export default App;

 

In this example :

  • The useMemo Hook memoizes the result of the expensive calculation.
  • The calculation will only be re-executed when a or b changes.
  • Without useMemo, the calculation would be re-executed on every render, even if a and b remained the same.

By strategically using useMemo, you can significantly improve the performance of your React applications.

 

 

Handling Asynchronous Operations with Hooks in React

React Hooks don't have built-in support for asynchronous operations, but you can handle them using async functions, Promises, or libraries like axios and fetch. Here’s how you can manage async operations effectively.


1. Using useEffect for Side Effects (Fetching Data)

Since useEffect runs after render, it’s a great place to fetch data from an API.

Example: Fetching Data with fetch
import { useState, useEffect } from "react";

function FetchData() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
        if (!response.ok) throw new Error("Failed to fetch");
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []); // Empty dependency array → runs once

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <p>Title: {data?.title}</p>;
}
* Key Takeaways :
  • Fetch inside useEffect to run the request when the component mounts.
  • Use async/await inside a function (not directly in useEffect).
  • Handle loading and error states properly.

2. Handling Asynchronous State Updates

State updates using useState are synchronous in React, but you can use useEffect to react to state changes asynchronously.

import { useState, useEffect } from "react";

function AsyncCounter() {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDoubleCount(count * 2);
    }, 500);

    return () => clearTimeout(timer); // Cleanup
  }, [count]); // Runs when count changes

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <p>Double (delayed): {doubleCount}</p>
    </div>
  );
}
* Why use useEffect?
  • Prevents race conditions by cleaning up previous timers before setting a new one.
  • Ensures derived state updates asynchronously.

3. Using useCallback for Asynchronous Functions

If you're passing an async function as a prop to child components, wrap it in useCallback to prevent unnecessary re-renders.

import { useState, useCallback } from "react";

function FetchButton({ fetchData }) {
  return <button onClick={fetchData}>Fetch Data</button>;
}

function ParentComponent() {
  const [data, setData] = useState(null);

  const fetchData = useCallback(async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/2");
    const result = await response.json();
    setData(result);
  }, []);

  return (
    <div>
      <FetchButton fetchData={fetchData} />
      <p>Data: {data?.title}</p>
    </div>
  );
}
* Why use useCallback?
  • Prevents unnecessary re-renders of child components that depend on fetchData.

4. Using useReducer for Complex Async State Logic

For managing complex async state (e.g., loading, success, failure states), use useReducer.

import { useReducer, useEffect } from "react";

const initialState = {
  loading: true,
  data: null,
  error: null,
};

function reducer(state, action) {
  switch (action.type) {
    case "SUCCESS":
      return { loading: false, data: action.payload, error: null };
    case "ERROR":
      return { loading: false, data: null, error: action.payload };
    default:
      return state;
  }
}

function FetchWithReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/posts/3");
        const result = await response.json();
        dispatch({ type: "SUCCESS", payload: result });
      } catch (err) {
        dispatch({ type: "ERROR", payload: err.message });
      }
    }
    fetchData();
  }, []);

  if (state.loading) return <p>Loading...</p>;
  if (state.error) return <p>Error: {state.error}</p>;
  return <p>Title: {state.data?.title}</p>;
}
* Why useReducer?
  • Better for handling complex async state transitions (loading, success, failure).
  • Makes state updates more predictable.

5. Using useContext for Asynchronous Global State

For fetching data once and sharing it globally, use useContext with a provider.

import { createContext, useContext, useState, useEffect } from "react";

const DataContext = createContext();

function DataProvider({ children }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts/4");
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []);

  return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
}

function DisplayData() {
  const data = useContext(DataContext);
  return <p>Title: {data?.title || "Loading..."}</p>;
}

function App() {
  return (
    <DataProvider>
      <DisplayData />
    </DataProvider>
  );
}
* Why useContext?
  • Avoids prop drilling when sharing async data across multiple components.

Best Practices for Handling Async in Hooks :
Best Practice Do This
Use useEffect for API calls Fetch data inside useEffect
Use async inside functions, not useEffect directly Define async functions inside useEffect
Handle errors properly Use try/catch and setError states
Cleanup side effects Return a cleanup function inside useEffect
Use useCallback for async functions in props Prevent unnecessary re-renders
Use useReducer for complex state updates Manage loading, success, and error states
No, You Cannot Use Hooks Inside Loops or Conditions!

React has strict rules for using Hooks, and one of the most important rules is:

Hooks must be called at the top level of a component and not inside loops, conditions, or nested functions.

This ensures that React preserves the order of Hooks across renders.


What Happens if You Break This Rule?

If you call Hooks inside a loop, condition, or nested function, React loses track of their order across renders, causing unpredictable behavior.

Example of Incorrect Usage :
function Example({ condition }) {
  if (condition) {
    // WRONG: Hook inside a condition
    const [count, setCount] = useState(0);
  }

  return <p>Count: {count}</p>; // count is not defined
}

Fix : Always declare Hooks at the top level:

function Example({ condition }) {
  const [count, setCount] = useState(0); // Always at the top

  return condition ? <p>Count: {count}</p> : <p>No count</p>;
}

How to Handle Conditional Logic Properly?

Instead of conditionally using a Hook, conditionally render components that use Hooks.

Correct Approach: Move Hooks to a Separate Component
function Counter() {
  const [count, setCount] = useState(0);

  return <p>Count: {count}</p>;
}

function Example({ condition }) {
  return condition ? <Counter /> : <p>No counter</p>;
}

This way, Counter always initializes its Hooks properly.


What About Loops?

If you need multiple instances of a Hook, store data in an array and map over it.

Incorrect Usage (Hook Inside a Loop)
function WrongComponent() {
  const items = [1, 2, 3];

  items.forEach(item => {
    // ? WRONG: Hooks inside a loop
    const [count, setCount] = useState(0);
  });

  return <p>Invalid Hook usage!</p>;
}
Correct Usage (Use Arrays and Map)
function Counter({ id }) {
  const [count, setCount] = useState(0);
  return (
    <p>
      Item {id}: {count}
    </p>
  );
}

function CorrectComponent() {
  const items = [1, 2, 3];

  return (
    <div>
      {items.map(item => (
        <Counter key={item} id={item} />
      ))}
    </div>
  );
}

* Each Counter component has its own useState, ensuring consistent behavior.


Summary
Don't Do This Do This Instead
Hooks inside loops Use .map() to render components
Hooks inside conditions Always declare Hooks at the top level
Hooks inside nested functions Define the Hook outside the function
Performance Pitfalls When Using Hooks & How to Fix Them

React Hooks make development easier, but misusing them can lead to performance issues like unnecessary re-renders, stale closures, and memory leaks. Here are some common pitfalls and how to fix them.


1. Unnecessary Re-renders Due to useState Updates :
Pitfall: Updating state with the same value :

If you call setState with the same value, React will still re-render the component.

function Counter() {
  const [count, setCount] = useState(0);

  console.log("Component re-rendered");

  return <button onClick={() => setCount(0)}>Set to 0</button>;
}

* Issue: Clicking the button re-renders even though the state hasn’t changed.

* Fix : Use a function update to check the current state.

<button onClick={() => setCount(prev => (prev === 0 ? prev : 0))}>Set to 0</button>

2. Infinite Loops in useEffect :
Pitfall: Updating state inside useEffect without proper dependencies
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // Causes infinite loop
  }, [count]);

  return <p>Count: {count}</p>;
}

* Issue: Every time count updates, useEffect triggers another update.

* Fix: Use a function update or an appropriate dependency array.

useEffect(() => {
  setCount(prev => prev + 1);
}, []); //  Runs only once

3. Unnecessary Function Re-Creation in useEffect :
Pitfall : Declaring functions inside useEffect
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const log = () => console.log(count); // Recreated every render
    document.addEventListener("click", log);

    return () => document.removeEventListener("click", log);
  }, [count]); // Memory leak risk
}

* Issue: Every re-render creates a new log function, causing multiple event listeners.

* Fix: Use useCallback to memoize the function.

const log = useCallback(() => console.log(count), [count]);

useEffect(() => {
  document.addEventListener("click", log);
  return () => document.removeEventListener("click", log);
}, [log]);

4. Stale State Due to Missing Dependencies in useEffect :
Pitfall: Using old state inside useEffect
useEffect(() => {
  console.log(count); //  May log an outdated count
}, []); //  Missing [count]

* Issue : The effect captures the initial value of count, not the latest.

* Fix : Always add dependencies to the array.

useEffect(() => {
  console.log(count);
}, [count]);

5. Over-Reliance on useMemo and useCallback :
Pitfall : Using useMemo and useCallback when unnecessary
const computedValue = useMemo(() => a * b, [a, b]); //  Unnecessary if computation is simple
const handleClick = useCallback(() => console.log("Clicked"), []); //  Doesn't need memoization

* Issue : These Hooks add complexity and should be used only for expensive computations.

* Fix : Use them only when necessary (e.g., for expensive calculations or preventing unnecessary prop changes).


6. Unnecessary Re-renders Due to Unstable Props :
Pitfall: Passing functions directly as props without memoization
function Parent() {
  return <Child onClick={() => console.log("Clicked")} />; //  New function every render
}

* Issue : Child re-renders every time even if onClick doesn’t change.

* Fix : Use useCallback to prevent unnecessary re-renders.

const handleClick = useCallback(() => console.log("Clicked"), []);
<Child onClick={handleClick} />;

7. Memory Leaks from Unmounted Components :
Pitfall: Updating state after component unmounts
useEffect(() => {
  fetchData().then(data => setState(data)); // Possible memory leak
}, []);

* Issue : If the component unmounts before fetchData completes, React tries to update an unmounted component.

* Fix : Use a cleanup function.

useEffect(() => {
  let isMounted = true;

  fetchData().then(data => {
    if (isMounted) setState(data);
  });

  return () => {
    isMounted = false;
  };
}, []);

Best Practices to Optimize Hooks
 Bad Practice Best Practice
Updating state unnecessarily Only update if values change
Missing dependencies in useEffect Always include necessary dependencies
Recreating functions inside components Use useCallback for stable functions
Using useMemo everywhere Use it only for expensive calculations
Updating state inside useEffect without cleanup Use cleanup functions to avoid memory leaks
Implementing Debouncing and Throttling with React Hooks

When working with events like search inputs, scrolling, or resizing, it's important to optimize performance by reducing unnecessary function calls. Two common techniques for this are:

  • Debouncing: Delays execution until after a certain period of inactivity.
  • Throttling: Ensures execution happens at most once in a given time interval.

1. Debouncing with useEffect and setTimeout

Debouncing is useful when handling search inputs or resizing where you want to execute a function only after the user stops typing/moving.

* Custom useDebounce Hook
import { useState, useEffect } from "react";

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler); // Cleanup previous timer
  }, [value, delay]);

  return debouncedValue;
}
* Usage: Debounced Search Input
function SearchBar() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 500); // Waits 500ms after last input

  useEffect(() => {
    if (debouncedQuery) {
      console.log("Fetching results for:", debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}
* Key Benefits :
  • Prevents excessive API calls.
  • Updates only after user stops typing.

2. Throttling with useRef and setTimeout :

Throttling ensures a function executes at most once per interval. It's useful for scrolling, resizing, or button spamming.

* Custom useThrottle Hook
import { useState, useEffect, useRef } from "react";

function useThrottle(value, delay) {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastExecuted = useRef(Date.now());

  useEffect(() => {
    const handler = setTimeout(() => {
      if (Date.now() - lastExecuted.current >= delay) {
        setThrottledValue(value);
        lastExecuted.current = Date.now();
      }
    }, delay - (Date.now() - lastExecuted.current));

    return () => clearTimeout(handler);
  }, [value, delay]);

  return throttledValue;
}
* Usage: Throttled Scroll Event
function ScrollLogger() {
  const [scrollPos, setScrollPos] = useState(0);
  const throttledScrollPos = useThrottle(scrollPos, 1000); // Throttles updates every 1s

  useEffect(() => {
    const handleScroll = () => {
      setScrollPos(window.scrollY);
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  return <p>Scroll Position: {throttledScrollPos}</p>;
}
* Key Benefits :
  • Limits execution rate to once per interval.
  • Improves performance in scroll-heavy applications.

When to Use Debounce vs Throttle?
Feature Debounce Throttle
Use Case Search input, API calls Scroll, resize, click events
Execution Fires after user stops Fires at fixed intervals
Example Auto-suggest search bar Tracking scroll position
Testing React Components with Hooks

When testing components that use React Hooks, the goal is to ensure they behave correctly across renders, state changes, and side effects. You can use Jest and React Testing Library (RTL) to write effective tests.


1. Testing Hooks in Components (Using React Testing Library)

For components that use useState, useEffect, or other Hooks, test their behavior, not implementation details.

* Example: Testing useState in a Counter Component
import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "./Counter"; // Component using useState

test("increments counter on button click", () => {
  render(<Counter />);
  
  const button = screen.getByText("Count: 0");
  fireEvent.click(button);
  
  expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
* Key Points :
  • Render the component using render().
  • Simulate user interaction with fireEvent.click().
  • Assert that the UI updates as expected.

2. Testing useEffect Side Effects (API Calls)

For components that fetch data inside useEffect, mock API calls using Jest.

* Example: Testing API Call in useEffect
import { render, screen, waitFor } from "@testing-library/react";
import axios from "axios";
import Users from "./Users"; // Fetches users from API
jest.mock("axios");

test("fetches and displays users", async () => {
  axios.get.mockResolvedValue({ data: [{ name: "Alice" }] });

  render(<Users />);
  
  expect(screen.getByText(/Loading/i)).toBeInTheDocument();

  // Wait for async data to load
  await waitFor(() => screen.getByText("Alice"));

  expect(screen.getByText("Alice")).toBeInTheDocument();
});
* Key Points :
  • Mock API calls with jest.mock().
  • Use waitFor() to wait for async updates.

3. Testing Custom Hooks

For testing standalone Hooks, use the renderHook utility from @testing-library/react-hooks.

* Example: Testing a Custom useCounter Hook
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

test("increments counter", () => {
  const { result } = renderHook(() => useCounter());

  act(() => result.current.increment());

  expect(result.current.count).toBe(1);
});
* Key Points :
  • Use renderHook() to call the Hook.
  • Use act() to simulate state updates.

Summary :
Hook Used Testing Approach
useState Simulate user events, check UI updates
useEffect Mock API calls, use waitFor()
useContext Wrap component in context provider
useReducer Dispatch actions, verify state changes
useCustomHook Use renderHook(), trigger updates with act()
The differences between the class component and functional component are as follows :

Points Functional Components Class Components
Declaration These are nothing but JavaScript Functions. So, you declare it in the same manner as the JavaScript function. On the other hand, class components are declared using the ES6 class.
Handling Props Handling props is very straightforward. You can use any prop as an argument to a functional component that can be directly used inside HTML elements. For class components, the props are handled differently. Here, we make use of the “this” keyword.
Handling State Functional components use react hooks for handling state. For the class components, we can't use the hooks, so for this case, for handling the state, we make use of a different syntax.
Constructor For the functional components, constructors are not used. For the class components, constructors are used for storing the state.
Render Method In the functional component, there is no use of the render() method. In the class component, it must have the render() method.
Both the state and props are JavaScript objects. They are different in their functionality regarding the component.

State Props
States are managed within the component, similar to variables declared within a function. Props get passed to the component similar to function parameters.
State is mutable(that is it can be modified). Props are immutable(that is they cannot be modified).
You can read as well as modify states. Props are read-only.
class Count extends Component {
    state = {
      value: 0,
    };
 
    incrementValue = () => {
      this.setState({
        value: this.state.value + 1,
      });
    };
 
    render() {
      return (
        <div>
          <button onClick={this.incrementValue}>Count:{this.state.value}</button>
        </div>
      );
    }
  }


how can you rewrite this component using react hooks?

The equivalent code using function component and react hooks are shown below,

import React, { useState } from "react";

const Count = () => {
 const [value, setvalue] = useState(0);

  return (
    <div>
      <button
        onClick={() => {
          setCount(value + 1);
        }}
      >
        Count: {value}
      </button>
    </div>
  );
}
Thing is that you can update the current state value by passing the new value to the update function or by passing a  callback function. The second technique is safe to use.

Below, is the code for updating the current state value based on the previous state.
import React, { useState } from "react";

const Count = () => {
 const [value, setValue] = useState(0);

 const increment= () => {
   setCount((prevValue) => {
     return prevValue + 1;
   });
 };

 const decrement = () => {
   setCount((prevValue) => {
     return prevValue - 1;
   });
 };

 return (
   <div>
     <strong>Count: {value}</strong>
     <button onClick={increment}>Increment</button>
     <button onClick={decrement}>Decrement</button>
   </div>
 );
}?
we can fetch data with useEffect in React by performing the data fetching operation inside the effect function. Typically, we use asynchronous functions like fetch to make HTTP requests to an API endpoint and update the component state with the fetched data.

import React,
{
    useEffect,
    useState
}
    from 'react';

function App() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("See the Effect here");
    });

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() =>
                setCount(count + 1)}>
                Increment
            </button>
        </div>
    );
}
export default App;?
Lazy Initialization with useState in React

Lazy initialization in useState helps optimize performance by computing the initial state only when the component first renders, rather than on every render.


1. What is Lazy Initialization?

Normally, useState directly assigns an initial value:

const [count, setCount] = useState(0);

* Issue: If the initial value is expensive to compute (e.g., fetching data, performing calculations), it runs on every render, even if unnecessary.

* Lazy Initialization Fix: Pass a function to useState instead of a value:

const [count, setCount] = useState(() => expensiveCalculation());

Here, expensiveCalculation runs only once, during the initial render.


2. Example: Expensive Calculation with Lazy Initialization
function Counter() {
  function expensiveCalculation() {
    console.log("Running expensive calculation...");
    return 1000; // Imagine a complex operation
  }

  const [count, setCount] = useState(() => expensiveCalculation());

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
    </div>
  );
}
What's Happening?
  • useState(() => expensiveCalculation()) ensures expensiveCalculation only runs once during the initial render.
  • On re-renders, count is retrieved from React’s state instead of recalculating.

3. When Should You Use Lazy Initialization?
* Use it when :
  • The initial state requires expensive computations.
  • The initial state fetches data from local storage, API, or database.
* Don't use it if :
  • The initial value is a simple primitive (like 0, "", or false).
  • The function is lightweight and doesn't impact performance.

Summary :
Approach Code Example When to Use?
Normal Initialization useState(0) For simple values
Lazy Initialization useState(() => expensiveCalculation()) When computing the initial state is expensive
We can share state logic between components in React using techniques such as prop drilling, lifting state up, or by using state management libraries like Redux or React Context.

* Prop Drilling : Pass state and functions down through props from parent to child components. This is suitable for simple applications with a shallow component tree.

* Lifting State Up : Move shared state and related functions to the nearest common ancestor component and pass them down as props to child components. This is effective for managing state across multiple related components.

* State Management Libraries : Use state management libraries like Redux or React Context for managing global state that needs to be accessed by multiple components across the application. These libraries provide centralized state management and make it easier to share state logic across components.