The dependency array is not a list of things to watch

It reads like a watch list and it is not. It is a declaration of everything the effect closes over, and the effect re-runs whenever any of them changes identity.

// wrong: onSelect is a new function every render, so this runs forever
useEffect(() => {
  const sub = subscribe(id, onSelect);
  return () => sub.unsubscribe();
}, [id, onSelect]);

// the parent has to stabilise it
const onSelect = useCallback(handleSelect, []);

// and the empty array means: this closes over nothing that changes.
// if that is a lie, the effect sees the first render's values forever.

The linter rule computes the correct array and it is right far more often than intuition, which is why arguing with it usually ends in a bug. Removing a dependency to stop an effect looping is treating the symptom — the cause is a value being recreated upstream, and that is where the fix belongs. An empty array is a claim, not a convenience, and it is the most common source of stale data on screen.