DEV_NET_CORE
GET_STARTED
ReactHooks, effects, and custom hooks

“You might not need an effect” patterns

Overview

"You might not need an effect" is a React design principle: use effects for synchronization with external systems, not for ordinary data flow inside React. An external system might be a browser API, a network connection, a timer, a subscription, a non-React widget, analytics, or imperative DOM integration.

Many effects in React applications are unnecessary because they are trying to do work that can happen during rendering or in an event handler. Removing those effects usually makes code simpler, faster, and less bug-prone.

Bad pattern:

Code
function Form({ firstName, lastName }: Props) {
  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  return <p>{fullName}</p>;
}

Better:

Code
function Form({ firstName, lastName }: Props) {
  const fullName = `${firstName} ${lastName}`;

  return <p>{fullName}</p>;
}

For interviews, this topic matters because it shows whether a developer understands React's rendering model. A strong candidate should be able to explain when to derive values during render, when to handle logic directly in event handlers, when to reset state with a key, when to store IDs instead of duplicated objects, and when an effect is genuinely appropriate.

The practical goal is to use effects as escape hatches, not as a default way to coordinate React state.

Core Concepts

Effects Are for External Synchronization

Use an effect when a component needs to synchronize with something outside React.

Good effect example:

Code
function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return <h1>Room {roomId}</h1>;
}

This is appropriate because a network connection exists outside React. The effect starts synchronization, and the cleanup stops it.

Common external systems:

  • Browser events.
  • Timers.
  • WebSocket or SignalR connections.
  • Third-party widgets.
  • Imperative APIs.
  • Subscriptions.
  • Analytics that should run because a screen appeared.
  • Client-side data fetching when no framework or data library owns it.

If there is no external system, pause before writing an effect.

Pattern: Calculate Derived Values During Render

If a value can be calculated from props or state, calculate it during render.

Bad:

Code
function InvoiceSummary({ items }: { items: InvoiceItem[] }) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(items.reduce((sum, item) => sum + item.price, 0));
  }, [items]);

  return <p>Total: {total}</p>;
}

Better:

Code
function InvoiceSummary({ items }: { items: InvoiceItem[] }) {
  const total = items.reduce((sum, item) => sum + item.price, 0);

  return <p>Total: {total}</p>;
}

The effect version renders once with stale total, then renders again after the effect sets state. The render-time calculation is simpler and avoids an extra render pass.

Good derived values:

  • fullName from firstName and lastName.
  • total from cart items.
  • isValid from form fields.
  • visibleItems from items and a filter.
  • selectedItem from items and selectedId.

Pattern: Use useMemo for Expensive Pure Calculations

Most derived calculations should just run during render. If a pure calculation is noticeably expensive and its inputs often stay the same, cache it with useMemo.

Code
function TodoList({
  todos,
  filter,
}: {
  todos: Todo[];
  filter: string;
}) {
  const visibleTodos = useMemo(
    () => getFilteredTodos(todos, filter),
    [todos, filter]
  );

  return <List items={visibleTodos} />;
}

Use useMemo for performance, not correctness. The component should still behave correctly if the calculation runs every render.

Avoid this:

Code
const [visibleTodos, setVisibleTodos] = useState<Todo[]>([]);

useEffect(() => {
  setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

That creates redundant state and an unnecessary effect.

Pattern: Put Event-Specific Logic in Event Handlers

If logic happens because the user did something, put it in the event handler.

Bad:

Code
function ProductPage({ product }: { product: Product }) {
  const [isInCart, setIsInCart] = useState(false);

  useEffect(() => {
    if (isInCart) {
      showNotification(`${product.name} added to cart`);
    }
  }, [isInCart, product.name]);

  return (
    <button onClick={() => setIsInCart(true)}>
      Add to cart
    </button>
  );
}

Better:

Code
function ProductPage({ product }: { product: Product }) {
  function handleAddToCart() {
    addToCart(product.id);
    showNotification(`${product.name} added to cart`);
  }

  return (
    <button onClick={handleAddToCart}>
      Add to cart
    </button>
  );
}

The notification should happen because the user clicked the button, not because the component later observed a state value.

Event-handler logic includes:

  • Submitting forms.
  • Showing user-action notifications.
  • Navigating after a click.
  • Starting a checkout flow.
  • Updating state in response to input.
  • Calling a parent callback.

Pattern: Reset State with a Key

If changing a prop means a subtree should be treated as a different instance, use a key.

Bad:

Code
function ProfilePage({ userId }: { userId: string }) {
  const [comment, setComment] = useState("");

  useEffect(() => {
    setComment("");
  }, [userId]);

  return <CommentBox value={comment} onChange={setComment} />;
}

This renders once with the old comment, then clears it after the effect runs.

Better:

Code
function ProfilePage({ userId }: { userId: string }) {
  return <Profile key={userId} userId={userId} />;
}

function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState("");

  return <CommentBox value={comment} onChange={setComment} />;
}

Changing the key tells React that this is a different profile instance, so local state below it resets naturally.

Use this when the identity of a screen, form, or subtree truly changes.

Pattern: Store IDs Instead of Duplicated Objects

If you store both a collection and a selected object from that collection, the selected object can become stale.

Bad:

Code
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(initialItems[0]);

Better:

Code
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(initialItems[0]?.id ?? null);

const selectedItem =
  items.find((item) => item.id === selectedId) ?? null;

Now item data has one source of truth, and selection is derived from the current list.

This often removes effects like:

Code
useEffect(() => {
  setSelectedItem(null);
}, [items]);

Instead of adjusting duplicated state, store a stable identity and derive the object.

Pattern: Avoid Chains of Effects

A chain of effects is when one effect updates state, which triggers another effect, which updates more state.

Bad:

Code
useEffect(() => {
  if (card !== null) {
    setGoldCardCount((count) => count + 1);
  }
}, [card]);

useEffect(() => {
  if (goldCardCount > 3) {
    setRound(round + 1);
  }
}, [goldCardCount, round]);

This makes the flow indirect and can create extra renders. Prefer calculating the next values in the event handler or reducer that knows what happened.

Better:

Code
function handleCardDrawn(card: Card) {
  setState((state) => {
    const nextGoldCount =
      card.kind === "gold" ? state.goldCardCount + 1 : state.goldCardCount;

    return {
      ...state,
      card,
      goldCardCount: nextGoldCount,
      round: nextGoldCount > 3 ? state.round + 1 : state.round,
    };
  });
}

When state transitions are related, keep them in one event handler or reducer.

Pattern: Notify Parents from the Event That Changed the Value

If a child changes a value and the parent needs to know, call the parent callback from the event handler.

Code
function Toggle({
  checked,
  onCheckedChange,
}: {
  checked: boolean;
  onCheckedChange: (checked: boolean) => void;
}) {
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    onCheckedChange(event.currentTarget.checked);
  }

  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={handleChange}
    />
  );
}

Avoid this pattern:

Code
useEffect(() => {
  onCheckedChange(checked);
}, [checked, onCheckedChange]);

The parent callback should usually be part of the event flow, not a reaction to a later render.

Pattern: Initialize State Correctly

If initial state is expensive, pass an initializer function to useState.

Code
const [todos, setTodos] = useState(() => loadInitialTodos());

Do not initialize with an effect unless the initialization depends on an external system that must happen after render.

Bad:

Code
const [todos, setTodos] = useState<Todo[]>([]);

useEffect(() => {
  setTodos(loadInitialTodos());
}, []);

Better:

Code
const [todos, setTodos] = useState(() => loadInitialTodos());

For values based on props, be clear whether the prop is an initial value or a controlled value.

Code
function Editor({ initialText }: { initialText: string }) {
  const [text, setText] = useState(initialText);
}

The name initialText communicates that later prop changes are not expected to overwrite local edits.

Pattern: Split Actual Effects by Purpose

Sometimes you do need effects, but one effect is doing multiple unrelated jobs.

Bad:

Code
useEffect(() => {
  document.title = title;

  const connection = createConnection(roomId);
  connection.connect();

  return () => connection.disconnect();
}, [title, roomId]);

Better:

Code
useEffect(() => {
  document.title = title;
}, [title]);

useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();

  return () => connection.disconnect();
}, [roomId]);

Each effect should describe one synchronization process. This makes dependencies and cleanup easier to reason about.

Data Fetching Nuance

Fetching in an effect can be acceptable for simple client-only components:

Code
useEffect(() => {
  let ignore = false;

  async function load() {
    const data = await fetchUser(userId);

    if (!ignore) {
      setUser(data);
    }
  }

  load();

  return () => {
    ignore = true;
  };
}, [userId]);

However, data fetching is often better handled by framework loaders, server rendering, server components, route-level APIs, or client data libraries that provide caching, deduplication, race handling, and preloading.

Interview answer: effects can fetch data, but manual effect fetching is not automatically the best production data-loading architecture.

Decision Checklist

Before adding an effect, ask:

  • Is there an external system involved?
  • Can this value be calculated during render?
  • Is this logic caused by a user event?
  • Can state be reset with a key?
  • Can duplicated state be replaced with an ID or derived value?
  • Are multiple related state updates better handled in one event handler or reducer?
  • Should this be a custom hook because it synchronizes with an external system?
  • Is a framework or data library a better home for data fetching?

If the answer does not involve external synchronization, the effect may be unnecessary.

Common Mistakes

Common mistakes include:

  • Using effects to calculate fullName, totals, filtered lists, or validation booleans.
  • Updating state in an effect immediately after render when the value could be derived.
  • Moving event-specific logic into effects.
  • Resetting state on prop change with an effect instead of a key.
  • Storing duplicated objects and trying to sync them with effects.
  • Chaining effects that update each other's dependencies.
  • Suppressing the dependency linter instead of changing the code.
  • Treating effects as lifecycle methods instead of synchronization processes.
  • Fetching data manually in every component without thinking about caching or race conditions.

Best Practices

Use these rules of thumb:

  • Effects are escape hatches for external synchronization.
  • Derive values during render whenever possible.
  • Use event handlers for user-triggered actions.
  • Store the minimal source of truth.
  • Reset state with keys when component identity changes.
  • Prefer IDs and derived lookup over duplicated selected objects.
  • Use reducers for related state transitions.
  • Split unrelated effects.
  • Let dependency warnings guide refactoring instead of suppressing them.

Interview Practice

PreviousRerender triggers, derived state, and avoiding duplicated stateNext UpMemoization with useMemo and dependency correctness