React Mastery / Advanced Concepts

Context+Reducer - Handling Async Operations

🧠 Why Async Handling Needs Special Care

Reducers must be pure:

  • ❌ No API calls

  • ❌ No async/await

  • ❌ No side effects

So the rule is simple:

Async logic lives OUTSIDE the reducer

Reducer only updates state


🧩 Common Async Use Cases

  • Login / Logout

  • Fetch user profile

  • Load dashboard data

  • Submit forms

  • API-based state updates


πŸͺœ Step 1: Extend State for Async Handling

πŸ” Why?

Async operations have three phases:

  1. Loading

  2. Success

  3. Error

So our state must track them.

πŸ“ authReducer.js

export const initialAuthState = {
  user: null,
  isAuthenticated: false,
  loading: false,
  error: null,
};

πŸͺœ Step 2: Add Async Action Types

πŸ” Why?

Each async operation needs separate actions to represent its lifecycle.

πŸ“ authReducer.js

export function authReducer(state, action) {
  switch (action.type) {
    case 'LOGIN_START':
      return {
        ...state,
        loading: true,
        error: null,
      };

    case 'LOGIN_SUCCESS':
      return {
        user: action.payload,
        isAuthenticated: true,
        loading: false,
        error: null,
      };

    case 'LOGIN_ERROR':
      return {
        ...state,
        loading: false,
        error: action.payload,
      };

    case 'LOGOUT':
      return initialAuthState;

    default:
      return state;
  }
}

πŸ“Œ Reducer stays:

  • Synchronous

  • Predictable

  • Easy to test


πŸͺœ Step 3: Handle Async Logic in the Provider

πŸ” Why here?

The Context Provider is the best place to:

  • Perform API calls

  • Dispatch actions before & after the call

  • Expose helper functions like login()

This keeps components clean and dumb.

πŸ“ AuthProvider.jsx

import { useReducer } from 'react';
import { AuthContext } from './AuthContext';
import { authReducer, initialAuthState } from './authReducer';

export default function AuthProvider({ children }) {
  const [state, dispatch] = useReducer(authReducer, initialAuthState);

  // βœ… Async function lives here
  const login = async (credentials) => {
    dispatch({ type: 'LOGIN_START' });

    try {
      // Fake API call
      await new Promise((resolve) => setTimeout(resolve, 1000));

      const user = { name: 'Bhaskar', email: 'test@mail.com' };

      dispatch({
        type: 'LOGIN_SUCCESS',
        payload: user,
      });
    } catch (error) {
      dispatch({
        type: 'LOGIN_ERROR',
        payload: 'Login failed',
      });
    }
  };

  const logout = () => {
    dispatch({ type: 'LOGOUT' });
  };

  return (
    <AuthContext.Provider
      value={{ state, login, logout }}
    >
      {children}
    </AuthContext.Provider>
  );
}

πŸ“Œ Key idea:

  • Components don’t dispatch async actions

  • They just call login() or logout()


πŸͺœ Step 4: Consume Async State in Components

πŸ” Why?

UI should:

  • Show loader

  • Show error

  • React to success

πŸ“ Dashboard.jsx

import { useContext } from 'react';
import { AuthContext } from './AuthContext';

export default function Dashboard() {
  const { state, login, logout } = useContext(AuthContext);

  if (state.loading) {
    return <p>Logging in...</p>;
  }

  if (state.error) {
    return <p style={{ color: 'red' }}>{state.error}</p>;
  }

  return (
    <>
      {state.isAuthenticated ? (
        <>
          <h2>Welcome, {state.user.name}</h2>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <button onClick={() => login()}>
          Login
        </button>
      )}
    </>
  );
}

🧠 Mental Model for Async Flow


πŸ§ͺ Best Practices for Async Handling

βœ… Keep API logic outside reducer

βœ… Dispatch START / SUCCESS / ERROR

βœ… Expose async functions from provider

βœ… Show loading & error in UI

❌ Don’t call APIs inside reducer

❌ Don’t put async logic in components