reactjsreact-hooks

Does calling a custom hook inline inside another hook's arguments violate the Rule of Hooks?


As far as I know, all hooks in React should be called at the top level of a component or custom hook — not inside conditions, loops, or expressions. But I recently stumbled upon a code pattern that made me question this rule:

useSubscribe(events, 'close', useEventCallback((e) => handleAction(onClose, e)));

useEventCallback:

export function useEventCallback<TArgs = any, TResult = any>(callback: (args?: TArgs) => TResult) {
  const refCallback = useRef(callback);

  refCallback.current = callback;

  return useCallback((args?: TArgs): TResult => {
    return refCallback.current(args);
  }, []);
}

useSubscribe:

export function useSubscribe(source: Subscribable, name: string, fn: SubscribeHandler, namespace?: ObservableNamespace) {
  useEffect(() => {
    if (!(source && fn)) {
      return;
    }
    source.subscribe(name, fn, namespace);
 
    return () => source.unsubscribe(name, fn, namespace);
  }, [source, name, fn, namespace]);
}

However, a senior colleague suggested this usage is fine.

But I thought that even though useEventCallback returns a stable function (using useCallback with an empty [] dependency), calling it inline inside another hook’s arguments still creates a new hook instance on every render.

That means a new function reference is passed to useSubscribe every render, which defeats the purpose of using a stable callback, because useEffect inside useSubscribe sees a different fn on every render and re-subscribes.

Does this pattern violate the Rules of Hooks and break referential stability?


Solution

  • Yes, this is valid and totally normal.

    useSubscribe(events, 'close', useEventCallback((e) => handleAction(onClose, e)));
    

    is no different than

    const cb = useEventCallback((e) => handleAction(onClose, e));
    useSubscribe(events, 'close', cb);
    

    in its evaluation, and React hooks couldn't tell the difference between them. The two snippets behave exactly the same.

    AFAIK, all hooks in React should be called at the top level of a component or custom hook — not inside conditions, loops, or expressions.

    The rules of hooks just say that you cannot call hooks dynamically, in loops or conditions, as React couldn't distinguish the hook identities when the number of hook calls changes from render to render - it simply identifies them by order.

    There is however no rule that a hook cannot be called inside an expression (and technically, even the rhs of an assignment isn't a top-level expression already), as long as that expression is not evaluated conditionally (e.g. ? … : …, ?? … or || …). A team may however have conventions that discourage this, so that the code is easier to read and scan for validating the rules of hooks - it should be readily apparent whether a line contains a hook call or not. That's also why hook names should begin with use…, while there is no technical reason behind this.

    When the rules of hooks say "top level", they refer to nested functions in particular. The hooks need to be called during the rendering of the function component. Calling it from a function declared inside the component is illegal, if that function is e.g. used as an event handler, as an effect callback, state initialiser, reducer, or is in any other way passed around and called elsewhere/elsewhen.