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/State Best Practices

🏛️ State Architecture Patterns — Designing Scalable State and Custom Hooks

Enterprise applications require careful state architecture. Strategic use of custom hooks, composition, and clear separation of concerns ensures code is maintainable, testable, and scalable.


🎯 Custom Hooks for State Logic

Extract state logic into custom hooks. This reuses logic across components and makes state independent of UI implementation.

Custom hooks are functions that use React hooks. They encapsulate state logic, making it composable and testable independently of components.

import { useState, useCallback } from 'react';

// Custom hook: Single responsibility - counter logic
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 };
}

// Custom hook: Form state
function useForm<T extends Record<string, any>>(initialValues: T) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState<Partial<T>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      const { name, value, type } = e.target;
      const fieldValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;

      setValues((prev) => ({ ...prev, [name]: fieldValue }));
      setTouched((prev) => ({ ...prev, [name]: true }));
    },
    []
  );

  const setFieldValue = useCallback((name: string, value: any) => {
    setValues((prev) => ({ ...prev, [name]: value }));
  }, []);

  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    handleChange,
    setFieldValue,
    resetForm,
    setErrors,
  };
}

// Use custom hooks in components
function CounterComponent() {
  const counter = useCounter(10);

  return (
    <div>
      <p>Count: {counter.count}</p>
      <button onClick={counter.increment}>+</button>
      <button onClick={counter.decrement}>-</button>
      <button onClick={counter.reset}>Reset</button>
    </div>
  );
}

function FormComponent() {
  const form = useForm({ email: '', password: '' });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Submitted:', form.values);
    form.resetForm();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={form.values.email}
        onChange={form.handleChange}
        onBlur={() => {}}
      />
      <input
        name="password"
        type="password"
        value={form.values.password}
        onChange={form.handleChange}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Advanced custom hook - async data fetching

interface UseFetchOptions {
  onSuccess?: (data: any) => void;
  onError?: (error: Error) => void;
  cacheTime?: number;
}

function useFetch<T>(url: string, options: UseFetchOptions = {}) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);

      const result = await response.json();
      setData(result);
      options.onSuccess?.(result);
    } catch (err) {
      const error = err instanceof Error ? err : new Error(String(err));
      setError(error);
      options.onError?.(error);
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  return { data, loading, error, refetch: fetchData };
}

// Example: Composing multiple custom hooks

function useUser(userId: string) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  return { user, loading, error };
}

function useUserPosts(userId: string) {
  const { data: posts, loading, error } = useFetch(`/api/users/${userId}/posts`);
  return { posts, loading, error };
}

function UserProfile({ userId }: { userId: string }) {
  const { user, loading: userLoading } = useUser(userId);
  const { posts, loading: postsLoading } = useUserPosts(userId);

  return (
    <div>
      {userLoading && <p>Loading user...</p>}
      {user && <h1>{user.name}</h1>}
      {postsLoading && <p>Loading posts...</p>}
      {posts && <p>Posts: {posts.length}</p>}
    </div>
  );
}

</details>

💡 useReducer vs useState Trade-offs

`useReducer` is better for complex state with many updates, while `useState` suits simple state. Understand the trade-offs to choose correctly.

useState is simple but can lead to complex update logic. useReducer centralizes logic but requires more boilerplate.

import { useState, useReducer } from 'react';

// SIMPLE STATE: useState is better
function TodoListWithState() {
  const [todos, setTodos] = useState<Array<{ id: string; text: string; done: boolean }>>([]);

  const addTodo = (text: string) => {
    setTodos([...todos, { id: Date.now().toString(), text, done: false }]);
  };

  const toggleTodo = (id: string) => {
    setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
  };

  return (
    <div>
      <button onClick={() => addTodo('New task')}>Add</button>
      {todos.map((todo) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          {todo.text}
        </div>
      ))}
    </div>
  );
}

// COMPLEX STATE: useReducer is better
interface TodoState {
  todos: Array<{ id: string; text: string; done: boolean }>;
  filter: 'all' | 'done' | 'pending';
  sortBy: 'date' | 'alphabetical';
  lastUpdated: Date;
  loading: boolean;
  error: string | null;
}

type TodoAction =
  | { type: 'ADD_TODO'; text: string }
  | { type: 'TOGGLE_TODO'; id: string }
  | { type: 'REMOVE_TODO'; id: string }
  | { type: 'SET_FILTER'; filter: 'all' | 'done' | 'pending' }
  | { type: 'SET_SORT'; sortBy: 'date' | 'alphabetical' }
  | { type: 'START_LOADING' }
  | { type: 'SET_ERROR'; error: string }
  | { type: 'CLEAR_ERROR' };

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now().toString(), text: action.text, done: false }],
        lastUpdated: new Date(),
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((t) =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
        lastUpdated: new Date(),
      };
    case 'SET_FILTER':
      return { ...state, filter: action.filter };
    case 'START_LOADING':
      return { ...state, loading: true, error: null };
    case 'SET_ERROR':
      return { ...state, error: action.error, loading: false };
    case 'CLEAR_ERROR':
      return { ...state, error: null };
    default:
      return state;
  }
}

function TodoListWithReducer() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all',
    sortBy: 'date',
    lastUpdated: new Date(),
    loading: false,
    error: null,
  });

  const filteredTodos = state.todos.filter((t) => {
    if (state.filter === 'done') return t.done;
    if (state.filter === 'pending') return !t.done;
    return true;
  });

  return (
    <div>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      <button onClick={() => dispatch({ type: 'ADD_TODO', text: 'New task' })}>
        Add
      </button>
      <select
        value={state.filter}
        onChange={(e) =>
          dispatch({ type: 'SET_FILTER', filter: e.target.value as any })
        }
      >
        <option value="all">All</option>
        <option value="done">Done</option>
        <option value="pending">Pending</option>
      </select>
      {filteredTodos.map((todo) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
          />
          {todo.text}
        </div>
      ))}
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Combining useState and useReducer

function useOptimisticUpdate<T>(initialValue: T) {
  const [value, setValue] = useState(initialValue);
  const [isPending, setIsPending] = useState(false);

  const updateOptimistically = async (newValue: T, serverUpdate: () => Promise<void>) => {
    // Optimistic update
    setValue(newValue);
    setIsPending(true);

    try {
      await serverUpdate();
    } catch (error) {
      // Rollback on error
      setValue(initialValue);
      throw error;
    } finally {
      setIsPending(false);
    }
  };

  return { value, isPending, updateOptimistically };
}

function ArticleEditor() {
  const { value: title, isPending, updateOptimistically } = useOptimisticUpdate(
    'Initial Title'
  );

  const handleSave = (newTitle: string) => {
    updateOptimistically(newTitle, async () => {
      const response = await fetch('/api/article', {
        method: 'PUT',
        body: JSON.stringify({ title: newTitle }),
      });
      if (!response.ok) throw new Error('Save failed');
    });
  };

  return (
    <div>
      <input value={title} onChange={(e) => handleSave(e.target.value)} />
      {isPending && <p>Saving...</p>}
    </div>
  );
}

</details>

🔧 State Composition Patterns

Build complex state from simpler custom hooks. This modular approach makes state logic reusable and testable.

Compose state hooks to create higher-level abstractions. Each custom hook handles one concern, then combine them.

import { useState, useCallback } from 'react';

// Atomic hooks for single concerns
function useAsync<T>(asyncFunction: () => Promise<T>, immediate = true) {
  const [state, setState] = useState<{
    loading: boolean;
    data: T | null;
    error: Error | null;
  }>({ loading: immediate, data: null, error: null });

  const execute = useCallback(async () => {
    setState({ loading: true, data: null, error: null });
    try {
      const response = await asyncFunction();
      setState({ loading: false, data: response, error: null });
    } catch (error) {
      setState({
        loading: false,
        data: null,
        error: error instanceof Error ? error : new Error(String(error)),
      });
    }
  }, [asyncFunction]);

  if (immediate) {
    execute();
  }

  return { ...state, execute };
}

// Compose hooks
function useUserWithPosts(userId: string) {
  const user = useAsync(() => fetch(`/api/users/${userId}`).then((r) => r.json()), true);
  const posts = useAsync(
    () => fetch(`/api/users/${userId}/posts`).then((r) => r.json()),
    true
  );

  const isLoading = user.loading || posts.loading;
  const error = user.error || posts.error;

  return {
    user: user.data,
    posts: posts.data,
    isLoading,
    error,
    refetchUser: user.execute,
    refetchPosts: posts.execute,
  };
}

function UserProfile({ userId }: { userId: string }) {
  const { user, posts, isLoading, error } = useUserWithPosts(userId);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>Posts: {posts?.length}</p>
    </div>
  );
}

📊 State Management Pattern Comparison

┌────────────────────┬─────────────────┬──────────────────┐
│ Pattern            │ Complexity      │ Use Case         │
├────────────────────┼─────────────────┼──────────────────┤
│ useState           │ Simple          │ Single values    │
│ useReducer        │ Medium-High     │ Complex state    │
│ Custom Hook       │ Medium          │ Reusable logic   │
│ Composition       │ Medium-High     │ Combined hooks   │
│ Context API       │ High            │ Global state     │
│ State library     │ Very High       │ Enterprise       │
└────────────────────┴─────────────────┴──────────────────┘

🔑 Key Takeaways

  • ✅ Extract state logic into custom hooks for reusability
  • ✅ Custom hooks are testable independently of components
  • ✅ Use useState for simple, independent state
  • ✅ Use useReducer for complex state with many updates
  • ✅ Compose custom hooks to build complex state from simple pieces
  • ✅ Custom hooks enable clear separation of concerns
  • ✅ Keep custom hooks focused on single responsibilities
  • ✅ Use callbacks to memoize hook return values
  • ✅ Export hook state in consistent, predictable shapes
  • ✅ Document custom hook contracts clearly

Ready to practice? Challenges | Next: Form State at Scale


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