React Hooks Cheat Sheet: useState, useEffect, useMemo, and useCallback

Four hooks, zero fluff. A practical reference for the React hooks you'll actually use daily, with the gotchas that'll save you from bugs I've already hit.

React Hooks Cheat Sheet: useState, useEffect, useMemo, and useCallback

If you’ve written React for more than a week, you’ve used these hooks. The problem isn’t syntax, it’s knowing when each one matters and when you’re just adding complexity for no reason.

I’ve gone through phases with hooks. First I overused everything. Then I stopped using useMemo and useCallback entirely because “premature optimization.” Now I’m somewhere in the middle, reaching for each hook when it actually solves a problem. That’s what this post is, a practical cheat sheet for four hooks I use daily, with the gotchas that caught me off guard.


useState: Your Component’s Memory

The basic building block. You need state, you use useState.

import React, { useState } from 'react';

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

  return (
    <div className="counter-container">
      <h3>Count: {count}</h3>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

The Functional Update Thing

This one matters more than you’d think. If your new state depends on the current state, always use the functional form:

setCount(prevCount => prevCount + 1);

Without it, you can get stale values in closures, especially in event handlers or intervals that capture the state value at render time. I had a polling component that was incrementing a counter but occasionally skipping numbers. The fix was exactly this.


useEffect: The Side Effect Swiss Army Knife

Anything that happens “outside” the component render cycle, data fetching, subscriptions, DOM manipulation, timers, goes here.

The Dependency Array

This is where most bugs live. Seriously.

// Runs after every render (almost never what you want)
useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const result = await response.json();
    setData(result);
  };

  fetchData();
}, []); // Empty array means this runs once on mount
  • No array: Runs after every render. You almost never want this.
  • Empty []: Runs once on mount. Good for initial data fetches.
  • With dependencies [a, b]: Runs when a or b changes. Put everything your effect actually uses in here.

Clean Up After Yourself

If your effect sets up a subscription or timer, return a cleanup function:

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Tick');
  }, 1000);

  return () => clearInterval(timer); // Cleanup on unmount
}, []);

I shipped a component once that added a scroll listener without cleanup. Every time the component remounted, it stacked another listener. The app got progressively slower until someone noticed the tab was eating memory like crazy. Clean up your effects.


useMemo: Cache the Expensive Stuff

useMemo caches the result of a computation. It only recalculates when its dependencies change.

const expensiveCalculation = (num) => {
  console.log('Calculating...');
  for (let i = 0; i < 1000000000; i++) {} // Artificial delay
  return num * 2;
};

function App({ number }) {
  const memoizedValue = useMemo(() => expensiveCalculation(number), [number]);

  return <div>Result: {memoizedValue}</div>;
}

When to Actually Use It

Two cases where it’s genuinely worth it:

  1. Expensive calculations: anything that takes noticeable time. Filtering a massive array, complex math, that kind of thing.
  2. Object reference stability: when you’re creating an object or array that’s passed to a memoized child component. Without useMemo, the reference changes every render and the child re-renders anyway.

When NOT to Use It

Don’t wrap every calculation in useMemo. The hook itself has overhead, it stores the result and checks dependencies on every render. For cheap operations, you’re adding complexity for zero benefit. I’ve seen code where useMemo(() => value + 1, [value]) was a thing. Don’t do that.


useCallback: Stable Function References

While useMemo caches a value, useCallback caches a function.

Functions get recreated on every render in JavaScript. That’s just how it works. If you pass a function as a prop to a child component wrapped in React.memo, that child re-renders every time because the function reference changed, even if the function does the exact same thing.

const handleIncrement = useCallback(() => {
  setCount(c => c + 1);
}, []); // Function reference stays the same across renders

The Real Use Case

function Parent() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(0);

  // Without useCallback, MemoizedButton re-renders every time Parent renders
  const handleClick = useCallback(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <>
      <MemoizedButton onClick={handleClick} />
      <button onClick={() => setOther(other + 1)}>Other: {other}</button>
    </>
  );
}

Changing other won’t cause MemoizedButton to re-render because the handleClick reference is stable.

The Honest Take

useCallback is the most overused optimization hook. If the child component isn’t memoized, wrapping the callback in useCallback does literally nothing. And even when it is memoized, the performance difference is usually negligible unless you’re rendering hundreds of items or dealing with very expensive child renders.

Use it when you have a measurable problem. Not by default.


Quick Reference

HookBest Used ForKey Behavior
useStateLocal stateTriggers a re-render when updated.
useEffectSide effectsRuns after render; supports cleanup.
useMemoCalculated valuesReturns a cached value.
useCallbackFunctionsReturns a stable function reference.

The Actual Advice

Here’s what I’d tell myself when I was starting with hooks:

  • Start with useState and useEffect. They cover 90% of cases.
  • Add useMemo when you see a performance problem with expensive calculations or when referential equality matters for memoized children.
  • Add useCallback when the same. If a child is memoized and re-renders anyway because of function references, wrap the callback.
  • Don’t put eslint-plugin-react-hooks on ignore. The exhaustive-deps rule is annoying but it catches real bugs.
  • Profile before optimizing. React DevTools Profiler is your friend. Don’t guess.

The best React code isn’t the code that uses every optimization hook, it’s the code that’s simple enough that it doesn’t need them.

Member discussion

0 comments

Start the conversation

Become a member of >hacksubset_ to start commenting.