Chapter 2 of 4

Extracting a Hook

Refactoring a component into a hook, and choosing what to return.

The usual path is: write the logic inside a component first, then move it out when a second component needs it - or when the component gets hard to read.

// Before: fetching logic mixed into the component
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isCurrent = true;
    setIsLoading(true);

    fetchUser(userId)
      .then((data) => { if (isCurrent) { setUser(data); setError(null); } })
      .catch((err) => { if (isCurrent) setError(err); })
      .finally(() => { if (isCurrent) setIsLoading(false); });

    return () => { isCurrent = false; };
  }, [userId]);

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <h1>{user.name}</h1>;
}
// After: the hook owns the mechanics
function useUser(userId) {
  const [state, setState] = useState({ user: null, isLoading: true, error: null });

  useEffect(() => {
    let isCurrent = true;
    setState((previous) => ({ ...previous, isLoading: true }));

    fetchUser(userId)
      .then((user) => isCurrent && setState({ user, isLoading: false, error: null }))
      .catch((error) => isCurrent && setState({ user: null, isLoading: false, error }));

    return () => { isCurrent = false; };
  }, [userId]);

  return state;
}

function UserProfile({ userId }) {
  const { user, isLoading, error } = useUser(userId);

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <h1>{user.name}</h1>;
}

Return an array or an object?

  • Array when there are one or two values the caller will want to rename, like useState.
  • Object when there are several, so callers destructure by name and the order does not matter.
const [value, setValue] = useToggle(false);      // array: names are chosen by caller
const { user, isLoading, error } = useUser(id);  // object: self-documenting

Keep hooks focused

A hook that fetches data, tracks scroll position and writes to localStorage is three hooks. Small hooks compose; large ones only get larger.