And I'd like to be very clear that I strongly advise against cutting through layers with context because I wholeheartedly agree with your assessment there. You can plumb down those handles explicitly through props and enforce they're provided in a type system.
At the same time, unless I'm really misunderstanding the example you shared, I can't see the problem or mental overhead you're mentioning where we need to be vigilant about having the same hook used in multiple places or understanding the scope of the hook's "local" state and callbacks. That's the point of hooks as a unit of encapsulation -- just like with a class, each use of a hook is a different instance, and the scope is that of the instance (and hook instances are scoped/bound to the lifecycle of the containing component instance). The code in the problem you showed is equivalent to calling setState on the one component instance and expecting that to show up on another instance which happens to be of the same component class. If we understand how React component instances work, that's clearly not the case (state isn't broadcast across instances), and the same goes for hooks.
Looking at what I mean by the improved portabiliy/hoistability of hooks I mentioned in previous comments, let's say we have three components: App, Foo, and Bar. App renders Foo and Bar as children.
We have a business requirement that Foo has some behavior which contains multiple pieces of state and bindings to multiple React lifecycle events (e.g. mount). What we don't know is whether we'll ever need to share that behavior with Bar.
React class API: We need to write bindings for this behavior against class state and lifecycle methods, intermingled with other code. This intermingling means you know there's a good deal of refactoring to do to move the behavior upward to App if we ever need to share the behavior with Bar. As a component grows, more and more logic intermingles on those lifecycle methods and makes refactoring more challenging.
class Foo extends React.Component {
state: {
behaviorState: {...},
unrelatedState: {...},
};
componentDidMount() {
initBehavior(this.props.input);
initUnrelated();
}
...
}
React hook API: We can easily write this behavior in a custom hook, encapsulating the behavior in some combination of useState, useEffect, and other React hooks. Now we have a self-contained useBehavior hook, by definition isolated from any local state, which you can choose to use within Foo. const Foo = ({ input }) => {
const behaviorState = useBehavior(input);
const unrelatedState = useUnrelated();
return ...;
};
At this point, we're not so worried if we need to hoist that one function call up to App and pass the hook's returned handle (behaviorState) back down to Foo and Bar at some point in the future.If this example sounds trivial, I've packaged up 500+ lines of functionality in a single self-contained hook before, and that's all exposed as a single function that consumers can treat as a full encapsulation of all of that functionality, while not worrying if they need to hoist that function call up and pass the output down at some point in the future.
I'm not talking about calling the same hook in Foo and Bar (these are different instances), nor sharing local state sideways across different invocations of hooks, but rather having full confidence it won't be hard to re-anchor that hook upward in the component tree if need be.
> One of the problems Redux is used to solve (as a global store) is peace of mind that you won't need to refactor large swathes of component trees to reparent state and callbacks that need to be shared or persisted across different subtrees as new business requirements arise
> Hooks (especially custom hooks, which are just a composition of other hooks packaged together as a single function) make the reparenting/hoisting/anchoring of state and callbacks trivial compared to other mechanisms, providing similar peace of mind that you're not boxing yourself into an inextensible component tree. (This is regardless of context; passing down props isn't a large pain point, and is often misguided to try to solve because it leads to importable components and hidden contracts.)
My earlier example was to show that re-parenting/hoisting when you replace a global store like Redux, with custom hooks using local state, is not straightforward, and unsafe except for very localized components where you shouldn't be using Redux anyway.
So you'll eventually end up doing prop drilling, or resorting to Context, or back to Redux; in the end hooks did not solve those issues as you proposed (other than the increased portability compared to classes, which you already have with redux or context).