Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 12 min read

Your own hooks & the rules of hooks

Sharing logic – without inheriting components

In one sentence

A custom hook is just a function whose name starts with “use” and that uses other hooks. It shares code – never values: every component gets its own copy.

As soon as the same combination of state and effect turns up a second time, you can pull it into a function of its own. That is all a “custom hook” is: an ordinary function whose name starts with use and that uses hooks itself.

The two rules

rules.txt
1. Call hooks only at the top level
   -> not in an if, not in a for, not after an early return
   (exception: the new use from React 19)

2. Call hooks only from React functions
   -> components (capital letter) or your own hooks (useXyz)

The reason: React keeps track of hooks by their order, not their name. If a useState is skipped on the second pass, the whole list shifts – and your counter suddenly holds the value of the text field.

the order changes
if (!user) return null;
const [name, setName] = useState("");   // sometimes skipped
condition moved down
const [name, setName] = useState("");
if (!user) return null;

Rule of thumb: all the hooks first, then the conditions. Or move the conditional logic into a component of its own.

Writing your own hook

useLocalStorage.ts
export function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const raw = localStorage.getItem(key);
      return raw ? (JSON.parse(raw) as T) : initial;
    } catch {
      return initial;
    }
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;   // as const -> a tuple, not an array
}

// Using it feels just like useState:
const [theme, setTheme] = useLocalStorage("theme", "light");
Pitfall Hooks share logic, not values

If two components call useLocalStorage("theme", …), each gets its own state. They do not see each other's changes. If you want shared state you need context or an external store.

usePrevious.ts
// 1) remember the previous value
export function usePrevious<T>(value: T) {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}

// 2) debounce
export function useDebounced<T>(value: T, ms = 300) {
  const [late, setLate] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setLate(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return late;
}

// 3) a toggle
export function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((o) => !o), []);
  return [on, toggle] as const;
}
Three classics that turn up in almost every project.
Reach for it when …
  • +

    The same state-plus-effect combination turns up a second time

  • +

    A component is so full of wiring you can no longer see the output

  • +

    You want to test the logic without rendering any markup

Skip it when …
  • Building a hook for a single line

  • Pure computations that use no hooks – that is an ordinary function

  • A hook that does ten unrelated things

Tip Name it after the job, not the mechanism

useChatConnection(roomId) says more than useEffectWrapper. A good hook name describes what it does for you – the way useState is not called useMemoryCell.

Does it stick?

4 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Build useToggle

Two places in this code do the same thing: flip a boolean. Pull the logic into a useToggle hook and use it twice – the two switches have to stay independent.

  • useToggle returns [value, toggle]
  • Both switches can be flipped separately
  • The hook is exported and called useToggle
import { useState } from "react";

// TODO: build useToggle here and export it
export function useToggle(initial = false) {
  return [initial, () => {}] as const;
}

export default function App() {
  const [a, toggleA] = useToggle();
  const [b, toggleB] = useToggle(true);

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <button onClick={toggleA} data-testid="a">A: {String(a)}</button>{" "}
      <button onClick={toggleB} data-testid="b">B: {String(b)}</button>
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →