Just recently I had a good conversation with @samwightt about the design of Context (not limited to react but as a general Dependency Injection mechanism). Several conclusions are made and some problems (objectively) come to light. So I wrote this memo.
Both of them pass information down (from parent to children) so they seem to be comparable. A good starting point is trying to answer the question: what will happen if there is only props/context available.
- If only props available, it's easy to get "props drilling" if the children requiring information is 'far away' from the parent providing such information.
- To solve props drilling, we should try not encapsulate subcomponents as much as possible but that's not ideal (as Separation of Concern) and sometimes subcomponents need to manage their own local state.
- If only context available, the View (template) is hard to be represented.
Seems being without context is acceptable and context is more likely a complement to props.
But that's not a satisfying answer to me. I have a more radical statement: Props for View, Context for Model. That means
- Props are accessed and should be only accessed in template
- Model is only accessible via Context (Dependency Injection)
The reason why this is not obvious (at least in React) is that React Context are not designed for passing frequently changed state. But if the state is a reference-stable reactive container (that you can subscribe to get the value changes) then it becomes applicable (that's exactly how Redux works in React).
By locking props in view, props drilling will be automatically prevented from you because you are limited to abuse props (grabbing everything from props). The Model and View are decoupled, and Context(DI) is the only bridge connecting up them.
There is a better interpretation: State drive view (as side effect) and for children components props can drive view because props are delegate of state from parent (props as actual argument). Also for parent components props are delegate of children's side effect (of view) (props as formal parameter)
Also this implies the component should be either fully controlled or fully uncontrolled. Not possible to mutate internal state on changes of props.
But I found an exception: list rendering of components which have their own model. This kind of component will probably need to read a constant prop which identify the identity of current model (not to be confused with key but they probably get the same value). That's the only case I found the model has to read props. To solve it, render list with pure component only (but it might be not always applicable)
Although I don't see how DI introduce coupling, while someone argues that the component consuming data from Context is coupled with the corresponding provider. Are they coupled? Yes and No?! They are coupled because the business requires them to be coupled. They are not coupled because DI are designed to decouple things. Are they talking about the same coupling?...
A not so appropriate metaphor: you have legal relationships with your family members, is that kind of coupling? (no pun)
One thing objectively obvious is that, a component needs some information, no matter how it's delivered. Some need very few (like UI controls) and some requires more contexts especially those related to your business. It's also worth noting that, we divide components into smaller components (Separation of Concern applied?), by simply the visual layout but not the information required. Now you heavily rely on props and think it's explicit and low-coupling (just provide props to use the component, very great reusability!) but now every other components using it but can't provide all of required props will simply throw these to where they are used, and then the same pattern will spread like a virus (props drilling, exactly). That means some components declare a prop not because they need it but their children in template need it. The conclusion is either these component are actually coupled via prop definitions, or The Principle of Least Privilege is violated (you know something you don't need to know).
And a more interesting conclusion come out: not all the components have the same reusability (not a binary 'reusable vs non-reusable', but a possibility of being reused), no matter how pure it is or not, a
<Button> tends to be reused more than
<GoodItemDetail> because the latter need more contexts.
Is that hard to declare a variable at top level and directly import it from components? Yes sometime it works. It's also known as Singleton and if you think your application is a singleton, just go for it. I don't think so though.
@samwightt points out a weakness of current React Context API design: you don't know which contexts the component depends on, from the type definition. And I see some down side of flexibility of
useContext hooks and the Hooks design itself - too flexible to be abused.
He compared Angular which has built-in DI and forces dependencies to be declared in constructor. One thing obvious is that a Angular service is more easy to test than a custom React Hook which uses context, because for the former you can just provide some Mock/Double/Spy objects (without enabling DI mechanism), But for the latter, firstly you have no idea what the custom hook depend on, secondly the provided context is probably a internal thing encapsulated by third party that you shouldn't directly rely on (like
useXXXQuery grabbing a cache management implementation that doesn't exposed as a public API), so you must build a mini application with least working requirement to test a single hook. The first issue could be solved by generators - collecting yielded type you will be able to get a union type of all dependencies. But the second point so far I think it's really unbeatable...... I understand why he thought the React Context is magical.
Being magical is not necessary a bad thing but I can't help trying to think a more explicit design of API, and how it would impact the current mental modal. I really love the current one but it could be improved further. I'm still investigating into this.
To be continue