Context API's are an interesting new concept in the new backoffice UI, and really I think HQ themselves are still learning the best approach for it's use.
About the Context API
Fundamentally it is an event based mechanism to access state or "context" from ancestores of a component node. Based on the Web Components Context Protocol RFC which in turn is inspired by React's Context Api, the key purpose is to solve the problem of prop drilling
.
Prop drilling is when you have deeply nested components where the outer most component exposes some state needed by one of it's deeply nested descendants and where previously the only way to pass this state down was to expose properties on each component in the tree to pass through this state till it reaches the necesarry component.
A secondary use case of the Context API being used in the backoffice UI (thought this may be open to change) is for a simplified dependency injection mechanism whereby a root level component can expose all available services and then individual components can request a service they require via the Context API.
The Basics
The Context API is split into two parts, a provider
and a consumer
.
Context Provider
Components that are able to provide some "context" make use of the UmbContextProviderMixin
in their component definition (See here for more info on Lit Mixins).
@defineElement('my-component')
export default class MyComponent extends UmbContextProviderMixin(LitElement) {
...
}
This mixin provides components with a single method provideContext(alias, instance)
which when called within the components constructor, registers the given state as consumable.
@defineElement('my-component')
export default class MyComponent extends UmbContextProviderMixin(LitElement) {
constructor() {
super();
this._state = new MyComponentState();
this.provideContext('myState', this._state);
}
}
Context Consumer
For a Web Component to consume this state, it needs to make use of the UmbContextConsumerMixin
in it's component definition.
@defineElement('my-child-component')
export default class MyChildComponent extends UmbContextConsumerMixin(LitElement) {
...
}
This mixin provides components with a single method consumeContext(alias, callback)
which when called within the components constructor, registers the given callback for any state found in it's ancestry with the given alias.
@defineElement('my-child-component')
export default class MyChildComponent extends UmbContextProviderMixin(LitElement) {
constructor() {
super();
this.consumeContext('myState', (state) => {
// So something with state
});
}
}
How It Works
Under the hood this is simply built around the browsers own event system. When a consumer requests a context an event is fired which, naturally for events bubbles up the DOM tree till it hits a component that can handle it. Any provider component that is able to fulfill the request stops the event from bubbling further and calls the callback with the requested state.
Dependency Injection
Whilst we've only been talking about "state" so far, really the context being provided / requested could be anything, so in it's simplest form it could be a stateful object, but it equally could be a service object that exposes an API for requested networked resources.
Currently this is also what the Context API is being used for in the current codebase, but I'm not sure yet whether the Context API is truely the right solution for this.
Questions
One big issue I have at the moment, which is mostly around DI, is that this only works if you have some containing component that wraps all of your application. Of course Umbraco does as they are in control of the markup, but for package developers we don't. The issue here then is if we do things like inject trees and editor windows into Umbraco, these live on different DOM tree graphs and so they have no common ancestor in my control from which to register these dependencies.
I've already raised this as a discussion point and there are already some interesting conversations about the possibilities which is positive.
My other main concern is that of how context requirements by child components get documented as from the outside components don't expose an API that suggests they need a specific context, but it's only once they run that we may find out it needs a context. Maybe this is just documentation, or maybe there is some tooling that can highlight this requirement, we'll have to wait and see.
Conclusion
Contexts are a neat solution to the specific problem of prop drilling, but it comes with some trade offs. If you read the React docs they clearly point out that it should be used sparingly due to how it ties you to a "magic" structure and prevents component reuse.
With Umbraco currently extending it's use for Dependency Injection, it does kind of work, but there are still questions of how it will work outside of the main codebase which I think are still clearly being thought about.
I get why HQ would implement this as it's really quite lightweight and avoids some big DI library dependency, but I think there are definitely still some wrinkles to be ironed out on this one.
Top comments (1)
@mattbrailsford Should the
@defineElement
decorators be@customElement
instead?