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/Multiple State Variables

πŸ—οΈ Complex State Structures β€” Managing Interdependent State and Data

As applications scale, managing multiple interdependent state variables becomes complex. Strategic state structure prevents bugs, improves performance, and makes code maintainable.


🎯 Interdependent State and Consistency

When multiple state values must stay in sync, naive approaches create inconsistent states. Use compound state or careful structure to ensure atomicity.

Related state should update together. If changing one value requires updating others, they should be tied together to prevent intermediate invalid states.

import { useState } from 'react';

// ❌ BAD: Independent state variables that should be linked
function BadOrderForm() {
  const [items, setItems] = useState<string[]>([]);
  const [total, setTotal] = useState(0);
  const [itemCount, setItemCount] = useState(0);

  const addItem = (item: string, price: number) => {
    // Inconsistency window: items updated, but total and count aren't yet
    setItems([...items, item]);
    setTotal(total + price); // What if this fails?
    setItemCount(itemCount + 1); // What if this fails?
  };
}

// βœ… GOOD: Compound state structure
function GoodOrderForm() {
  interface Order {
    items: string[];
    prices: number[];
    total: number;
  }

  const [order, setOrder] = useState<Order>({
    items: [],
    prices: [],
    total: 0,
  });

  const addItem = (item: string, price: number) => {
    // Atomic update - all or nothing
    setOrder((prev) => {
      const newItems = [...prev.items, item];
      const newPrices = [...prev.prices, price];
      const newTotal = prev.total + price;

      return {
        items: newItems,
        prices: newPrices,
        total: newTotal,
      };
    });
  };

  return (
    <div>
      <p>Items: {order.items.length}</p>
      <p>Total: ${order.total}</p>
      <button onClick={() => addItem('Apple', 1.99)}>Add Apple</button>
    </div>
  );
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Complex form state with interdependent fields

interface FormState {
  email: string;
  password: string;
  confirmPassword: string;
  isValid: boolean;
  errors: Record<string, string>;
  touched: Record<string, boolean>;
  isDirty: boolean;
}

function ComplexForm() {
  const [form, setForm] = useState<FormState>({
    email: '',
    password: '',
    confirmPassword: '',
    isValid: false,
    errors: {},
    touched: {},
    isDirty: false,
  });

  const updateField = (field: string, value: string) => {
    setForm((prev) => {
      const newForm = { ...prev };
      newForm[field as keyof FormState] = value as any;
      newForm.touched = { ...prev.touched, [field]: true };
      newForm.isDirty = true;

      // Validate interdependent fields
      const newErrors = { ...prev.errors };
      if (field === 'password' || field === 'confirmPassword') {
        if (newForm.password !== newForm.confirmPassword) {
          newErrors.confirmPassword = 'Passwords must match';
        } else {
          delete newErrors.confirmPassword;
        }
      }

      newForm.errors = newErrors;
      newForm.isValid = Object.keys(newErrors).length === 0 && newForm.isDirty;

      return newForm;
    });
  };

  return (
    <form>
      <input
        value={form.email}
        onChange={(e) => updateField('email', e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={form.password}
        onChange={(e) => updateField('password', e.target.value)}
        placeholder="Password"
      />
      <input
        type="password"
        value={form.confirmPassword}
        onChange={(e) => updateField('confirmPassword', e.target.value)}
        placeholder="Confirm Password"
      />
      {form.errors.confirmPassword && (
        <p style={{ color: 'red' }}>{form.errors.confirmPassword}</p>
      )}
      <button disabled={!form.isValid} type="submit">
        Submit
      </button>
    </form>
  );
}

</details>

πŸ’‘ Normalized vs Denormalized State

Normalized state is flat, preventing deep nesting. Denormalized state includes duplicated data for easier access. Choose based on your data relationships.

Normalized state reduces re-renders and prevents inconsistency. Denormalized state simplifies access patterns. Most applications use a mix.

import { useState, useMemo } from 'react';

// Deeply nested (problematic)
interface NestedUsers {
  user1: {
    profile: {
      name: string;
      settings: { theme: 'light' | 'dark' };
    };
    posts: Array<{ id: string; title: string }>;
  };
}

// Normalized structure
interface NormalizedState {
  users: Record<string, { id: string; name: string }>;
  userSettings: Record<string, { theme: 'light' | 'dark' }>;
  posts: Record<string, { id: string; title: string; userId: string }>;
  userPostIds: Record<string, string[]>;
}

function UserManager() {
  const [state, setState] = useState<NormalizedState>({
    users: {
      '1': { id: '1', name: 'Alice' },
    },
    userSettings: {
      '1': { theme: 'dark' },
    },
    posts: {
      'p1': { id: 'p1', title: 'Post 1', userId: '1' },
    },
    userPostIds: {
      '1': ['p1'],
    },
  });

  // Denormalize on demand for display
  const userProfile = useMemo(() => {
    const userId = '1';
    const user = state.users[userId];
    const settings = state.userSettings[userId];
    const postIds = state.userPostIds[userId] || [];

    return {
      ...user,
      settings,
      posts: postIds.map((id) => state.posts[id]),
    };
  }, [state]);

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

  return (
    <div>
      <p>User: {userProfile.name}</p>
      <p>Theme: {userProfile.settings.theme}</p>
      <p>Posts: {userProfile.posts.length}</p>
      <button onClick={() => updateUserTheme('1', 'light')}>
        Switch Theme
      </button>
    </div>
  );
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Selector pattern for consistent denormalization

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

// Selectors encapsulate denormalization logic
const selectors = {
  getUserWithPosts: (state: AppState, userId: string) => {
    const user = state.users[userId];
    const userPosts = Object.values(state.posts).filter(
      (post) => post.authorId === userId
    );
    return { ...user, posts: userPosts };
  },

  getPostWithComments: (state: AppState, postId: string) => {
    const post = state.posts[postId];
    const author = state.users[post.authorId];
    const postComments = Object.values(state.comments).filter(
      (c) => c.postId === postId
    );
    return {
      ...post,
      author,
      comments: postComments.map((c) => ({
        ...c,
        author: state.users[c.authorId],
      })),
    };
  },
};

function Blog() {
  const [state] = useState<AppState>({
    users: {
      'u1': { id: 'u1', name: 'Alice', email: 'alice@example.com' },
    },
    posts: {
      'p1': { id: 'p1', title: 'Hello', authorId: 'u1', likes: 5 },
    },
    comments: {
      'c1': { id: 'c1', text: 'Nice!', postId: 'p1', authorId: 'u1' },
    },
  });

  const post = selectors.getPostWithComments(state, 'p1');

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

</details>

πŸ”§ Handling Circular References and Graphs

Complex data like graphs or trees with circular references requires special handling to avoid infinite loops and maintain consistency.

Store references by ID, not by nesting objects. Use selector functions to reconstruct relationships when needed.

import { useState, useCallback } from 'react';

// Graph-like data structure
interface Node {
  id: string;
  name: string;
  parentId: string | null;
  childIds: string[];
}

interface TreeState {
  nodes: Record<string, Node>;
  root: string | null;
}

function TreeManager() {
  const [state, setState] = useState<TreeState>({
    nodes: {
      'node-1': {
        id: 'node-1',
        name: 'Root',
        parentId: null,
        childIds: ['node-2', 'node-3'],
      },
      'node-2': {
        id: 'node-2',
        name: 'Child 1',
        parentId: 'node-1',
        childIds: [],
      },
      'node-3': {
        id: 'node-3',
        name: 'Child 2',
        parentId: 'node-1',
        childIds: [],
      },
    },
    root: 'node-1',
  });

  // Safe tree traversal
  const getNodeWithChildren = useCallback(
    (nodeId: string): Node & { children: ReturnType<typeof getNodeWithChildren>[] } => {
      const node = state.nodes[nodeId];
      return {
        ...node,
        children: node.childIds.map((id) => getNodeWithChildren(id)),
      };
    },
    [state.nodes]
  );

  // Add child safely
  const addChild = useCallback(
    (parentId: string, childName: string) => {
      const newNodeId = `node-${Date.now()}`;

      setState((prev) => {
        const newNode: Node = {
          id: newNodeId,
          name: childName,
          parentId,
          childIds: [],
        };

        return {
          ...prev,
          nodes: {
            ...prev.nodes,
            [newNodeId]: newNode,
            [parentId]: {
              ...prev.nodes[parentId],
              childIds: [...prev.nodes[parentId].childIds, newNodeId],
            },
          },
        };
      });
    },
    []
  );

  return (
    <div>
      {state.root && (
        <TreeNode
          node={getNodeWithChildren(state.root)}
          onAddChild={addChild}
        />
      )}
    </div>
  );
}

function TreeNode({
  node,
  onAddChild,
}: {
  node: any;
  onAddChild: (parentId: string, name: string) => void;
}) {
  return (
    <div style={{ marginLeft: 20 }}>
      <p>{node.name}</p>
      <button onClick={() => onAddChild(node.id, 'New Child')}>
        Add Child
      </button>
      {node.children.map((child: any) => (
        <TreeNode key={child.id} node={child} onAddChild={onAddChild} />
      ))}
    </div>
  );
}

πŸ“Š State Structure Patterns Comparison

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Structure        β”‚ Best For        β”‚ Trade-offs        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Compound         β”‚ Related values  β”‚ More boilerplate  β”‚
β”‚ Normalized       β”‚ Complex data    β”‚ Requires selectorsβ”‚
β”‚ Denormalized     β”‚ Fast access     β”‚ Sync issues       β”‚
β”‚ Hybrid           β”‚ Most cases      β”‚ More complex      β”‚
β”‚ Graph/Tree       β”‚ Hierarchical    β”‚ ID references     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”‘ Key Takeaways

  • βœ… Use compound state to keep related values in sync
  • βœ… Normalize deeply nested structures to reduce re-renders
  • βœ… Denormalize on demand with useMemo for display
  • βœ… Use selectors to encapsulate denormalization logic
  • βœ… Store references by ID for graphs and trees, not nesting
  • βœ… Atomic updates prevent intermediate invalid states
  • βœ… Functional updates enable safe state mutations
  • βœ… Choose structure based on access patterns and data relationships
  • βœ… Update interdependent values together
  • βœ… Avoid storing derived or redundant data

Ready to practice? Challenges | Next: State Architecture Patterns


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