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 Performance

⚑ State Performance Optimization β€” Avoiding Re-renders and Memoization Strategies

State changes trigger re-renders. Poor state management causes cascading re-renders, tanking performance. Master optimization techniques to keep applications fast.


🎯 Understanding Re-render Causes

Every state change triggers a re-render. But unnecessary re-renders happen when:

1. A parent re-renders, triggering all children

2. Context value changes, causing all consumers to re-render

3. Props change even if not used in the component

4. Object/array references change, breaking memoization

import { useState, memo, useMemo } from 'react';

// ❌ BAD: Unnecessary re-renders cascade
function BadParent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <input value={name} onChange={(e) => setName(e.target.value)} />

      {/* Both children re-render when parent re-renders, even if their props didn't change */}
      <ExpensiveChildA count={count} />
      <ExpensiveChildB name={name} />
    </div>
  );
}

function ExpensiveChildA({ count }: { count: number }) {
  // Very expensive computation
  let sum = 0;
  for (let i = 0; i < 100000000; i++) {
    sum += i;
  }
  return <div>Count: {count}, Sum: {sum}</div>;
}

function ExpensiveChildB({ name }: { name: string }) {
  let sum = 0;
  for (let i = 0; i < 100000000; i++) {
    sum += i;
  }
  return <div>Name: {name}, Sum: {sum}</div>;
}

// βœ… GOOD: Memoize children to prevent unnecessary re-renders
const MemoizedChildA = memo(ExpensiveChildA);
const MemoizedChildB = memo(ExpensiveChildB);

function GoodParent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <input value={name} onChange={(e) => setName(e.target.value)} />

      {/* Only re-render when their specific props change */}
      <MemoizedChildA count={count} />
      <MemoizedChildB name={name} />
    </div>
  );
}

<details>

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

// Example: Identifying expensive components with Profiler

import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRenderCallback: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) => {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
};

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Parent />
    </Profiler>
  );
}

// Example: Memoizing with dependencies

interface DataListProps {
  items: string[];
  onSelect: (item: string) => void;
}

function DataList({ items, onSelect }: DataListProps) {
  const sorted = useMemo(() => {
    console.log('Sorting items...');
    return [...items].sort();
  }, [items]); // Only sort when items change

  return (
    <ul>
      {sorted.map((item) => (
        <li key={item}>
          <button onClick={() => onSelect(item)}>{item}</button>
        </li>
      ))}
    </ul>
  );
}

</details>

πŸ’‘ State Splitting for Performance

Split state into multiple useState calls so unrelated changes don't trigger unnecessary re-renders. This is especially important in context providers.

Related state updates together. Unrelated state in separate calls. This enables fine-grained subscriptions.

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

// ❌ BAD: Single state object
function BadStateComponent() {
  const [state, setState] = useState({
    user: null,
    theme: 'light',
    notifications: [],
    sidebarOpen: false,
  });

  const updateTheme = () => {
    setState((prev) => ({ ...prev, theme: prev.theme === 'light' ? 'dark' : 'light' }));
  };

  const toggleSidebar = () => {
    setState((prev) => ({ ...prev, sidebarOpen: !prev.sidebarOpen }));
  };

  // Changing theme also re-renders SidebarComponent even though it doesn't use theme
  return (
    <div>
      <button onClick={updateTheme}>Toggle Theme</button>
      <SidebarComponent isOpen={state.sidebarOpen} toggle={toggleSidebar} />
    </div>
  );
}

// βœ… GOOD: Split state by concern
function GoodStateComponent() {
  const [theme, setTheme] = useState('light');
  const [sidebarOpen, setSidebarOpen] = useState(false);

  const updateTheme = useCallback(
    () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light')),
    []
  );

  const toggleSidebar = useCallback(() => setSidebarOpen((prev) => !prev), []);

  // SidebarComponent only re-renders when sidebarOpen changes
  return (
    <div>
      <button onClick={updateTheme}>Toggle Theme</button>
      <MemoizedSidebar isOpen={sidebarOpen} toggle={toggleSidebar} />
    </div>
  );
}

const MemoizedSidebar = memo(function SidebarComponent({
  isOpen,
  toggle,
}: {
  isOpen: boolean;
  toggle: () => void;
}) {
  return (
    <aside>
      <button onClick={toggle}>{isOpen ? 'Close' : 'Open'}</button>
      {isOpen && <nav>Navigation menu</nav>}
    </aside>
  );
});

<details>

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

// Example: State splitting in list rendering

interface TodoItem {
  id: string;
  text: string;
  completed: boolean;
}

// ❌ BAD: Updating one todo re-renders entire list
function BadTodoList() {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
  const [editingId, setEditingId] = useState<string | null>(null);

  return (
    <div>
      <select value={filter} onChange={(e) => setFilter(e.target.value as any)}>
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="completed">Completed</option>
      </select>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          isEditing={editingId === todo.id}
          onToggleEdit={(id) => setEditingId(id === editingId ? null : id)}
        />
      ))}
    </div>
  );
}

// βœ… GOOD: Separate state by concern
function GoodTodoList() {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
  const [editingIds, setEditingIds] = useState<Set<string>>(new Set());

  const filteredTodos = todos.filter((todo) => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  return (
    <div>
      <select value={filter} onChange={(e) => setFilter(e.target.value as any)}>
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="completed">Completed</option>
      </select>
      {filteredTodos.map((todo) => (
        <MemoTodoItem
          key={todo.id}
          todo={todo}
          isEditing={editingIds.has(todo.id)}
          onToggleEdit={(id) => {
            const newIds = new Set(editingIds);
            if (newIds.has(id)) {
              newIds.delete(id);
            } else {
              newIds.add(id);
            }
            setEditingIds(newIds);
          }}
        />
      ))}
    </div>
  );
}

const MemoTodoItem = memo(function TodoItem({
  todo,
  isEditing,
  onToggleEdit,
}: any) {
  return <li>{todo.text}</li>;
});

</details>

πŸ”§ useCallback and useReducer for Optimization

useCallback prevents new function references from breaking memoization. useReducer can optimize state updates with many dependencies.

Callbacks and state updaters should be stable references. useCallback memoizes functions.

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

// ❌ BAD: New callback on every render breaks memo
function BadParent() {
  const [count, setCount] = useState(0);

  // New function every render!
  const handleIncrement = () => {
    setCount((prev) => prev + 1);
  };

  return <MemoChild onIncrement={handleIncrement} />;
}

const MemoChild = memo(function Child({
  onIncrement,
}: {
  onIncrement: () => void;
}) {
  // Will re-render every time because onIncrement reference changed
  console.log('Child rendered');
  return <button onClick={onIncrement}>Increment</button>;
});

// βœ… GOOD: useCallback keeps function reference stable
function GoodParent() {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []); // Empty deps = same reference always

  return <MemoChild onIncrement={handleIncrement} />;
}

// βœ… ALSO GOOD: useReducer for multiple related actions
interface State {
  count: number;
  step: number;
  history: number[];
}

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET_STEP'; step: number }
  | { type: 'RESET' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + state.step,
        history: [...state.history, state.count + state.step],
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - state.step,
        history: [...state.history, state.count - state.step],
      };
    case 'SET_STEP':
      return { ...state, step: action.step };
    case 'RESET':
      return { count: 0, step: 1, history: [] };
    default:
      return state;
  }
}

function OptimizedCounter() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0,
    step: 1,
    history: [],
  });

  // Dispatch function never changes
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <input
        type="number"
        value={state.step}
        onChange={(e) =>
          dispatch({ type: 'SET_STEP', step: parseInt(e.target.value) })
        }
      />
      <p>History: {state.history.join(', ')}</p>
    </div>
  );
}

πŸ”§ Context Optimization

Context causes all consumers to re-render when value changes. Optimize by splitting contexts and memoizing values.

Split contexts by update frequency. Memoize context values to prevent unnecessary re-renders.

import { createContext, useContext, useState, useMemo, useCallback } from 'react';

// ❌ BAD: Single context with all data
const BadAppContext = createContext<{
  user: any;
  setUser: (user: any) => void;
  notifications: any[];
  addNotification: (msg: string) => void;
  theme: string;
  toggleTheme: () => void;
} | null>(null);

// βœ… GOOD: Split contexts by change frequency
const UserContext = createContext<{
  user: any;
  setUser: (user: any) => void;
} | null>(null);

const NotificationContext = createContext<{
  notifications: any[];
  addNotification: (msg: string) => void;
} | null>(null);

const ThemeContext = createContext<{
  theme: string;
  toggleTheme: () => void;
} | null>(null);

function AppProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState<any[]>([]);
  const [theme, setTheme] = useState('light');

  // Memoize context values to prevent unnecessary re-renders
  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const notificationValue = useMemo(
    () => ({
      notifications,
      addNotification: (msg: string) => {
        setNotifications((prev) => [...prev, msg]);
      },
    }),
    [notifications]
  );
  const themeValue = useMemo(
    () => ({
      theme,
      toggleTheme: () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light')),
    }),
    [theme]
  );

  return (
    <UserContext.Provider value={userValue}>
      <NotificationContext.Provider value={notificationValue}>
        <ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
      </NotificationContext.Provider>
    </UserContext.Provider>
  );
}

// Only re-render when user changes
function useUser() {
  return useContext(UserContext)!;
}

// Only re-render when notifications change
function useNotifications() {
  return useContext(NotificationContext)!;
}

// Only re-render when theme changes
function useTheme() {
  return useContext(ThemeContext)!;
}

πŸ“Š Performance Optimization Techniques Comparison

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Technique      β”‚ Impact           β”‚ Complexity     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ memo()         β”‚ Prevents renders β”‚ Low            β”‚
β”‚ State splittingβ”‚ Focused updates  β”‚ Low-Medium     β”‚
β”‚ useCallback    β”‚ Stable refs      β”‚ Low            β”‚
β”‚ useMemo        β”‚ Compute once     β”‚ Low-Medium     β”‚
β”‚ useReducer     β”‚ Complex logic    β”‚ Medium         β”‚
β”‚ Context split  β”‚ Granular updates β”‚ Medium         β”‚
β”‚ Profiler       β”‚ Identify issues  β”‚ Low            β”‚
β”‚ Code splitting β”‚ Smaller bundles  β”‚ Medium-High    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”‘ Key Takeaways

  • βœ… Every state change triggers a re-render of that component and children
  • βœ… Use memo() to prevent child re-renders when props don't change
  • βœ… Split state into separate useState calls for unrelated data
  • βœ… Split contexts by update frequency (theme, user, notifications)
  • βœ… Memoize context values with useMemo
  • βœ… Use useCallback to keep function references stable
  • βœ… useReducer can optimize complex state with many interdependencies
  • βœ… Profile with React DevTools Profiler to identify bottlenecks
  • βœ… Only memoize when you have actual performance issues
  • βœ… Measure before and after to verify optimizations work

Ready to practice? Challenges | Next: Custom Hooks for State


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