React State Management

React State Managment

In React, state refers to data that can change over time. For example, if a user clicks a button, the state might update, and the app’s display will change accordingly.

State management is the process of handling this data, storing it, and making sure it updates the app when needed. React provides tools like useState, Context API, and Redux to manage state in different ways.

1. What is State in React?

In React, state refers to a built-in object that allows components to store and manage dynamic data that can change over time. It determines how a component renders and behaves. When the state of a component changes, React re-renders that component to reflect the updated data.

Example: Basic State with useState

import React, { useState } from “react”;

function Counter() {

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

  return (

    <div>

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

      <button onClick={() => setCount(count + 1)}>Increment</button>

    </div>

  );

}

export default Counter;

In this example:

  • count is a state variable.

  • setCount is the function used to update the state.

  • When the button is clicked, count is updated, and React re-renders the component to reflect the new value.

Key Points About State

  • State is mutable (it can change).

  • State updates trigger re-renders in React.

  • Unlike props (which are read-only), state can be modified within the component.

2. Difference Between Local, Component, and Global State

State in React can be categorized into different types based on its scope and how it is used.

(A) Local State (Component-Level State)

Local state is contained within a single component and does not affect other components.
Example:

function ToggleButton() {

  const [isOn, setIsOn] = useState(false); // Local state

  return (

    <button onClick={() => setIsOn(!isOn)}>

      {isOn ? “ON” : “OFF”}

    </button>

  );

}

  • The isOn state only affects the ToggleButton component.

  • Other components are not aware of this state.

(B) Component State (Shared Between Components)

Sometimes, state needs to be shared between multiple sibling components. This is done using props drilling (passing state from a parent component down to children).

Example: Managing state in a parent component and passing it down:

function Parent() {

  const [message, setMessage] = useState(“Hello from Parent!”);

  return (

    <Child message={message} />

  );

}

function Child({ message }) {

  return <p>{message}</p>;

}

  • The Parent component manages the state.

  • The Child component receives it via props.

 Limitation: When multiple components need to access the same state, prop drilling can make code hard to maintain.

(C) Global State (App-Level State)

When multiple components across the application need access to the same state, we use global state management solutions like:

  • React Context API

  • Redux

  • Recoil

  • Zustand

Example: Using Context API for Global State

import React, { createContext, useState, useContext } from “react”;

// Create Context

const ThemeContext = createContext();

function App() {

  const [theme, setTheme] = useState(“light”);

  return (

    <ThemeContext.Provider value={{ theme, setTheme }}>

      <ChildComponent />

    </ThemeContext.Provider>

  );

}

function ChildComponent() {

  const { theme, setTheme } = useContext(ThemeContext);

  return (

    <div>

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

      <button onClick={() => setTheme(“dark”)}>Change Theme</button>

    </div>

  );

}

  • The theme state is accessible across multiple components without passing it explicitly.

3. Why State Management is Important in Large Applications

In small applications, state management is simple. But as applications grow, managing state efficiently becomes crucial. Here’s why:

(A) Avoids Prop Drilling

  • Passing state through multiple layers of components is inefficient.

  • Solution: Use Context API or state management libraries.

(B) Ensures Data Consistency

  • Multiple components may need synchronized data.

  • Example: A shopping cart that needs to update items across different pages.

(C) Enhances Performance

  • Poorly managed state can cause unnecessary re-renders, slowing down the app.

  • Solution: Optimize state updates with useMemo, useCallback, and selectors.

(D) Makes Debugging Easier

  • With centralized state, tracking changes is easier.

  • Redux, for example, provides a time-travel debugging feature.

4. Challenges of Poor State Management

Without proper state management, applications can suffer from:

(A) Prop Drilling Problem

  • Passing state deep down through multiple child components makes code hard to read and maintain.

(B) Unnecessary Renders

  • Frequent and uncontrolled re-renders affect performance.

  • Example: If a parent component updates, all child components may re-render unnecessarily.

(C) Difficulty in Debugging

  • If state is spread across multiple components, tracking down issues becomes time-consuming.

(D) Synchronization Issues

  • Multiple components might use outdated state if not properly synchronized.

5. Overview of State Management Approaches (Built-in vs External Libraries)

(A) Built-in State Management

  1. useState

    • Best for local component-level state.

    • Example: Form inputs, toggles, counters.

  2. useReducer

    • Best for managing complex state logic within a component.

    • Example: Managing a state with multiple conditions.

  3. Context API

    • Best for sharing state globally without prop drilling.

    • Example: Theme switching, authentication state.

(B) External State Management Libraries

  1. Redux (with Redux Toolkit)

    • Best for large-scale applications with complex state logic.

    • Example: E-commerce shopping carts, large dashboards.

  2. Recoil

    • Simplifies global state with atoms and selectors.

    • Example: Modern, flexible state management.

  3. Zustand

    • Lightweight state management without boilerplate.

    • Example: Game states, UI settings.

  4. React Query

    • Best for handling async state (API requests).

    • Example: Fetching and caching API data.

Module 2:Local Component State with useState

1. Understanding useState Hook

useState is a React Hook that allows functional components to have local state. It helps store and update values that change dynamically within a component.

 

Syntax of useState

 

const [state, setState] = useState(initialValue);

  • state: Holds the current value.

  • setState: A function to update the state.

  • initialValue: The initial value of the state.

Example: Basic Counter using useState

 

import React, { useState } from “react”;

function Counter() {

  const [count, setCount] = useState(0); // Initial state set to 0

  return (

    <div>

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

      <button onClick={() => setCount(count + 1)}>Increment</button>

    </div>

  );

}

export default Counter;

 How It Works:

  • Clicking the button updates the state (count).

  • React re-renders the component to reflect the updated state.

2. Updating Primitive vs Complex State

 

(A) Primitive State (Numbers, Strings, Booleans)

Primitive values like numbers, strings, and booleans are directly replaced when updated.

Example: Updating a String

function Greeting() {

  const [message, setMessage] = useState(“Hello”);

  return (

    <div>

      <p>{message}</p>

      <button onClick={() => setMessage(“Welcome!”)}>Change Message</button>

    </div>

  );

}

 Best Practice: Always replace primitives directly using setState.

(B) Complex State (Objects & Arrays)

Updating objects and arrays requires careful handling because React doesn’t automatically merge nested updates.

 

Example: Handling Object State

function UserProfile() {

  const [user, setUser] = useState({ name: “John”, age: 25 });

  return (

    <div>

      <p>Name: {user.name}</p>

      <p>Age: {user.age}</p>

      <button onClick={() => setUser({ …user, age: user.age + 1 })}>

        Increase Age

      </button>

    </div>

  );

}

 Explanation:

  • { …user } ensures that the previous object properties remain unchanged.

  • Only age is updated without affecting name.

Best Practice: Use spread operator () to maintain previous object values.

3. Handling Objects and Arrays in State

(A) Updating Arrays in useState

Since arrays are reference types, direct modification won’t trigger a re-render.

Example: Adding Items to an Array

function TodoList() {

  const [tasks, setTasks] = useState([“Task 1”, “Task 2”]);

  return (

    <div>

      <ul>

        {tasks.map((task, index) => (

          <li key={index}>{task}</li>

        ))}

      </ul>

      <button onClick={() => setTasks([…tasks, `Task ${tasks.length + 1}`])}>

        Add Task

      </button>

    </div>

  );

}

Explanation:

  • setTasks([…tasks, newTask]) creates a new array with the added item.

Best Practice: Always use the spread operator to create a new array instead of modifying the existing one.

(B) Removing Items from an Array

To remove an item from an array, use filter().

function ShoppingCart() {

  const [cart, setCart] = useState([“Apple”, “Banana”, “Orange”]);

  const removeItem = (itemToRemove) => {

    setCart(cart.filter((item) => item !== itemToRemove));

  };

  return (

    <div>

      {cart.map((item) => (

        <p key={item}>

          {item} <button onClick={() => removeItem(item)}>Remove</button>

        </p>

      ))}

    </div>

  );

}

Best Practice: Use filter() to return a new array without the removed item.

4. Functional Updates for Performance Optimization

Using the previous state value directly inside setState ensures that updates happen sequentially and correctly.

(A) Example: Using a Function Inside setState

function ClickCounter() {

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

  return (

    <div>

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

      <button onClick={() => setCount((prevCount) => prevCount + 1)}>

        Increment

      </button>

    </div>

  );

}

 

Why Use Functional Updates?

 

  • Ensures correctness when multiple updates happen quickly.

  • Avoids stale state issues in async updates.

 Best Practice: Use the previous state (prevState) when updating based on the old value.

5. Common Pitfalls and Best Practices

(A) Directly Modifying State (❌ Bad Practice)

function WrongUpdate() {

  const [user, setUser] = useState({ name: “Alice”, age: 30 });

  const updateAge = () => {

    user.age = 31; // Direct modification (React won’t detect this change)

    setUser(user); //  Won’t trigger re-render

  };

  return <button onClick={updateAge}>Update Age</button>;

}

 Fix: Always create a new object when updating state.

setUser({ …user, age: 31 });

(B) Skipping Dependencies in State Updates ( Bad Practice)

Updating state without referencing the previous state can lead to stale state issues.

function WrongCounter() {

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

  return (

    <button onClick={() => setCount(count + 1)}>Increment</button>

  );

}

 Fix: Use a function to ensure the latest state.

setCount((prevCount) => prevCount + 1);

(C) Using State Inefficiently

 Storing unnecessary state:

const [isVisible, setIsVisible] = useState(someProp ? true : false);

 Fix: Compute derived values dynamically.

const isVisible = someProp;

Module 3: Using useReducer for Complex State Management

1. What is useReducer, and Why Use It?

useReducer is a React Hook that is an alternative to useState for managing complex state logic. It is particularly useful when:

  • State transitions depend on the previous state.

  • Multiple related state variables need to be updated together.

  • State updates involve multiple actions (like handling form submissions or a counter with multiple operations).

 Key Benefits of useReducer:
Helps manage complex state logic more effectively.

 Makes state transitions predictable by defining them in a single function (the reducer).


Improves code maintainability by separating state logic from the UI.

2. How useReducer Works (Actions, Reducer, State)

The useReducer Syntax

const [state, dispatch] = useReducer(reducerFunction, initialState);

  • state: Holds the current state.

  • dispatch: A function used to trigger actions.

  • reducerFunction: A function that updates the state based on an action.

  • initialState: The starting state.

The Reducer Function

A reducer is a pure function that takes current state and an action, then returns new state.

function 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;

  }

}

3. Example: Counter App Using useReducer

import React, { useReducer } from “react”;

// Reducer function

function 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;

  }

}

function 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>

  );

}

export default Counter;

 Why Use useReducer Here?

  • The counter has multiple actions (INCREMENT, DECREMENT, RESET).

  • The state transitions are clear and centralized in the reducer function.

  • Helps avoid prop drilling if integrated with Context API.

4. Example: Managing Form State with useReducer

For complex forms, useReducer is better than useState because:

  • It handles multiple fields efficiently.

  • It groups all state updates in a single place.

Implementation: Managing a Login Form

import React, { useReducer } from “react”;

// Reducer function

function formReducer(state, action) {

  switch (action.type) {

    case “UPDATE_FIELD”:

      return { …state, [action.field]: action.value };

    case “RESET”:

      return { username: “”, password: “” };

    default:

      return state;

  }

}

function LoginForm() {

  const [state, dispatch] = useReducer(formReducer, { username: “”, password: “” });

  return (

    <div>

      <input

        type=”text”

        value={state.username}

        onChange={(e) => dispatch({ type: “UPDATE_FIELD”, field: “username”, value: e.target.value })}

        placeholder=”Username”

      />

      <input

        type=”password”

        value={state.password}

        onChange={(e) => dispatch({ type: “UPDATE_FIELD”, field: “password”, value: e.target.value })}

        placeholder=”Password”

      />

      <button onClick={() => console.log(state)}>Submit</button>

      <button onClick={() => dispatch({ type: “RESET” })}>Reset</button>

    </div>

  );

}

export default LoginForm;

Why Use useReducer Here?

  • Each field is updated efficiently without multiple useState calls.

  • The RESET action clears all fields at once.

  • Centralized logic makes the form easier to maintain.

5. When to Choose useReducer Over useState

Feature

useState

useReducer

Simple state

✅ Good choice

❌ Overkill

Complex state logic

❌ Difficult to manage

✅ Better approach

Multiple state updates

❌ Scattered logic

✅ Centralized logic

Dependent state updates

❌ Hard to handle

✅ Easier with actions

Performance optimization

❌ Can cause unnecessary re-renders

✅ Optimized updates

Choose useReducer when:  The state depends on the previous state.
Multiple related state values need to be managed together.
There are many types of actions affecting the state.

6. Combining useReducer with Context API for Global State Management

Using useReducer with the Context API allows state to be shared across multiple components without prop drilling.

Step 1: Create a Global State Context

import React, { createContext, useReducer, useContext } from “react”;

// Reducer function

function counterReducer(state, action) {

  switch (action.type) {

    case “INCREMENT”:

      return { count: state.count + 1 };

    case “DECREMENT”:

      return { count: state.count – 1 };

    default:

      return state;

  }

}

// Create Context

const CounterContext = createContext();

export function CounterProvider({ children }) {

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

  return (

    <CounterContext.Provider value={{ state, dispatch }}>

      {children}

    </CounterContext.Provider>

  );

}

// Custom Hook to Use Counter

export function useCounter() {

  return useContext(CounterContext);

}

Step 2: Use Context in Components

import React from “react”;

import { useCounter } from “./CounterProvider”;

function CounterDisplay() {

  const { state } = useCounter();

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

}

function CounterControls() {

  const { dispatch } = useCounter();

  return (

    <div>

      <button onClick={() => dispatch({ type: “INCREMENT” })}>+</button>

      <button onClick={() => dispatch({ type: “DECREMENT” })}>-</button>

    </div>

  );

}

export default function App() {

  return (

    <CounterProvider>

      <CounterDisplay />

      <CounterControls />

    </CounterProvider>

  );

}

Why Combine useReducer with Context API? 

 Avoids prop drilling (no need to pass state manually).


Global state management without an external library (like Redux).
Better organization of complex state logic.

Module 4: Context API for Global State Management

1. Understanding Context API and Its Role

What is Context API?

The Context API is a built-in React feature that helps manage global state without prop drilling (passing props manually through multiple components). It allows you to share state across different parts of your application efficiently.

Why Use Context API?

Avoids Prop Drilling: No need to pass state manually through multiple layers of components.


 Centralized State Management: Ideal for auth, themes, language preferences, and other global states.


Lightweight Alternative to Redux: Unlike Redux, it doesn’t require extra dependencies.

When to Use Context API?

Use Context API when:


 The state needs to be shared across multiple components.


 The state does not change frequently (e.g., theme, authentication).


 You want a simple, built-in solution instead of an external library.

2. Creating a Context and Provider

To use Context API, we need to:
Create a Context
Provide the Context using a Provider
 Consume the Context in components

Step 1: Create a Context

import { createContext } from “react”;

export const ThemeContext = createContext(null);

Step 2: Create a Provider Component

A Provider wraps components and makes state available to them.

import React, { createContext, useState } from “react”;

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {

  const [theme, setTheme] = useState(“light”);

  const toggleTheme = () => {

    setTheme((prevTheme) => (prevTheme === “light” ? “dark” : “light”));

  };

  return (

    <ThemeContext.Provider value={{ theme, toggleTheme }}>

      {children}

    </ThemeContext.Provider>

  );

}

Step 3: Wrap the App with Provider

import React from “react”;

import { ThemeProvider } from “./ThemeContext”;

import Home from “./Home”;

function App() {

  return (

    <ThemeProvider>

      <Home />

    </ThemeProvider>

  );

}

export default App;

Now, the Home component and its children can access the theme state.

3. Consuming Context using useContext

Once Context is provided, any child component can consume it using useContext.

Using useContext in a Component

import React, { useContext } from “react”;

import { ThemeContext } from “./ThemeContext”;

function Home() {

  const { theme, toggleTheme } = useContext(ThemeContext);

  return (

    <div style={{ background: theme === “light” ? “#fff” : “#333”, color: theme === “light” ? “#000” : “#fff”, padding: “20px” }}>

      <h2>Current Theme: {theme}</h2>

      <button onClick={toggleTheme}>Toggle Theme</button>

    </div>

  );

}

export default Home;

How useContext Helps?


No need for prop drilling – the theme and toggleTheme are directly available inside Home.


Cleaner Code – We don’t need to pass props from App to Home.

4. Performance Optimization Techniques (Avoiding Unnecessary Renders)

While Context API is powerful, it can cause unnecessary re-renders if not optimized properly.

Problem: Unwanted Rerenders

When the context state updates, all components consuming it re-render, even if the update is not relevant to them.

Solution 1: Separate Context for Different States

Instead of having one context for all states, create separate contexts for different types of state.

Bad Approach: One Large Context

export const AppContext = createContext();

export function AppProvider({ children }) {

  const [theme, setTheme] = useState(“light”);

  const [user, setUser] = useState(null); // User state

  return (

    <AppContext.Provider value={{ theme, setTheme, user, setUser }}>

      {children}

    </AppContext.Provider>

  );

}

Problem: Even if only user changes, all components using theme will also re-render.

Better Approach: Separate Contexts

export const ThemeContext = createContext();

export const UserContext = createContext();

export function ThemeProvider({ children }) {

  const [theme, setTheme] = useState(“light”);

  return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;

}

export function UserProvider({ children }) {

  const [user, setUser] = useState(null);

  return <UserContext.Provider value={{ user, setUser }}>{children}</UserContext.Provider>;

}

Benefit: Components consuming ThemeContext won’t re-render when UserContext updates.

Solution 2: Memoization with useMemo

Use useMemo to optimize context value and avoid unnecessary updates.

import React, { createContext, useState, useMemo } from “react”;

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {

  const [theme, setTheme] = useState(“light”);

  const contextValue = useMemo(() => ({ theme, setTheme }), [theme]);

  return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;

}

Benefit: The provider only updates when theme changes, reducing unnecessary re-renders.

5. Limitations of Context API and When to Use External Libraries

While Context API is useful for global state, it has limitations in large applications.

Limitations of Context API

 Re-renders on every state change – Can be inefficient for frequently changing data.
 Complexity increases with multiple contexts – Managing multiple providers can be hard to scale.


 Not ideal for deeply nested updates – If a deeply nested component needs to update global state, Context API alone may not be efficient.

When to Use an External Library?

Use Redux or Zustand when:

  • You need efficient state updates without unnecessary re-renders.

  • Your app has many components relying on global state.

  • You need state persistence (e.g., saving user data between sessions).

  • Your state management logic is too complex for Context API.

Example: Use Redux when handling large-scale data like e-commerce carts.


 Use Zustand when needing lightweight, scalable state management.

Module 5: Redux – Scalable State Management

Redux is one of the most powerful state management libraries used in React applications. In this module, we’ll cover why Redux is needed, how it works, and how to implement it efficiently using Redux Toolkit and Redux Thunk.

1. What is Redux and Why Use It?

What is Redux?

Redux is a predictable state container for JavaScript applications. It helps manage global state efficiently and makes state changes traceable, scalable, and maintainable.

Why Use Redux?

 Centralized State Management – Stores the entire application’s state in one place.


Predictable Updates – Uses a strict unidirectional data flow, making state changes clear and debuggable.


Works Well with Large Applications – Ideal for complex state interactions, such as authentication, cart management, or API responses.
Time-Travel Debugging – Redux DevTools allow developers to rewind and replay state changes.

 When to Use Redux?


Use Redux when:
The application has a lot of shared state (e.g., authentication, cart, user settings).


Components deeply nested need access to global state.


You need predictable and trackable state updates.

 When NOT to Use Redux?


For simple apps where Context API or useState is enough.
When state updates are local to a few components.

2. Understanding Redux Concepts: Actions, Reducers, Store

Redux follows a strict architecture to manage state in a predictable way.

Core Concepts:

1 Actions: Define what should happen (e.g., ADD_TO_CART).
2 Reducers: Define how state should change based on an action.
3 Store: The global state container holding all the data.
4 Dispatch: The function used to send actions to the store.
5 Selectors: Functions to retrieve specific state from the store.

Redux Data Flow:

1 Component dispatches an action
2 Reducer updates the state based on the action
3 Updated state is stored in the Redux store
4 Components re-render with the new state.

3. Setting Up Redux in a React Project

Step 1: Install Redux and React-Redux

npm install @reduxjs/toolkit react-redux

Step 2: Create a Redux Store

Create a file store.js:

import { configureStore } from “@reduxjs/toolkit”;

import counterReducer from “./counterSlice”;

export const store = configureStore({

  reducer: {

    counter: counterReducer,

  },

});

Step 3: Create a Reducer (Slice)

Create counterSlice.js:

import { createSlice } from “@reduxjs/toolkit”;

const counterSlice = createSlice({

  name: “counter”,

  initialState: { count: 0 },

  reducers: {

    increment: (state) => {

      state.count += 1;

    },

    decrement: (state) => {

      state.count -= 1;

    },

    reset: (state) => {

      state.count = 0;

    },

  },

});

export const { increment, decrement, reset } = counterSlice.actions;

export default counterSlice.reducer;

Step 4: Provide the Store to React App

Wrap your app inside the Provider (in index.js or App.js):

import React from “react”;

import ReactDOM from “react-dom”;

import { Provider } from “react-redux”;

import { store } from “./store”;

import App from “./App”;

ReactDOM.render(

  <Provider store={store}>

    <App />

  </Provider>,

  document.getElementById(“root”)

);

Step 5: Use Redux State in Components

Use useSelector to read state and useDispatch to modify it.

Counter Component Example:

import React from “react”;

import { useSelector, useDispatch } from “react-redux”;

import { increment, decrement, reset } from “./counterSlice”;

function Counter() {

  const count = useSelector((state) => state.counter.count);

  const dispatch = useDispatch();

  return (

    <div>

      <h2>Count: {count}</h2>

      <button onClick={() => dispatch(increment())}>Increment</button>

      <button onClick={() => dispatch(decrement())}>Decrement</button>

      <button onClick={() => dispatch(reset())}>Reset</button>

    </div>

  );

}

export default Counter;

 Now, Redux manages the counter state globally!

4. Using Redux Toolkit for Simplified State Management

Redux Toolkit (RTK) simplifies Redux by reducing boilerplate code.

Benefits of Redux Toolkit

Less Code – Reduces the need for writing action types and reducers manually.


Built-in createSlice – Automatically creates actions and reducers.


Easier Async Handling – Built-in createAsyncThunk simplifies API calls.

 We already used Redux Toolkit (createSlice) in our counter example!

5. Handling Asynchronous Data with Redux Thunk

Redux does not handle async operations (like API calls) natively. Redux Thunk allows you to write async logic inside actions.

Step 1: Install Redux Thunk

npm install redux-thunk

Step 2: Create an Async Thunk

Example: Fetching data from an API.

usersSlice.js

import { createSlice, createAsyncThunk } from “@reduxjs/toolkit”;

// Async action to fetch users

export const fetchUsers = createAsyncThunk(“users/fetchUsers”, async () => {

  const response = await fetch(“https://jsonplaceholder.typicode.com/users”);

  return response.json();

});

const usersSlice = createSlice({

  name: “users”,

  initialState: { users: [], loading: false, error: null },

  reducers: {},

  extraReducers: (builder) => {

    builder

      .addCase(fetchUsers.pending, (state) => {

        state.loading = true;

      })

      .addCase(fetchUsers.fulfilled, (state, action) => {

        state.loading = false;

        state.users = action.payload;

      })

      .addCase(fetchUsers.rejected, (state, action) => {

        state.loading = false;

        state.error = action.error.message;

      });

  },

});

export default usersSlice.reducer;

Step 3: Use it in a Component

import React, { useEffect } from “react”;

import { useSelector, useDispatch } from “react-redux”;

import { fetchUsers } from “./usersSlice”;

function UsersList() {

  const dispatch = useDispatch();

  const { users, loading, error } = useSelector((state) => state.users);

  useEffect(() => {

    dispatch(fetchUsers());

  }, [dispatch]);

  if (loading) return <p>Loading…</p>;

  if (error) return <p>Error: {error}</p>;

  return (

    <ul>

      {users.map((user) => (

        <li key={user.id}>{user.name}</li>

      ))}

    </ul>

  );

}

export default UsersList;

 Redux Thunk handles async logic cleanly!

6. Best Practices for Efficient Redux Implementation

 Use Redux only for truly global state – Avoid using it for local state (use useState).


Use Redux Toolkit – Reduces boilerplate and improves performance.


Use createSlice – Automatically generates actions and reducers.


Optimize performance with useMemo & useSelector – Prevent unnecessary re-renders.


Use Redux DevTools – Helps debug state changes easily.

Module 6: Recoil – Simplified and Efficient State Management

Recoil is a modern state management library for React that provides a simpler and more flexible alternative to Redux. This module covers what Recoil is, how it works, and why it’s a great choice for managing global state efficiently.

1. What is Recoil and How It Differs from Redux?

What is Recoil?

Recoil is a state management library for React developed by Facebook. It allows components to share state without needing complex boilerplate like Redux.

How Recoil Differs from Redux?

Feature

Recoil

Redux

Setup Complexity

Simple

Complex (Needs actions, reducers, store)

Boilerplate Code

Minimal

More code required

State Structure

Decentralized (Atoms)

Centralized (Global Store)

Performance

Fine-grained updates (renders only affected components)

Can cause unnecessary re-renders

Ease of Use

Uses React-like APIs (useRecoilState, useRecoilValue)

Requires dispatch, actions, reducers

Async State Handling

Built-in support with selectors (useRecoilValueLoadable)

Needs middleware (Redux Thunk, Redux Saga)

When to Use Recoil?

 When you need simpler global state management.
When Redux is too heavy for your project.
When you want better performance with fewer re-renders.

 When NOT to Use Recoil?
If you already have a well-structured Redux setup.
If your app does not require global state management.

2. Understanding Atoms and Selectors

Recoil’s state management is based on two key concepts:

Atoms (State Units)

Atoms are the smallest pieces of state in Recoil.

 They can be shared across components and updated independently.

 Example: Creating an Atom (counterAtom.js)

import { atom } from “recoil”;

export const counterState = atom({

  key: “counterState”, // Unique identifier

  default: 0, // Initial value

});

Selectors (Derived State)

 Selectors allow you to compute derived state based on atoms.
They automatically update when the atom state changes.

 Example: Creating a Selector (counterSelector.js)

import { selector } from “recoil”;

import { counterState } from “./counterAtom”;

export const doubleCounterState = selector({

  key: “doubleCounterState”,

  get: ({ get }) => get(counterState) * 2, // Derived state

});

3. Setting Up Recoil Root in a React App

Step 1: Install Recoil

npm install recoil

Step 2: Wrap the App with RecoilRoot

Recoil needs to be initialized at the root level to manage state.

 In index.js or App.js:

import React from “react”;

import ReactDOM from “react-dom”;

import { RecoilRoot } from “recoil”;

import App from “./App”;

ReactDOM.render(

  <RecoilRoot>

    <App />

  </RecoilRoot>,

  document.getElementById(“root”)

);

4. Managing Global State with Recoil

Step 1: Create a Counter Component

Recoil provides useRecoilState (similar to useState) to manage state.

Counter.js

import React from “react”;

import { useRecoilState, useRecoilValue } from “recoil”;

import { counterState } from “./counterAtom”;

import { doubleCounterState } from “./counterSelector”;

function Counter() {

  const [count, setCount] = useRecoilState(counterState);

  const doubleCount = useRecoilValue(doubleCounterState);

  return (

    <div>

      <h2>Counter: {count}</h2>

      <h3>Double: {doubleCount}</h3>

      <button onClick={() => setCount(count + 1)}>Increment</button>

      <button onClick={() => setCount(count – 1)}>Decrement</button>

    </div>

  );

}

export default Counter;

 Now, state is managed globally with Recoil!
No reducers, actions, or dispatch!
Simpler and more readable than Redux!

5. Performance Benefits of Recoil

Recoil optimizes performance in several ways:

Fine-Grained State Updates – Only components using specific atoms re-render.
Selector Caching – Derived state is computed only when necessary.
Asynchronous Data Handling – Recoil can handle API calls without Redux Thunk.
Concurrent Mode Compatibility – Works efficiently with React 18’s Concurrent Mode.

 Example: Fetching Data Asynchronously with Recoil

import { atom, selector, useRecoilValueLoadable } from “recoil”;

// Atom to store user list

export const usersState = atom({

  key: “usersState”,

  default: [], // Default empty array

});

// Selector to fetch users from API

export const fetchUsersState = selector({

  key: “fetchUsersState”,

  get: async () => {

    const response = await fetch(“https://jsonplaceholder.typicode.com/users”);

    return response.json();

  },

});

// Component to Display Users

function UsersList() {

  const usersLoadable = useRecoilValueLoadable(fetchUsersState);

  if (usersLoadable.state === “loading”) return <p>Loading…</p>;

  if (usersLoadable.state === “hasError”) return <p>Error loading users.</p>;

  return (

    <ul>

      {usersLoadable.contents.map((user) => (

        <li key={user.id}>{user.name}</li>

      ))}

    </ul>

  );

}

Module 7: Zustand – Minimalist State Management

 

1. Introduction to Zustand and Why It’s Lightweight

What is Zustand?

Zustand (German for “state”) is a fast and scalable state management library that simplifies managing global state in React applications. It was designed as an alternative to Redux, removing unnecessary complexity like reducers, actions, and context providers.

Why is Zustand Lightweight?

 Minimal Setup – No need for reducers, actions, or middleware.


No Context API Dependency – Unlike Redux or Recoil, Zustand doesn’t rely on React Context, leading to fewer re-renders.


Simple API – Uses a single store with easy-to-use methods (set, get, subscribe).


Built-in Middleware Support – Supports persisting state, logging, and undo/redo features without extra configuration.

Zustand is ideal for both small and large applications that need global state management without unnecessary complexity.

2. Creating a Store in Zustand

Zustand uses a store-based approach where you create a global store and access state across components using a simple API.

Step 1: Install Zustand

npm install zustand

Step 2: Create a Zustand Store

 counterStore.js

import { create } from “zustand”;

// Define Zustand store

const useCounterStore = create((set) => ({

  count: 0, // Initial state

  increment: () => set((state) => ({ count: state.count + 1 })), // Update function

  decrement: () => set((state) => ({ count: state.count – 1 })), // Update function

}));

export default useCounterStore;

3. Updating and Retrieving State in Zustand

Zustand provides an easy way to access and modify state in any component.

Counter Component using Zustand

import React from “react”;

import useCounterStore from “./counterStore”;

function Counter() {

  // Access state and actions

  const { count, increment, decrement } = useCounterStore();

  return (

    <div>

      <h2>Counter: {count}</h2>

      <button onClick={increment}>Increment</button>

      <button onClick={decrement}>Decrement</button>

    </div>

  );

}

export default Counter;

No need for reducers, dispatch, or extra boilerplate!


Works instantly without wrapping components with a Provider!

4. Zustand vs Redux: Which One to Choose?

Feature

Zustand

Redux

Boilerplate

Minimal

High

Performance

Fast (no unnecessary re-renders)

Can cause extra re-renders

Ease of Use

Very simple API (useStore())

Requires actions, reducers, dispatch

Middleware Support

Built-in (persist, subscribe)

Requires extra setup

Async State Handling

Native async support (without middleware)

Requires Redux Thunk/Saga

Best For

Small to large projects needing fast state updates

Large apps needing predictable state management

  1. Choose Zustand if:


You need simple global state management.
You want to avoid Redux boilerplate.
You need better performance with fewer re-renders.

  1. Choose Redux if:


You have a large, enterprise-level app.
You need strict state predictability and debugging tools.
You already have Redux experience and setup in your project.

5. Best Use Cases for Zustand

 When to Use Zustand?

 Small to medium-sized apps – Where Redux feels like overkill.


Real-time apps – Games, dashboards, or anything that updates state frequently.


Simple global state needs – Where React’s useState is insufficient.

 Avoiding unnecessary re-renders – Since Zustand doesn’t rely on Context API, it doesn’t cause extra component re-renders.

 When NOT to Use Zustand?

If your app already uses Redux and you don’t want to migrate.
If you need strict unidirectional state flow with complex debugging tools.

Module 8: Jotai – Atomic State Management

1. Introduction to Jotai

What is Jotai?

Jotai (meaning “atomic” in Japanese) is a primitive and flexible state management library that allows you to manage global and local state using atoms (independent state units).

Why Use Jotai?

Simpler than Redux & Recoil – No need for actions, reducers, or dispatch.

 Atomic State Management – Each piece of state (atom) is independent, avoiding unnecessary re-renders.


Built-in Asynchronous State Support – Handles async data without extra middleware.
Minimal Boilerplate – Easy to integrate with any React project.

 Installation

npm install jotai

2. Understanding Atoms in Jotai

Jotai revolves around atoms, which are the smallest units of state. Atoms can be shared across components, updated independently, and derived from other atoms.

 Creating and Using an Atom

import { atom, useAtom } from “jotai”;

// Define an atom (a piece of state)

const counterAtom = atom(0);

function Counter() {

  const [count, setCount] = useAtom(counterAtom);

  return (

    <div>

      <h2>Counter: {count}</h2>

      <button onClick={() => setCount(count + 1)}>Increment</button>

      <button onClick={() => setCount(count – 1)}>Decrement</button>

    </div>

  );

}

export default Counter;

This works just like useState, but the state is global!

 No need for a Provider like Context API!

3. Simplifying State Updates with Jotai

Using Derived State (Computed Atoms)

Jotai allows you to create derived state using selectors. These atoms automatically update when their dependencies change.

Example: Creating a Derived Atom

import { atom, useAtom } from “jotai”;

// Base atom (state)

const counterAtom = atom(0);

// Derived atom (computed value)

const doubleCounterAtom = atom((get) => get(counterAtom) * 2);

function Counter() {

  const [count, setCount] = useAtom(counterAtom);

  const doubleCount = useAtom(doubleCounterAtom)[0]; // Read-only

  return (

    <div>

      <h2>Counter: {count}</h2>

      <h3>Double: {doubleCount}</h3>

      <button onClick={() => setCount(count + 1)}>Increment</button>

      <button onClick={() => setCount(count – 1)}>Decrement</button>

    </div>

  );

}

export default Counter;

 No extra re-renders!
Efficient updates with atomic state management!

 

4. Jotai vs Recoil: Key Differences

Feature

Jotai

Recoil

Boilerplate

Minimal

Requires Context Provider

Atom Structure

Simple, no unique keys needed

Requires unique keys

Re-renders

Optimized, fewer re-renders

May cause extra renders

Async Support

Built-in async support

Uses selectors for async state

Ease of Use

Very easy (useAtom)

Requires useRecoilState and selectors

When to Use

Small/medium projects, simplicity-focused apps

Complex apps needing structured global state

Choose Jotai if:

  • You need simpler atomic state management.

  •  You want minimal re-renders and better performance.

  • You don’t want to define selectors or unique keys like Recoil.

 Choose Recoil if:

  •  You need structured global state management for large apps.

  •  You want to use Context API-like global state.

5. When to Use Jotai in Your React Projects

 Best Use Cases for Jotai:

  • Simple and scalable state management – Ideal for projects where useState and useContext are not enough.

  •  Real-time applications – Jotai’s atomic state updates work well in dashboards and financial apps.

  •  Forms and UI state – Great for managing UI state like modals, toggles, and form inputs.

  •  Asynchronous state handling – No need for Redux Thunk or additional libraries for async operations.

When NOT to Use Jotai?

  •  If you already use Redux or Recoil and don’t want to migrate.

  • If your app has very complex business logic requiring structured actions and reducers.

 

Module 9: Async State Management in React

1. Managing Async Data with Redux Toolkit (RTK Query)

What is RTK Query?

RTK Query is a built-in feature of Redux Toolkit that simplifies API data fetching, caching, and state management. It reduces the need for manual API handling with reducers, actions, and thunks.

Why Use RTK Query?

  •  Automatical

  • ly caches and updates data Reduces Redux boilerplate

  • Handles loading, success, and error states

  • Built-in refetching and polling

Setting Up RTK Query

Step 1: Install Redux Toolkit & RTK Query

npm install @reduxjs/toolkit react-redux

Step 2: Create an API Slice

 features/apiSlice.js

import { createApi, fetchBaseQuery } from “@reduxjs/toolkit/query/react”;

// Define API slice

export const apiSlice = createApi({

  reducerPath: “api”,

  baseQuery: fetchBaseQuery({ baseUrl: “https://jsonplaceholder.typicode.com” }),

  endpoints: (builder) => ({

    getPosts: builder.query({

      query: () => “/posts”,

    }),

  }),

});

// Export hook for use in components

export const { useGetPostsQuery } = apiSlice;

Step 3: Add API Slice to Store

store.js

import { configureStore } from “@reduxjs/toolkit”;

import { apiSlice } from “./features/apiSlice”;

export const store = configureStore({

  reducer: {

    [apiSlice.reducerPath]: apiSlice.reducer,

  },

  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware),

});

Step 4: Use RTK Query in a Component

import React from “react”;

import { useGetPostsQuery } from “./features/apiSlice”;

function PostList() {

  const { data: posts, error, isLoading } = useGetPostsQuery();

  if (isLoading) return <p>Loading…</p>;

  if (error) return <p>Error fetching posts!</p>;

  return (

    <ul>

      {posts.map((post) => (

        <li key={post.id}>{post.title}</li>

      ))}

    </ul>

  );

}

export default PostList;

 RTK Query automatically handles caching, fetching, and updating UI!

2. Using React Query for API Data Caching

What is React Query?

React Query is an efficient data-fetching library that helps manage server state, caching, pagination, and real-time updates in React.

Why Use React Query?

  •  Automatic data caching

  • Background refetching

  •  Optimistic updates for a smoother UI

  • Handles retries and error management

Setting Up React Query

Step 1: Install React Query

npm install @tanstack/react-query

Step 2: Create a Query Client

App.js

import { QueryClient, QueryClientProvider } from “@tanstack/react-query”;

import PostList from “./PostList”;

const queryClient = new QueryClient();

function App() {

  return (

    <QueryClientProvider client={queryClient}>

      <PostList />

    </QueryClientProvider>

  );

}

export default App;

Step 3: Fetch Data with React Query

PostList.js

import { useQuery } from “@tanstack/react-query”;

const fetchPosts = async () => {

  const res = await fetch(“https://jsonplaceholder.typicode.com/posts”);

  return res.json();

};

function PostList() {

  const { data: posts, error, isLoading } = useQuery([“posts”], fetchPosts);

  if (isLoading) return <p>Loading…</p>;

  if (error) return <p>Error loading posts!</p>;

  return (

    <ul>

      {posts.map((post) => (

        <li key={post.id}>{post.title}</li>

      ))}

    </ul>

  );

}

export default PostList;

React Query automatically caches data and refetches when needed!

3. Fetching and Updating Data with SWR

What is SWR?

SWR (Stale-While-Revalidate) is a data-fetching library from Vercel that provides fast, dynamic, and automatically updated data.

Why Use SWR?

  • Auto-revalidation when the page regains focus

  •  Supports SSR & CSR

  •  Built-in error handling and caching

  •  Lightweight and easy to use

Setting Up SWR

Step 1: Install SWR

npm install swr

Step 2: Fetch Data with SWR

import useSWR from “swr”;

const fetcher = (url) => fetch(url).then((res) => res.json());

function PostList() {

  const { data: posts, error } = useSWR(“https://jsonplaceholder.typicode.com/posts”, fetcher);

  if (!posts) return <p>Loading…</p>;

  if (error) return <p>Error fetching data</p>;

  return (

    <ul>

      {posts.map((post) => (

        <li key={post.id}>{post.title}</li>

      ))}

    </ul>

  );

}

export default PostList;

 SWR fetches fresh data while displaying cached data to improve UX!

4. Handling Loading, Error, and Success States Efficiently

When working with async state, it’s important to handle different states properly to improve UX and debugging.

Best Practices

  •  Show a Loading Indicator

if (isLoading) return <p>Loading…</p>;

  •  Handle Errors Gracefully

if (error) return <p>Error loading data. Please try again.</p>;

  • Retry Failed Requests (React Query)

const { data, error, isLoading } = useQuery([“posts”], fetchPosts, {

  retry: 3, // Retry failed requests up to 3 times

});

  •  Refetch Data on User Interaction

const queryClient = useQueryClient();

queryClient.invalidateQueries([“posts”]); // Refetch data manually

5. Best Practices for Async State Management

 Choose the Right Library Based on Use Case

Use Case

Best Library

Simple API calls

fetch() + useState

Global state with async data

Redux Toolkit (RTK Query)

Optimized caching & auto-refetching

React Query

Real-time updates & auto revalidation

SWR

1) Minimize Re-renders

  • Use memoization (useMemo, useCallback) for derived state.

  • Avoid storing entire API responses in the state.

2 Use Background Fetching for Better UX

  • Use React Query’s background refetching to avoid flickering.

  • SWR automatically revalidates data when the page is refocused.

3) Optimize API Calls

  • Debounce input-based API calls (e.g., search bars).

  • Use pagination and lazy loading to fetch data in chunks.

Module 10: Optimizing State Management for Performance

1. Avoiding Unnecessary Renders

Every time state changes, React re-renders components to reflect the new state. However, unnecessary renders can slow down performance.

Common Causes of Unnecessary Renders

  •  Updating state when it’s not required

  •  Passing new object/array references as props

  •  Using inline functions in JSX (causes new function instance creation)

  •  Overuse of context, causing child components to re-render

Solution: Use React.memo

React.memo is a higher-order component that prevents re-renders if the props haven’t changed.

Example: Preventing Unnecessary Re-Renders

import React from “react”;

const ExpensiveComponent = React.memo(({ count }) => {

  console.log(“Re-rendered”);

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

});

export default ExpensiveComponent;

 Now, ExpensiveComponent only re-renders when count changes!

2. Using Memoization with useMemo and useCallback

React provides useMemo and useCallback to optimize performance by memoizing values and functions.

 useMemo – Memoize Expensive Calculations

useMemo caches computed values to prevent unnecessary recalculations.

Example: Optimizing Expensive Computation

import React, { useState, useMemo } from “react”;

function ExpensiveCalculation({ number }) {

  const squared = useMemo(() => {

    console.log(“Calculating square…”);

    return number * number;

  }, [number]);

  return <p>Squared Value: {squared}</p>;

}

 Now, the square calculation only runs when number changes instead of on every render!

 useCallback – Memoize Functions to Prevent Re-Creation

useCallback caches function instances, preventing unnecessary re-renders in child components.

Example: Preventing Function Re-Creation

import React, { useState, useCallback } from “react”;

const Button = React.memo(({ handleClick }) => {

  console.log(“Button Rendered”);

  return <button onClick={handleClick}>Click Me</button>;

});

function App() {

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

  const increment = useCallback(() => {

    setCount((prev) => prev + 1);

  }, []);

  return (

    <div>

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

      <Button handleClick={increment} />

    </div>

  );

}

export default App;

 Now, Button won’t re-render unless increment changes!

3. Implementing Selectors for Efficient State Access

Selectors help optimize global state access by retrieving only required data instead of re-rendering entire components when unrelated state changes.

Using Selectors in Redux

import { createSelector } from “@reduxjs/toolkit”;

import { useSelector } from “react-redux”;

// Memoized Selector

const selectUserName = createSelector(

  (state) => state.user,

  (user) => user.name

);

function Profile() {

  const userName = useSelector(selectUserName);

  console.log(“Profile Rendered”);

  return <p>User: {userName}</p>;

}

export default Profile;

 Now, Profile only re-renders when user.name changes, not the entire user object!

4. Lazy Loading State for Better Performance

Lazy loading means deferring state initialization or component loading until it’s needed. This improves performance by reducing initial load time.

Lazy Loading State with useState

Instead of initializing state directly, pass a function to useState to compute state only when needed.

const [data, setData] = useState(() => {

  console.log(“Expensive Calculation Running…”);

  return performHeavyCalculation();

});

 Now, performHeavyCalculation only runs once!

 Lazy Loading Components with React Suspense

For large applications, load components on demand using React.lazy().

Example: Lazy Loading a Component

import React, { lazy, Suspense } from “react”;

const HeavyComponent = lazy(() => import(“./HeavyComponent”));

function App() {

  return (

    <Suspense fallback={<p>Loading…</p>}>

      <HeavyComponent />

    </Suspense>

  );

}

Now, HeavyComponent loads only when needed!

5. Debugging and Monitoring State Changes

To optimize performance, identify and fix unnecessary state updates using debugging tools.

Use React Developer Tools to Track Renders

  1. Install the React Developer Tools extension.

  2. Use the “Highlight Updates” feature to see re-renders.

Log State Updates with useEffect

import React, { useState, useEffect } from “react”;

function Counter() {

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

  useEffect(() => {

    console.log(“Count changed:”, count);

  }, [count]);

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

}

Now, every state update is logged in the console!

Profiling React Components

Use React Profiler to analyze component performance:

  1. Open React DevTools

  2. Go to the Profiler tab

  3. Click Record and interact with the UI

  4. Identify slow components and optimize them

Summary of Optimization Techniques

Optimization Technique

Benefit

React.memo

Prevents unnecessary re-renders

useMemo

Caches expensive computations

useCallback

Memoizes functions to avoid re-creation

Selectors (Redux/Context API)

Efficiently extracts data from state

Lazy Loading

Improves performance by loading components on demand

Debugging with React DevTools

Helps identify unnecessary renders

Conclusion

State management plays a crucial role in React applications, especially as they scale. Inefficient handling of state can lead to performance bottlenecks, such as unnecessary re-renders, sluggish UI updates, and excessive memory usage. To build fast, scalable, and maintainable applications, developers need to adopt best practices and optimization techniques.

 

In this guide, we explored various strategies to enhance state management efficiency, ensuring smooth performance across all React applications.

 

Key Takeaways for Optimizing State Management

 

1 Avoiding Unnecessary Renders

  •  Use React.memo to prevent components from re-rendering when props remain unchanged.

  •  Ensure state updates only when necessary, avoiding redundant state modifications.

  •  Avoid passing new object/array references as props unless required.

  • Reduce overuse of the Context API, as it can trigger unnecessary renders in deeply nested components.

2 Using Memoization (useMemo, useCallback)

  • Use useMemo to cache expensive computations, preventing unnecessary recalculations on every render.

  •  Use useCallback to memoize functions, avoiding function re-creations that trigger child component re-renders.

  •  Optimize components by ensuring functions inside them do not change unnecessarily.

3 Implementing Selectors for Efficient State Access

  •  In Redux, use selectors to extract only the required part of the state instead of accessing the entire store.

  • Avoid passing large state objects to components—use derived state or useSelector() in Redux with reselect.

  • For the Context API, use multiple small contexts instead of a single large context to prevent excessive renders.

4 Lazy Loading State and Components

  • Defer state initialization in useState by passing a function to avoid unnecessary re-executions.

  • Use React.lazy() and Suspense to load heavy components only when needed, reducing the initial load time.

  •  For larger applications, split code into dynamic imports for improved performance.

5 Debugging and Monitoring State Changes

  • Use React Developer Tools to analyze unnecessary re-renders.

  • Use useEffect to log state changes and identify unexpected updates.

  • Leverage React Profiler to measure component performance and optimize rendering behavior.



FAQs

1. What is state in React?

State in React is an object that holds dynamic data and determines a component’s behavior and rendering. It is managed within components and updated using functions like setState or hooks like useState.

  • Local state is confined to a specific component and managed using useState or useReducer.
  • Global state is shared across multiple components and managed using tools like the Context API, Redux, Recoil, or Zustand.
  • Keep data consistent across components
  •  Reduce prop drilling and unnecessary re-renders
  • Improve performance and maintainability
  • Use useState for simple state logic (e.g., toggles, form inputs).
  • Use useReducer when dealing with complex state transitions (e.g., form management, state dependent on previous values).

Prop drilling occurs when a prop is passed through multiple components just to reach a deeply nested child.

Solutions:

  •  Use Context API to share state globally
  •  Use state management libraries like Redux or Recoil

The Context API provides a way to pass data through the component tree without prop drilling. It is useful for theme settings, authentication, and global state sharing.

  •  Context API causes unnecessary re-renders if not optimized
  •  Difficult to debug in large applications
  •  Not ideal for frequent state updates (use Redux or Recoil instead)

Redux is a predictable state container for managing global state in React apps. It is useful for complex applications where multiple components need shared state updates.

  • Redux Toolkit (RTK) is a modern approach that simplifies Redux setup by providing utilities like createSlice and configureStore.
  • Classic Redux requires more boilerplate code for actions, reducers, and store configuration.

Recoil is a lightweight state management library that allows components to subscribe to atoms (state units). Unlike Redux, Recoil does not require a central store and allows direct subscriptions to individual state pieces.

  •  Small to medium apps
  •  Avoiding Redux boilerplate
  •  Managing global state with minimal effort

Jotai is an atomic state management library similar to Recoil. It allows components to subscribe to small state units (atoms) and updates only the necessary parts of the UI.

  • React Query for caching and background data fetching
  • SWR for fetching API data with automatic revalidation
  • Redux Toolkit Query (RTK Query) for managing server state efficiently
  •  Use React.memo to prevent re-renders when props don’t change
  •  Use useCallback to memoize functions
  •  Use useMemo to optimize expensive calculations
  • Use Selectors in Redux/Context API to extract only necessary state
  • useState & useReducer – Best for local component state
  • Context API – Good for small-scale global state (e.g., authentication, themes
  •  Redux / Redux Toolkit – Best for large applications with complex state logic
  • Recoil / Zustand / Jotai – Lightweight alternatives to Redux for global state
  • React Query / SWR / RTK Query – Best for managing async API state

Enroll For Free Demo