Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
💾 Introduction to State⚛️ Using useState Hook🔄 Updating State Correctly🎯 Initial State Values🚫 Common State Mistakes📊 Multiple State Variables🔗 State & Rendering📝 Forms with State🏗️ State Structure Best Practices
Reactjs/State/Interactive Components

🎣 Custom Hooks for State — Building Reusable State Logic

Custom hooks extract state logic into reusable functions. Master advanced patterns to create composable, testable state abstractions that power enterprise applications.


🎯 Advanced Custom Hook Patterns

Beyond simple state wrapping, custom hooks can compose complex logic, handle side effects, and provide clean abstractions.

Custom hooks are functions that use other hooks. They're composable, testable independently, and enable clean separation of concerns.

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

// Advanced pattern: Hook composition and derivation
function usePaginatedData<T>(
  fetchData: (page: number) => Promise<T[]>,
  pageSize: number = 10
) {
  const [data, setData] = useState<T[]>([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [totalItems, setTotalItems] = useState(0);

  const fetch = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await fetchData(currentPage);
      setData(result);
      setTotalItems(result.length);
    } catch (err) {
      setError(err instanceof Error ? err : new Error(String(err)));
    } finally {
      setLoading(false);
    }
  }, [currentPage, fetchData]);

  useEffect(() => {
    fetch();
  }, [fetch]);

  const goToPage = useCallback((page: number) => {
    setCurrentPage(Math.max(1, page));
  }, []);

  const nextPage = useCallback(() => {
    setCurrentPage((prev) => prev + 1);
  }, []);

  const prevPage = useCallback(() => {
    setCurrentPage((prev) => Math.max(1, prev - 1));
  }, []);

  const totalPages = Math.ceil(totalItems / pageSize);

  return {
    data,
    currentPage,
    totalPages,
    pageSize,
    loading,
    error,
    goToPage,
    nextPage,
    prevPage,
    canNextPage: currentPage < totalPages,
    canPrevPage: currentPage > 1,
    refetch: fetch,
  };
}

// Usage: Clean, composable interface
function ProductCatalog() {
  const {
    data: products,
    currentPage,
    totalPages,
    loading,
    nextPage,
    prevPage,
    canNextPage,
  } = usePaginatedData(async (page) => {
    const response = await fetch(`/api/products?page=${page}`);
    return response.json();
  });

  return (
    <div>
      {loading && <p>Loading...</p>}
      <ul>
        {products.map((p: any) => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
      <p>
        Page {currentPage} of {totalPages}
      </p>
      <button onClick={prevPage}>Previous</button>
      <button onClick={nextPage} disabled={!canNextPage}>
        Next
      </button>
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Hook composition for feature-rich functionality

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  const updateValue = useCallback(
    (update: T | ((prev: T) => T)) => {
      setValue((prev) => {
        const newValue = typeof update === 'function' ? (update as (prev: T) => T)(prev) : update;
        localStorage.setItem(key, JSON.stringify(newValue));
        return newValue;
      });
    },
    [key]
  );

  const removeValue = useCallback(() => {
    localStorage.removeItem(key);
    setValue(initialValue);
  }, [key, initialValue]);

  return [value, updateValue, removeValue] as const;
}

function useUndoRedo<T>(initialValue: T) {
  const [value, setValue] = useState(initialValue);
  const [history, setHistory] = useState<T[]>([initialValue]);
  const [historyIndex, setHistoryIndex] = useState(0);

  const push = useCallback((newValue: T) => {
    const newHistory = history.slice(0, historyIndex + 1);
    newHistory.push(newValue);
    setHistory(newHistory);
    setHistoryIndex(newHistory.length - 1);
    setValue(newValue);
  }, [history, historyIndex]);

  const undo = useCallback(() => {
    if (historyIndex > 0) {
      const newIndex = historyIndex - 1;
      setHistoryIndex(newIndex);
      setValue(history[newIndex]);
    }
  }, [history, historyIndex]);

  const redo = useCallback(() => {
    if (historyIndex < history.length - 1) {
      const newIndex = historyIndex + 1;
      setHistoryIndex(newIndex);
      setValue(history[newIndex]);
    }
  }, [history, historyIndex]);

  return { value, push, undo, redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 };
}

// Compose hooks
function useLocalStorageWithHistory<T>(key: string, initialValue: T) {
  const [stored, setStored, removeStored] = useLocalStorage(key, initialValue);
  const { value, push, undo, redo, canUndo, canRedo } = useUndoRedo(stored);

  const update = useCallback(
    (newValue: T) => {
      push(newValue);
      setStored(newValue);
    },
    [push, setStored]
  );

  const reset = useCallback(() => {
    removeStored();
  }, [removeStored]);

  return { value, update, undo, redo, canUndo, canRedo, reset };
}

</details>

💡 useReducer for Complex State Logic

While useState works for simple state, useReducer excels at complex logic with multiple interdependent updates.

useReducer is great when state updates are complex or dependent on previous values. It centralizes logic and makes transitions explicit.

import { useReducer, useCallback } from 'react';

// State machine with useReducer
type LoadingState = 'idle' | 'loading' | 'success' | 'error';

interface DataState {
  status: LoadingState;
  data: any | null;
  error: string | null;
  retryCount: number;
}

type DataAction =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; data: any }
  | { type: 'FETCH_ERROR'; error: string }
  | { type: 'RETRY' }
  | { type: 'RESET' };

const MAX_RETRIES = 3;

function dataReducer(state: DataState, action: DataAction): DataState {
  switch (action.type) {
    case 'FETCH_START':
      return {
        ...state,
        status: 'loading',
        error: null,
      };
    case 'FETCH_SUCCESS':
      return {
        status: 'success',
        data: action.data,
        error: null,
        retryCount: 0,
      };
    case 'FETCH_ERROR':
      return {
        ...state,
        status: state.retryCount < MAX_RETRIES ? 'error' : 'error',
        error: action.error,
      };
    case 'RETRY':
      return {
        ...state,
        status: 'loading',
        retryCount: state.retryCount + 1,
      };
    case 'RESET':
      return {
        status: 'idle',
        data: null,
        error: null,
        retryCount: 0,
      };
    default:
      return state;
  }
}

function useDataFetching(url: string) {
  const [state, dispatch] = useReducer(dataReducer, {
    status: 'idle',
    data: null,
    error: null,
    retryCount: 0,
  });

  const fetch = useCallback(async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', data });
    } catch (error) {
      dispatch({
        type: 'FETCH_ERROR',
        error: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  }, [url]);

  const retry = useCallback(() => {
    dispatch({ type: 'RETRY' });
    fetch();
  }, [fetch]);

  const reset = useCallback(() => {
    dispatch({ type: 'RESET' });
  }, []);

  return {
    ...state,
    fetch,
    retry,
    reset,
    canRetry: state.retryCount < MAX_RETRIES,
  };
}

// Usage
function DataDisplay() {
  const { status, data, error, fetch, retry, canRetry } = useDataFetching('/api/data');

  return (
    <div>
      {status === 'idle' && <button onClick={fetch}>Load Data</button>}
      {status === 'loading' && <p>Loading...</p>}
      {status === 'success' && <p>Data: {JSON.stringify(data)}</p>}
      {status === 'error' && (
        <div>
          <p style={{ color: 'red' }}>Error: {error}</p>
          {canRetry && <button onClick={retry}>Retry</button>}
        </div>
      )}
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Complex form with useReducer

interface FormState {
  fields: Record<string, { value: string; touched: boolean; error?: string }>;
  isSubmitting: boolean;
  submitError?: string;
}

type FormAction =
  | { type: 'FIELD_CHANGE'; field: string; value: string }
  | { type: 'FIELD_BLUR'; field: string }
  | { type: 'SET_FIELD_ERROR'; field: string; error: string }
  | { type: 'START_SUBMIT' }
  | { type: 'SUBMIT_ERROR'; error: string }
  | { type: 'RESET' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'FIELD_CHANGE':
      return {
        ...state,
        fields: {
          ...state.fields,
          [action.field]: {
            ...state.fields[action.field],
            value: action.value,
          },
        },
      };
    case 'FIELD_BLUR':
      return {
        ...state,
        fields: {
          ...state.fields,
          [action.field]: {
            ...state.fields[action.field],
            touched: true,
          },
        },
      };
    case 'SET_FIELD_ERROR':
      return {
        ...state,
        fields: {
          ...state.fields,
          [action.field]: {
            ...state.fields[action.field],
            error: action.error,
          },
        },
      };
    case 'START_SUBMIT':
      return { ...state, isSubmitting: true, submitError: undefined };
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false, submitError: action.error };
    case 'RESET':
      return {
        fields: {},
        isSubmitting: false,
      };
    default:
      return state;
  }
}

function useComplexForm(initialFields: Record<string, string>) {
  const [state, dispatch] = useReducer(formReducer, {
    fields: Object.fromEntries(
      Object.entries(initialFields).map(([key, value]) => [
        key,
        { value, touched: false },
      ])
    ),
    isSubmitting: false,
  });

  const handleFieldChange = useCallback((field: string, value: string) => {
    dispatch({ type: 'FIELD_CHANGE', field, value });
  }, []);

  const handleFieldBlur = useCallback((field: string) => {
    dispatch({ type: 'FIELD_BLUR', field });
  }, []);

  return {
    fields: state.fields,
    isSubmitting: state.isSubmitting,
    submitError: state.submitError,
    handleFieldChange,
    handleFieldBlur,
    dispatch,
  };
}

</details>

🔧 Testing Custom Hooks

Custom hooks are testable independently of components. This enables reliable, fast tests.

Extract hooks to their own functions. Test them with React Testing Library's renderHook.

import { renderHook, act } from '@testing-library/react';
import { useState, useCallback } from 'react';

// Hook to test
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  const decrement = useCallback(() => {
    setCount((prev) => prev - 1);
  }, []);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

// Tests
describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('should increment', () => {
    const { result } = renderHook(() => useCounter());

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

    expect(result.current.count).toBe(1);
  });

  it('should decrement', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  it('should reset to initial value', () => {
    const { result } = renderHook(() => useCounter(10));

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

    expect(result.current.count).toBe(12);

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });

  it('should handle rerender with new initial value', () => {
    const { result, rerender } = renderHook(
      ({ initialValue }) => useCounter(initialValue),
      { initialProps: { initialValue: 10 } }
    );

    expect(result.current.count).toBe(10);

    rerender({ initialValue: 20 });

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(20);
  });
});

📊 Custom Hook Pattern Comparison

┌──────────────────┬──────────────────┬──────────────────┐
│ Pattern          │ Reusability      │ Complexity       │
├──────────────────┼──────────────────┼──────────────────┤
│ Simple useState  │ Low              │ Low              │
│ Custom hook      │ Medium-High      │ Medium           │
│ Composed hooks   │ High             │ Medium-High      │
│ useReducer hook  │ High             │ High             │
│ Custom provider  │ Very High        │ Very High        │
└──────────────────┴──────────────────┴──────────────────┘

🔑 Key Takeaways

  • ✅ Custom hooks extract state logic into reusable functions
  • ✅ Custom hooks are composable: one hook can use another
  • ✅ Return consistent, documented interfaces from hooks
  • ✅ Use useReducer for complex state with many interdependencies
  • ✅ Custom hooks are independently testable with renderHook
  • ✅ Keep hooks focused on single responsibilities
  • ✅ Document hook behavior, parameters, and return values
  • ✅ Consider performance when composing multiple hooks
  • ✅ Handle cleanup with useEffect returns
  • ✅ Think about hook dependencies to avoid stale data
  • ✅ Test hooks before using in components for reliability

Ready to practice? Challenges | Back to State Overview


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn