Codepath

React useEffect vs useState

Overview

React’s useState and useEffect hooks are essential tools for managing state and side effects in functional components. However, they serve distinct purposes, and knowing when to use each—or when to avoid them entirely—can make your code cleaner and more efficient.

This guide covers:

  • What useState and useEffect do
  • When to use useState vs. useEffect vs. local variables
  • Practical examples and decision-making workflows
  • Common pitfalls and how to avoid them
  • Additional resources for deeper learning

Quick Recap

What is useState?

The useState hook manages state—data that persists across renders and triggers re-renders when updated. It’s ideal for tracking component-specific data like user inputs or UI toggles.

const [count, setCount] = useState(0);

What is useEffect?

The useEffect hook handles side effects—operations that interact with the outside world (e.g., APIs, DOM updates) or need to run at specific lifecycle moments (mount, update, unmount).

useEffect(() => {
  fetch('https://api.example.com/data')
    .then(res => res.json())
    .then(setData);
}, [count]);

What About Neither?

Sometimes, you don’t need either hook. Local variables or event handler logic can suffice for transient or render-specific computations.

function Component() {
  const temporaryValue = Math.random(); // No need for useState
  return <p>{temporaryValue}</p>;
}

When to Use What?

Decision Workflow

graph TD
    A[What do I need?] --> B{Does it change and affect UI?}
    B -->|Yes| C[Use useState]
    B -->|No| D{Does it interact with external systems, or need to run at specific lifecycle moments?}
    D -->|Yes| E[Use useEffect]
    D -->|No| F[Use local variables or event logic]

1. Use useState When:

  • Data changes over time and impacts the UI.
  • The component needs to "remember" values between renders.
  • Examples: Form inputs, counters, toggles.
function Toggle() {
  const [isOn, setIsOn] = useState(false);
  return <button onClick={() => setIsOn(!isOn)}>{isOn ? 'On' : 'Off'}</button>;
}

2. Use useEffect When:

  • You need to perform side effects (e.g., fetch data, update DOM).
  • Code must run at specific lifecycle points (mount, update, unmount).
  • Examples: API calls, subscriptions, timers.
function DataFetcher() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(setData);
  }, []); // Runs once on mount
  return <div>{data ? data.message : 'Loading...'}</div>;
}

3. Use Neither When:

  • Data is transient and doesn’t need to persist across renders.
  • Logic can be handled in render or event handlers.
  • Examples: Temporary calculations, one-off event responses.
function Calculator() {
  const handleAdd = () => {
    const a = 5; // Local variable, no state needed
    const b = 10;
    alert(a + b);
  };
  return <button onClick={handleAdd}>Add</button>;
}

Key Differences

Feature useState useEffect
Purpose Manage state Handle side effects
Triggers Render Yes, when state updates No, runs after render
Runs When On state change On mount/update/unmount
Dependency None Optional dependency array
Use Case UI-related data External interactions
graph LR
    A[Component Lifecycle] --> B[Render]
    B --> |useState| C[State Updates]
    C --> B
    B --> |useEffect| D[Side Effects]
    D --> E[External Systems]
    E --> |May update state| C

Practical Examples

Example 1: Counter with Title Update

function Counter() {
  const [count, setCount] = useState(0); // State for UI
  useEffect(() => {
    document.title = `Count: ${count}`; // Side effect
  }, [count]); // Runs when count changes

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  • useState: Manages count for the UI.
  • useEffect: Updates the document title as a side effect.

Example 2: Form with Validation

function Form() {
  const [input, setInput] = useState(''); // State for input
  const isValid = input.length > 3; // Local variable, no hook needed

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <p>{isValid ? 'Valid' : 'Too short'}</p>
    </div>
  );
}
  • useState: Tracks input for the form.
  • Neither: isValid is computed during render, no need for state or effects.

Example 3: Fetching Data with Cleanup

function Timer() {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval); // Cleanup
  }, []); // Runs once on mount

  return <p>Seconds: {seconds}</p>;
}
  • useState: Tracks seconds for the UI.
  • useEffect: Sets up a timer (side effect) with cleanup.

How to Decide: A Checklist

  1. Does the data affect the UI and change over time?
    • Yes → Use useState.
  2. Does it involve external systems or lifecycle events?
    • Yes → Use useEffect.
  3. Is it a one-off calculation or event-driven logic?
    • Yes → Use local variables or event handlers.
graph TD
    A[Data Need] --> B{Affects UI?}
    B -->|Yes| C{Changes Over Time?}
    C -->|Yes| D[useState]
    C -->|No| E[Local Variable]
    B -->|No| F{External System?}
    F -->|Yes| G[useEffect]
    F -->|No| H[Local Logic]

Additional Resources

Official Documentation

Video Tutorials

Blogs and Articles

Tools

Fork me on GitHub