React Hooks Fundamentals

1. useState Hook and State Management

Pattern Syntax Description Use Case
Basic useState const [state, setState] = useState(initial) Declare state variable with setter Simple state values
Lazy Initialization useState(() => expensive()) Initialize state with function Expensive initial computation
Functional Update setState(prev => prev + 1) Update based on previous state Batched updates, async operations
Multiple States useState() for each value Split unrelated state into separate hooks Independent state management
Object State useState({ key: value }) Store related data together Form data, related properties
Array State useState([]) Manage lists and collections Todo lists, dynamic items

Example: useState patterns and updates

import { useState } from 'react';

// Basic counter
const Counter = () => {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      {/* Functional update - safer for async */}
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
    </div>
  );
};

// Lazy initialization
const ExpensiveComponent = () => {
  const [data, setData] = useState(() => {
    return computeExpensiveValue();
  });
  
  return <div>{data}</div>;
};

// Object state with spreading
const Form = () => {
  const [form, setForm] = useState({ name: '', email: '' });
  
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };
  
  return (
    <form>
      <input 
        value={form.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input 
        value={form.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
    </form>
  );
};

// Array state operations
const TodoList = () => {
  const [todos, setTodos] = useState([]);
  
  const addTodo = (text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  };
  
  const removeTodo = (id) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  };
  
  const updateTodo = (id, newText) => {
    setTodos(prev => prev.map(t => 
      t.id === id ? { ...t, text: newText } : t
    ));
  };
  
  return <ul>{/* render todos */}</ul>;
};
Warning: State updates are asynchronous and batched. Always use functional updates setState(prev => ...) when new state depends on previous state.

2. useEffect Hook and Side Effects

Pattern Syntax Description Use Case
Basic Effect useEffect(() => {}) Runs after every render DOM updates, logging (avoid)
Effect with Dependencies useEffect(() => {}, [deps]) Runs when dependencies change Data fetching, subscriptions
Mount Only Effect useEffect(() => {}, []) Runs once on mount Initial setup, API calls
Cleanup Function useEffect(() => { return () => {} }) Cleanup before unmount/re-run Unsubscribe, clear timers
Multiple Effects useEffect() for each concern Separate unrelated side effects Better code organization
Conditional Effect if (condition) { /* effect */ } Execute effect conditionally inside Conditional subscriptions

Example: useEffect patterns and cleanup

import { useState, useEffect } from 'react';

// Data fetching with cleanup
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    let isMounted = true; // Cleanup flag
    
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(\`/api/users/\${userId}\`);
        const data = await response.json();
        if (isMounted) setUser(data);
      } catch (error) {
        if (isMounted) console.error(error);
      } finally {
        if (isMounted) setLoading(false);
      }
    };
    
    fetchUser();
    
    return () => {
      isMounted = false; // Prevent state update after unmount
    };
  }, [userId]); // Re-fetch when userId changes
  
  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
};

// Timer with cleanup
const Timer = () => {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    return () => clearInterval(interval); // Cleanup
  }, []); // Empty deps - mount only
  
  return <div>{seconds}s</div>;
};

// Event listener with cleanup
const WindowSize = () => {
  const [size, setSize] = useState({ 
    width: window.innerWidth, 
    height: window.innerHeight 
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []); // No dependencies
  
  return <div>{size.width} x {size.height}</div>;
};

// Multiple effects for separation of concerns
const ChatRoom = ({ roomId }) => {
  const [messages, setMessages] = useState([]);
  
  // Effect 1: Connect to chat
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  
  // Effect 2: Load message history
  useEffect(() => {
    loadMessages(roomId).then(setMessages);
  }, [roomId]);
  
  // Effect 3: Update document title
  useEffect(() => {
    document.title = \`Chat: \${roomId}\`;
    return () => { document.title = 'App'; };
  }, [roomId]);
  
  return <div>{/* UI */}</div>;
};
Note: In React 18+, effects run twice in development (Strict Mode) to help catch bugs. Always implement cleanup functions properly.

3. useContext Hook for Context Consumption

Concept Syntax Description Use Case
useContext Hook const value = useContext(Context) Access context value in component Consume shared state
Context Creation createContext(defaultValue) Create context object Define context for provider
Provider Component <Context.Provider value={...}> Provide context value to children Share data down tree
Multiple Contexts useContext(Context1), useContext(Context2) Consume multiple contexts Separate concerns (theme, auth)
Default Value createContext(fallback) Value when no provider exists Testing, development defaults

Example: Context creation and consumption

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

// 1. Create context
const ThemeContext = createContext('light');

// 2. Create provider component
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// 3. Create custom hook for easier consumption
const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

// 4. Consume context in components
const Header = () => {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header className={theme}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'}
      </button>
    </header>
  );
};

const Content = () => {
  const { theme } = useTheme();
  return <div className={theme}>Content</div>;
};

// 5. App with provider
const App = () => (
  <ThemeProvider>
    <Header />
    <Content />
  </ThemeProvider>
);

// Multiple contexts example
const AuthContext = createContext(null);
const SettingsContext = createContext({});

const Dashboard = () => {
  const auth = useContext(AuthContext);
  const settings = useContext(SettingsContext);
  const theme = useContext(ThemeContext);
  
  return <div>{auth.user.name}</div>;
};

4. useReducer Hook for Complex State Logic

Pattern Syntax Description Use Case
Basic useReducer useReducer(reducer, initialState) Alternative to useState for complex state Multiple related state values
Reducer Function (state, action) => newState Pure function handling state updates Centralized state logic
Dispatch Action dispatch({ type, payload }) Trigger state update User interactions, events
Lazy Initialization useReducer(reducer, arg, init) Compute initial state lazily Expensive initialization
Action Types const ACTIONS = { ADD, UPDATE } Constants for action types Type safety, autocomplete
Immer Integration produce(state, draft => {}) Mutable-style updates with Immer Complex nested updates

Example: useReducer for complex state management

import { useReducer } from 'react';

// Action types
const ACTIONS = {
  ADD_TODO: 'add_todo',
  TOGGLE_TODO: 'toggle_todo',
  DELETE_TODO: 'delete_todo',
  SET_FILTER: 'set_filter'
};

// Reducer function
const todoReducer = (state, action) => {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false
        }]
      };
      
    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
      
    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(t => t.id !== action.payload)
      };
      
    case ACTIONS.SET_FILTER:
      return { ...state, filter: action.payload };
      
    default:
      return state;
  }
};

// Component using useReducer
const TodoApp = () => {
  const initialState = {
    todos: [],
    filter: 'all'
  };
  
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  const addTodo = (text) => {
    dispatch({ type: ACTIONS.ADD_TODO, payload: text });
  };
  
  const toggleTodo = (id) => {
    dispatch({ type: ACTIONS.TOGGLE_TODO, payload: id });
  };
  
  const deleteTodo = (id) => {
    dispatch({ type: ACTIONS.DELETE_TODO, payload: id });
  };
  
  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });
  
  return (
    <div>
      <input onSubmit={(e) => {
        addTodo(e.target.value);
        e.target.value = '';
      }} />
      
      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            {todo.text}
            <button onClick={() => deleteTodo(todo.id)}>✕</button>
          </li>
        ))}
      </ul>
      
      <div>
        {['all', 'active', 'completed'].map(filter => (
          <button
            key={filter}
            onClick={() => dispatch({ 
              type: ACTIONS.SET_FILTER, 
              payload: filter 
            })}
          >
            {filter}
          </button>
        ))}
      </div>
    </div>
  );
};
Note: Use useReducer over useState when: (1) complex state logic, (2) multiple sub-values, (3) next state depends on previous state, (4) need to optimize performance with deep updates.

5. useMemo and useCallback Optimization Hooks

Hook Syntax Description Use Case
useMemo useMemo(() => computation, [deps]) Memoize computed values Expensive calculations
useCallback useCallback(() => {}, [deps]) Memoize function references Prevent child re-renders
Dependencies Array [dep1, dep2] Recompute when deps change Control memoization lifecycle
Empty Dependencies useMemo(() => val, []) Compute once, never update Constants, one-time calculations
Referential Equality React.memo() with callbacks Prevent unnecessary re-renders Optimized child components

Example: useMemo and useCallback optimization

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

// useMemo for expensive computations
const DataTable = ({ data, filterText }) => {
  // Memoize filtered and sorted data
  const processedData = useMemo(() => {
    console.log('Processing data...'); // Only logs when deps change
    return data
      .filter(item => item.name.includes(filterText))
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [data, filterText]);
  
  return (
    <table>
      {processedData.map(item => (
        <tr key={item.id}><td>{item.name}</td></tr>
      ))}
    </table>
  );
};

// useCallback to memoize functions
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // Without useCallback - new function every render
  const handleClick = () => {
    console.log('Clicked');
  };
  
  // With useCallback - same function reference
  const handleClickMemo = useCallback(() => {
    console.log('Clicked', count);
  }, [count]); // Recreate when count changes
  
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      
      {/* Child re-renders when text changes without memo */}
      <ChildComponent onClick={handleClickMemo} />
      
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
    </div>
  );
};

// Memoized child component
const ChildComponent = memo(({ onClick }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click Me</button>;
});

// Complex example combining both
const SearchableList = ({ items }) => {
  const [query, setQuery] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');
  
  // Memoize filtered items
  const filteredItems = useMemo(() => {
    return items.filter(item => 
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [items, query]);
  
  // Memoize sorted items
  const sortedItems = useMemo(() => {
    return [...filteredItems].sort((a, b) => {
      const compare = a.name.localeCompare(b.name);
      return sortOrder === 'asc' ? compare : -compare;
    });
  }, [filteredItems, sortOrder]);
  
  // Memoize callback
  const handleSort = useCallback(() => {
    setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
  }, []);
  
  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
      />
      <button onClick={handleSort}>Sort</button>
      <List items={sortedItems} />
    </div>
  );
};
Warning: Don't overuse memoization! Only use useMemo and useCallback when: (1) expensive computations, (2) referential equality matters (props to memoized children), (3) profiling shows performance issues.

6. useRef Hook for DOM and Value References

Use Case Syntax Description Example
DOM Reference const ref = useRef(null) Access DOM element directly <input ref={ref} />
Mutable Value ref.current = value Store value that doesn't trigger re-render Previous state, timers
Access Element ref.current.focus() Call DOM methods imperatively Focus, scroll, measure
Instance Variable useRef(initialValue) Persist value across renders Intervals, subscriptions
Previous Value useRef() + useEffect Track previous prop/state value Compare changes
Ref Callback ref={(node) => {}} Function called with DOM node Dynamic refs, measurements

Example: useRef for DOM access and mutable values

import { useRef, useEffect } from 'react';

// Focus input on mount
const AutoFocusInput = () => {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} />;
};

// Store timer ID without triggering re-renders
const Timer = () => {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);
  
  const start = () => {
    if (!intervalRef.current) {
      intervalRef.current = setInterval(() => {
        setCount(c => c + 1);
      }, 1000);
    }
  };
  
  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };
  
  useEffect(() => {
    return () => stop(); // Cleanup
  }, []);
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
};

// Track previous value
const usePrevious = (value) => {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
};

const Counter = ({ count }) => {
  const prevCount = usePrevious(count);
  
  return (
    <div>
      Current: {count}, Previous: {prevCount}
    </div>
  );
};

// Measure element dimensions
const MeasureElement = () => {
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({});
  
  useEffect(() => {
    if (divRef.current) {
      const { width, height } = divRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);
  
  return (
    <div ref={divRef}>
      Size: {dimensions.width} x {dimensions.height}
    </div>
  );
};

// Ref callback for dynamic refs
const DynamicList = ({ items }) => {
  const itemRefs = useRef(new Map());
  
  const scrollToItem = (id) => {
    const node = itemRefs.current.get(id);
    node?.scrollIntoView({ behavior: 'smooth' });
  };
  
  return (
    <ul>
      {items.map(item => (
        <li
          key={item.id}
          ref={(node) => {
            if (node) {
              itemRefs.current.set(item.id, node);
            } else {
              itemRefs.current.delete(item.id);
            }
          }}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
};
Note: useRef returns a mutable object whose .current property is initialized with the passed argument. The object persists for the component's lifetime. Changing .current doesn't cause re-renders.