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/Introduction To State

🏗️ Advanced State Patterns and Architecture — Building Scalable Applications

As applications grow, naive state management creates performance problems and architectural complexity. Advanced patterns solve these challenges through careful design, memoization, and strategic state placement.


🎯 State Normalization and Denormalization

Deeply nested state structures cause unnecessary re-renders. Normalize state to keep related data flat, then denormalize only when needed for display.

State shape determines re-render behavior. Nested structures mean updating deep properties causes entire parent objects to change, triggering cascading re-renders. Normalization keeps related data together while avoiding nesting.

import { useState, useMemo } from 'react';

// Before: Deeply nested structure
interface BadUserState {
  users: {
    [userId: string]: {
      profile: {
        name: string;
        email: string;
        preferences: {
          theme: 'light' | 'dark';
          notifications: boolean;
        };
      };
      posts: Array<{ id: string; title: string }>;
    };
  };
}

// After: Normalized state
interface GoodUserState {
  users: Record<string, { id: string; name: string; email: string }>;
  userPreferences: Record<string, { theme: 'light' | 'dark'; notifications: boolean }>;
  userPosts: Record<string, string[]>; // userId -> postIds
  posts: Record<string, { id: string; title: string }>;
}

function UserManagement() {
  const [state, setState] = useState<GoodUserState>({
    users: {
      'user-1': { id: 'user-1', name: 'John', email: 'john@example.com' },
    },
    userPreferences: {
      'user-1': { theme: 'dark', notifications: true },
    },
    userPosts: {
      'user-1': ['post-1', 'post-2'],
    },
    posts: {
      'post-1': { id: 'post-1', title: 'First Post' },
      'post-2': { id: 'post-2', title: 'Second Post' },
    },
  });

  // Denormalize on demand for display
  const userWithPosts = useMemo(() => {
    const userId = 'user-1';
    const user = state.users[userId];
    const postIds = state.userPosts[userId] || [];
    return {
      ...user,
      posts: postIds.map(id => state.posts[id]),
    };
  }, [state.users, state.userPosts, state.posts]);

  const updateUserPreference = (userId: string, theme: 'light' | 'dark') => {
    setState(prev => ({
      ...prev,
      userPreferences: {
        ...prev.userPreferences,
        [userId]: {
          ...prev.userPreferences[userId],
          theme,
        },
      },
    }));
  };

  return (
    <div>
      <p>User: {userWithPosts.name}</p>
      <p>Posts: {userWithPosts.posts.length}</p>
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example 2: Selectors for consistent denormalization

interface AppState {
  users: Record<string, { id: string; name: string }>;
  posts: Record<string, { id: string; title: string; authorId: string }>;
  comments: Record<string, { id: string; text: string; postId: string }>;
}

// Selector functions encapsulate denormalization logic
const selectors = {
  getPost: (state: AppState, postId: string) => {
    const post = state.posts[postId];
    return {
      ...post,
      author: state.users[post.authorId],
      comments: Object.values(state.comments).filter(
        comment => comment.postId === postId
      ),
    };
  },

  getUser: (state: AppState, userId: string) => {
    return {
      ...state.users[userId],
      posts: Object.values(state.posts).filter(
        post => post.authorId === userId
      ),
    };
  },
};

function Post({ postId, state }: { postId: string; state: AppState }) {
  const post = selectors.getPost(state, postId);

  return (
    <div>
      <h2>{post.title}</h2>
      <p>By {post.author.name}</p>
      <ul>
        {post.comments.map(comment => (
          <li key={comment.id}>{comment.text}</li>
        ))}
      </ul>
    </div>
  );
}

</details>

💡 Compound State: Managing Related Values

When multiple state values always change together or depend on each other, use compound state with a reducer for clarity and correctness.

Compound state uses `useReducer` instead of multiple `useState` calls. This centralizes state logic, prevents inconsistent states, and makes complex state transitions explicit.

import { useReducer } from 'react';

interface FormState {
  values: { email: string; password: string; rememberMe: boolean };
  touched: { email: boolean; password: boolean };
  errors: { email?: string; password?: string };
  isSubmitting: boolean;
}

type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string | boolean }
  | { type: 'SET_TOUCHED'; field: string }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'CLEAR_ERROR'; field: string }
  | { type: 'START_SUBMIT' }
  | { type: 'END_SUBMIT' }
  | { type: 'RESET' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
      };
    case 'SET_TOUCHED':
      return {
        ...state,
        touched: { ...state.touched, [action.field]: true },
      };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.error },
      };
    case 'CLEAR_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: undefined },
      };
    case 'START_SUBMIT':
      return { ...state, isSubmitting: true };
    case 'END_SUBMIT':
      return { ...state, isSubmitting: false };
    case 'RESET':
      return {
        values: { email: '', password: '', rememberMe: false },
        touched: {},
        errors: {},
        isSubmitting: false,
      };
    default:
      return state;
  }
}

function LoginForm() {
  const [state, dispatch] = useReducer(formReducer, {
    values: { email: '', password: '', rememberMe: false },
    touched: {},
    errors: {},
    isSubmitting: false,
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    dispatch({ type: 'START_SUBMIT' });

    try {
      // Validate
      if (!state.values.email.includes('@')) {
        dispatch({ type: 'SET_ERROR', field: 'email', error: 'Invalid email' });
        return;
      }

      // Submit
      await new Promise(resolve => setTimeout(resolve, 1000));
      dispatch({ type: 'RESET' });
    } finally {
      dispatch({ type: 'END_SUBMIT' });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={state.values.email}
        onChange={(e) =>
          dispatch({ type: 'SET_FIELD', field: 'email', value: e.target.value })
        }
      />
      {state.errors.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
    </form>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example 2: State machine with useReducer

type PaymentState = 'idle' | 'processing' | 'success' | 'error';

interface PaymentReducerState {
  state: PaymentState;
  amount: number;
  error: string | null;
}

type PaymentAction =
  | { type: 'START'; amount: number }
  | { type: 'SUCCESS' }
  | { type: 'ERROR'; message: string }
  | { type: 'RESET' };

const paymentReducer = (
  state: PaymentReducerState,
  action: PaymentAction
): PaymentReducerState => {
  switch (action.type) {
    case 'START':
      return {
        ...state,
        state: 'processing',
        amount: action.amount,
        error: null,
      };
    case 'SUCCESS':
      return { ...state, state: 'success' };
    case 'ERROR':
      return { ...state, state: 'error', error: action.message };
    case 'RESET':
      return {
        state: 'idle',
        amount: 0,
        error: null,
      };
  }
};

function PaymentProcessor() {
  const [payment, dispatch] = useReducer(paymentReducer, {
    state: 'idle',
    amount: 0,
    error: null,
  });

  const processPayment = async (amount: number) => {
    dispatch({ type: 'START', amount });
    try {
      await new Promise(resolve => setTimeout(resolve, 2000));
      dispatch({ type: 'SUCCESS' });
    } catch (error) {
      dispatch({
        type: 'ERROR',
        message: error instanceof Error ? error.message : 'Payment failed',
      });
    }
  };

  return (
    <div>
      {payment.state === 'idle' && (
        <button onClick={() => processPayment(99.99)}>Pay $99.99</button>
      )}
      {payment.state === 'processing' && <p>Processing...</p>}
      {payment.state === 'success' && <p>Payment successful!</p>}
      {payment.state === 'error' && <p>Error: {payment.error}</p>}
    </div>
  );
}

</details>

🔧 Atomic State Patterns

Divide state into small, independent atoms. Each piece changes for one reason and doesn't depend on others, enabling fine-grained updates.

Atomic state prevents the "tearing" problem where different components see inconsistent state during render. Each atom updates independently, components subscribe to relevant atoms only.

import { useState, useCallback } from 'react';

// Atomic state pattern
function useAtomicState<T>(initialValue: T) {
  const [value, setValue] = useState(initialValue);
  return [value, setValue] as const;
}

function App() {
  // Each piece of state is atomic
  const [userId, setUserId] = useAtomicState<string | null>(null);
  const [posts, setPosts] = useAtomicState<Array<{ id: string; title: string }>>([]);
  const [loading, setLoading] = useAtomicState(false);
  const [error, setError] = useAtomicState<string | null>(null);

  // Queries select only what they need
  const selectedUserId = userId;
  const postCount = posts.length;
  const isLoading = loading;

  const fetchPosts = useCallback(
    async (id: string) => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(`/api/users/${id}/posts`);
        setPosts(await response.json());
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Error');
      } finally {
        setLoading(false);
      }
    },
    [setLoading, setError, setPosts]
  );

  return (
    <div>
      <p>User: {selectedUserId}</p>
      <p>Posts: {postCount}</p>
      {isLoading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
    </div>
  );
}

📊 State Architecture Comparison

┌─────────────────────┬──────────────────┬─────────────────┐
│ Pattern             │ Best For         │ Trade-offs      │
├─────────────────────┼──────────────────┼─────────────────┤
│ Atomic              │ Simple apps      │ Many useState   │
│ Compound            │ Related data     │ Complex reducer │
│ Normalized          │ Complex data     │ Denormalization │
│ State machine       │ Complex flows    │ Verbose actions │
└─────────────────────┴──────────────────┴─────────────────┘

🔑 Key Takeaways

  • ✅ Normalize deeply nested state to prevent cascading re-renders
  • ✅ Use compound state (useReducer) for related, interdependent values
  • ✅ Denormalize on-demand with useMemo for display
  • ✅ Atomic state enables fine-grained subscriptions and updates
  • ✅ Selectors encapsulate denormalization and data retrieval logic
  • ✅ State shape directly impacts performance and scalability
  • ✅ Choose patterns based on data complexity and update patterns

Ready to practice? Challenges | Next: useState Hooks Deep Dive


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