-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Context API #2
Comments
@sorvell might be able to put up a draft PR with a few more details relatively soon |
|
This should be pretty well addressed by the event carrying a callback. The provider calls the callback with data, the consumer reacts however it needs to, including triggering a render. If the consumer supports it, the callback can be called multiple times and trigger updates each time, very similar to |
The one thing I've wondered with this event sort of approach is timing. To use events am I correct the element would have to be connected to the DOM? Would the data be available to us at the time of The other thing is whether slotting should have any consideration. Can light DOM children slotted under Shadow DOM Provider still get access to context. With Solid Element I use a bit of a different approach. I do lookups up the DOM tree traversing in and out of Slots and Shadow roots. Once you are connected you are then attached to the context more or less since it's just using the DOM heirarchy. I realize events probably do the same thing without re-creating this walk but as I said I never was comfortable trusting the timing of things. It's possible things have changed from the heavily polyfilled ecosystem we had here a few years ago. And I had a second motivation where I wanted to unify context with the non-webcomponent side which does everything before being attached (or even connected to it's parent) so I needed some creativity there. |
Events are synchronous, so the timing is suitable for data needed synchronously.
Events work in disconnected DOM trees, so the elements would not necessarily need to be connected to the document, though a provider would need to be an ancestor in the disconnected tree to provide anything. The question there is what signal the element would use to fire the event to request the data?
Yep, if the provider is ready. One nice thing about firing the event in
Event bubble up the flattened tree, so slots and slot ancestors, including the host, could act as providers to slotted children. This can enable use cases where a utility element in a shadow root, maybe like a theme provider, could provide objects to a slotted child. I think events, their timing, and their scoping are really well suited for this problem. |
When I was implementing our router I created a context API using events. I abandoned the idea in the end for other reasons, but it worked out reliably cross browsers. Lion also uses events for form registration which needs to be synchronous. |
Here is one implementation Polymer/pwa-helpers#64 unistore is waiting for the context developit/unistore#175 |
While I find Context APIs handy, I've found them to not be quite enough in larger applications. In most cases, I've used a more robust Dependency Injection system. Not too long ago, I did some work to integrate a DI system I had written so that it worked on top of React's context. I'd really like to ensure that I can implement DI on top of this Context proposal in a similar way. I imagine that it would work by:
The first point should allow 3rd parties to introduce their own container into the DOM hierarchy to resolve services requested through the DI system, while the second point should allow the DI system to resolve requests by components that are based on the raw context API with no knowledge of containers. Does that make sense? |
The initial event's callback adds the need for an event dispatching mechanism. The provider must act as an observer and has to track consumers (all of the callbacks). Another idea: the provider could simply modify the context event object with the data, a reference to itself, and an event name that it will trigger on itself to update the value. The consumer could then use the data, and then addEventListener() onto the provider, and consume as normal. |
@sorvell did you ever get anywhere with your draft of a proposal here? We're looking at formulating a solid "How do I make apps without the context API?" resource for engineers at Adobe moving from React to Web Component projects and would love to shape it around something that was closer to a "community protocol". I'm sure we'll need a good amount of iteration on anything we bring together here, but having something to poke holes in would be a great first step! |
I have begun documenting my current approach to Context here in this repository: https://github.com/benjamind/lit-context I've outlined my best guess at what the API should be, and also provided some implementations against Would love some feedback on this. |
Hi, The proposal also does not specify how to pass arguments to the context provider. I assume this would be passed in the detail object. However, without standardization of this would make it only partially useful as you won't get proper types support. I fixed this by defining a number of events and these events have well defined properties. At least in the declaration file. I tried both defining properties on the event and on the detail object. Both works pretty well but from my experience it's easier to keep it all in the If I would to design the architecture for this today I would design a new type of event that extends CustomEvent interface. This event has the For an event subscription, when the context provider announces changes in the context, I use separate set of events which I call state events. The context provider dispatches an event when a value change. A component registers event listeners for a specific state change event. When change occurs it requests the changed data from the provider (I usually pass the meta data about the change to this state event containing the identifier of the object, if any, and optional change name). This way I was able to build rather large applications that work both separately as a standalone application and in a multi-tenant environment. I hope my POV will help understand different use cases and implementations. |
@jarrodek could you give a little more information as to what you found lacking in callbacks during your test period?
I'd love to see this fleshed out a little bit more in code. I'm not sure where the binding responsibilities and patterns for the "state events" occur. It feels like you'd be adding a good amount of decentralized responsibility to the consumer of the context, which might be beneficial for its disconnection from the context generally but feels like it might actually end up being a lot more work that just registering a callback centrally on the context provider. |
@benjamind this looks really great. Can't wait to discuss tomorrow! |
I'd definitely like to hear more about what failed you with regard to use of callbacks @jarrodek . One of the benefits for me of this approach is that it is very easy to reason about, and extremely simple protocol. It is also very easy to implement it synchronously, while not closing the door on asynchronous value fulfillment, which I think for many scenarios is quite valuable. I think I can see what you are driving at in terms of state update, making it an explicit action of the component to 'get' its updated value in response to an event emitted by the state store. I suspect this is not incompatible with the API as proposed above and could be built atop this to provide a different approach to state management. I've added a few goals/non-goals to the readme in the repo linked above. I've specifically called out wanting to keep this API as simple as possible, a variety of things could be implemented using this protocol, but I think at its core we should strive to keep this as straightforward as we can for maximum compatibility. |
btw, @jarrodek there's really no reason to use |
Sorry for not responding faster. @Westbrook @benjamind Callbacks a generally OK but as it tuns out they need additional logic around them. While with what I have done I can use an async function and relatively simple logic to read the state from the store, I have to do a bit more when working with callbacks. Consider this. The component requests an information from a context provider that is a IDB wrapper connected to the events system. The component renders a specific view for the data. I setup the component in the DOM with the id of the information this component should render. Inside this component I can build a single async function that refreshes the state via dispatching the event. Something similar to the following: set dbId(value) {
this.#id = value;
this.update();
}
async update() {
const { dbId } = this;
const e = new CustomEvent('getcustomer', {
bubbles: true,
cancelable: true,
composed: true,
detail: {
id: dbId,
}
});
this.dispatchEvent(e);
const customer = await e.detail.result;
// do stuff with the customer
} In my ecosystem I have a library of such events with types definitions so I can simplify this even further: set dbId(value) {
this.#id = value;
this.update();
}
async update() {
const { dbId } = this;
const customer = await AppEvents.Customer.get(this, id);
// do stuff with the customer
} With callbacks I can achieve the same goal but I would need at least two functions update() {
const e = new ContextEvent('getcustomer', this.customerCallback.bind(this));
this.dispatchEvent(e);
}
customerCallback(customer) {
// do stuff with the customer
} The effect is the same but I believe the ergonomics of this may be optimized. |
@justinfagnani I completely agree. At some point I started experimenting with having own events extending the |
As for an example. This is the library I am using in one of my OSS apps: https://github.com/advanced-rest-client/arc-events/tree/stage/src/telemetry |
I just use similar pattern in one of our projects. But I am actually attaching resolve and reject from a /**
* Request data using an event
* @param {?String} method - Data request method
* @param {?String} name - Data source name
* @param {?Object} params - Request parameters
* @returns Promise
*/
requestData(method, name, params) {
const promise = new Promise((resolve, reject) => {
this.dispatchEvent(
new CustomEvent(`somename`, {
detail: {
name: name,
method: method,
params: params,
resolve: resolve,
reject: reject,
},
bubbles: true,
composed: true,
})
);
});
return promise;
} Promises seems to be more flexible comparing with callbacks. |
I've been interested in an approach like this since watching Justin's talk on DI via events some years back. Really love the way things are shaping up One of the ideas I was eventually wanting to try out was to dispatch an event allowing a request for multiple values at once. Modifying Ben's example a bit.. this.dispatchEvent(
new ContextEvent({
'cool-thing': {
callback: coolThing => {
this.myCoolThing = coolThing; // do something with value
},
},
'another-thing': {
callback: anotherThing => {
this.anotherThing = anotherThing;
},
},
})
); Possibly a preoptimization, I'm not really sure how costly lots of events in connectedCallback might be... One potential problem is if two separate ancestors are responsible for providing a given piece of the puzzle. I'm not sure if the community protocol has an opinion around a provider using |
@MikeVaz I initially went down a similar path, but I realized one restriction of the promise attached to event model is that the promise can only be resolved once. For cases like wanting to request a theme from higher up the hierarchy and be able to respond to external theme changes this doesn't work so well. I also toyed with just attaching the payload to the detail in the event with the emitter reading it back off after firing the event. This has the same problem of only being usable once. We want I think to go for the 'most capable approach' with this API so it feels like allowing multiple delivery of requested data is valuable. @robrez this approach also came up in internal discussions at Adobe. I like this extension a lot and will likely include this in the proposal PR I hope to put up later this week/early next. I also want to detail some approaches I have considered for handling context name conflicts by having context providers which handle aliasing contexts, as well as the possibility of caching context providers that was discussed at the last meeting on this topic. I hope to get the proposal PR up more formally end of this week to detail the main proposal and outline some of these extensions so we can have something to more formally review and hone. Apologies for not getting to it sooner. |
You kind of want to have a single source of truth at the Datasource / ContextProvider level. Where we resolve the promise. Why would we want to resolve it twice? In our implementation we also add a |
If you only resolve the requested data once, how would an element become aware of a change in the context value? In the case of a theme provided from somewhere further up in the tree, if the theme is changed, how would the component be notified? With single resolution there's no way, unless we pass something instead of the value which in turn provides the value on a subscription basis. This would then require more book-keeping on the part of the consumer to properly cleanup these subscriptions with the lifecycle of the element. I feel like we're better off keeping this API as open as possible, rather than being pre-emptively restrictive and thus putting more burden on users of the API to define further specifications for how to interact with context values which change. If we compare the proposal here with the React Context API that too allows multiple resolution of the value:
I believe this ability has value, it means that component authors won't necessarily have to build up more subscriptions and do more handling for simple cases where we have a context value that is changing through the lifetime of the requesting component. I understand traditionally in a Dependency Injection framework some have a single resolution principle, but this is not a DI framework. You could build one atop this protocol, but that is not the main goal here, this is really about allowing shortcuts for data delivery to eliminate prop-drilling. |
Reading through the draft proposal again, particularly the It reminded me of The provider could dispatch an event to the consumer; and the consumer could add an event listener to itself -- perhaps using the "once" option to enforce that requirement where it exists. Seems overcomplicated and probably misses the mark on some of the goals... hesitant to even mention it but 🤷 |
@robrez interesting idea, I'm a little hesitant to introduce more Event traffic into the mix. They're not entirely free and have a small overhead in object creation. We could achieve the same result without the extra event perhaps, but we would be paying the event creation overhead on every value update. Its likely not a big deal though, so might be worth exploring. I would like a better mechanism for enforcing the 'once' behavior. |
With the recent expansion of AbortControllers to support removing event listeners, maybe there’s something to be found in leveraging those in a |
I'm excited about this proposal! It's critical for me in that I'm designing a solution to extract media state from a media provider (eg: Probably goes without saying but a standard is really needed around this, so I appreciate the effort by the community 😄 To summarise my thoughts: I think events and callback/s is the right way to implement this feature but it's an internal detail of the context provider/consumer discovery. It's not something that should be implemented by developers at the web component design level. I can't see the point of decoupling them at that level via events, and why anyone would want to deal with naming collisions/conventions, difficulty typing the context callback and a verbose means of wiring everything up. These are just my thoughts... feel free to let me know I'm wrong or what I'm missing. I guess I don't understand in what "context" (pun?) we're talking about this implementation. As a standard? Feature of Lit? Best practices for library authors? Or...? Current ProposalI was reading the proposal by @benjamind (thanks for your time/work thus far 👏 ), and I was just wondering what's the point of decoupling the context consumer and provider. I understand that it achieves a lower-level implementation that we can layer on top of, but I can't seem to think of why or where it would be required. This feels more like a DI solution than a focus purely on a context solution (prop drilling). I think your proposal actually highlights the disadvantages of decoupling well but doesn't highlight advantages really. I see this section also aims to address the concerns around a
It's a little hand-wavy and vague to me... I'm still not seeing an issue. I don't see any advantages in introducing a namespace for the context API which will obviously lead to collisions. In the logger example what's so hard about exporting the context from the same module where the In the current proposal, I believe most developers will either resort to extending this solution with a I'm also not a big fan of the idea of playing around with events to connect element/context... feels verbose, error prone and unnecessary. Simply the uncertainty of the type of object you're going to receive is bad sign to me (due to naming collisions). We can bandaid this with naming conventions but why if we can design a solution that avoids it? Design Thoughts
Here's a sample and very rough API of what I'm thinking... Definitionsexport type ContextHost = HTMLElement;
export type ContextConsumerDeclaration = Context<any> | {
context: Context<any>,
// ...options
};
export interface ContextConsumerDeclarations {
readonly [key: string]: ContextConsumerDeclaration;
}
export type ContextProviderDeclaration = Context<any> | {
context: Context<any>,
// ...options
}
export interface ContextProviderDeclarations {
readonly [key: string]: ContextProviderDeclaration;
}
export interface ContextHostConstructor {
new (...args: any[]): ContextHost;
readonly contextConsumers?: ContextConsumerDeclarations;
readonly contextProviders?: ContextProviderDeclarations;
}
export interface ContextProvider<T> {
value: T;
// Better name?
reset(): void;
}
export interface ContextConsumer<T> {
readonly value: T;
}
export interface ContextLifecycle<T> {
onConnected?(): void;
// Ability to prevent update by returning false?.
onUpdate?(newValue: T): boolean;
onUpdated?(newValue: T): void;
onDisconnected?(): void;
}
export interface ContextProviderOptions<T> extends ContextLifecycle<T> {}
export interface ContextConsumerOptions<T> extends ContextLifecycle<T> {
once?: boolean;
// Not sure but someway to transform the consumed context would be handy.
transform<R>?(newValue: T): R
}
export interface Context<T> {
initialValue: T;
provide(host: ContextHost, options?: ContextProviderOptions<T>): ContextProvider<T>;
consume(host: ContextHost, options?: ContextConsumerOptions<T>): ContextConsumer<T>;
} APIfunction createContext<T>(initialValue: T): Context<T> {
// ...
} // Decorators
function consumeContext<T extends Context<any>>(
context: T,
options?: ContextConsumerOptions
): PropertyDecorator {
// ...
}
function provideContext<T extends Context<any>>(
context: T,
options?: ContextProviderOptions
): PropertyDecorator {
// ...
} // Mixin to introduce the context API support. Ideally used on your base library element.
function WithContext<T extends ContextHostConstructor>(Base: T): T {
// ...
} UsageJavaScriptconst context = createContext(10);
class MyProviderElement extends WithContext(HTMLElement) {
constructor() {
super();
this.context = context.initialValue;
}
/** @type {ContextProviderDeclarations} */
static get contextProviders() {
return {
context,
// OR
context: {
context,
// ...options
}
};
}
}
class MyConsumerElement extends WithContext(HTMLElement) {
constructor() {
super();
this.context = context.initialValue;
}
/** @type {ContextConsumerDeclarations} */
static get contextConsumers() {
return {
context,
// OR
context: {
context,
// ...options
}
};
}
} TypeScript or with TC39 Decoratorsconst context = createContext(10);
class MyProviderElement extends WithContext(HTMLElement) {
@provideContext(context, { /** options */ }) context = context.initialValue;
}
class MyConsumerElement extends WithContext(HTMLElement) {
@consumeContext(context, { /** options */ }) context = context.initialValue;
} |
Hi, I share with you
Exampleimport { Channel } from "@atomico/channel";
const CHANNEL = "MyChannel";
// Parent channel
const parentChannel = new Channel(document.body, CHANNEL);
class MyComponent extends HTMLElement {
constructor() {
super();
// Child channel
this.channel = new Channel(this, CHANNEL);
}
connectedcallback() {
this.channel.connected((data) => (this.textContent = JSON.stringify(data)));
}
disconnectedCallback() {
this.channel.disconnect();
}
}
// Connect the channel to the native DOM event system
parentChannel.connect();
parentChannel.cast("I'm your father"); The api is minimalist.
Implementation example https://webcomponents.dev/edit/9X95yAWPKW0mdAg7OYZd |
@mihar-22 and @UpperCod thanks both for the detailed input! I think perhaps we are going down similar lines here. I've had more discussion with a few folks who have suggested similar approaches and I am in general agreement that tightly binding consumer and provider is the way to go now. Please take a look at the initial implementation of the context protocol as defined in this PR lit/lit#1955 I do also agree a decorator implementation would be desirable and is next on my list for inclusion into the lit context PR. It is however not something we should include in the protocol spec I think since it's very specifically an implementation detail that might be best tailored by libraries specific to component implementations. I will try and find time to revise the proposal to include these changes in direction and incorporate some of the ideas in your proposals. |
@mihar-22 one of the main goals of this proposal is decoupling via events, it's a core principle. Not only might providers and consumers not be directly aware of each other (ex: loggers, theming, etc.) but with web components the components might not share any implementation. Events already provide the cross-library, cross-component communication that we need for this. The reason why this protocol is defined in terms of events is because that's the underlying mechanism. Libraries can implement the protocol and provide whatever nice interface into the system that they want, as implemented by @benjamind in lit/lit#1955. The interfaces you have there might be ok, but at the protocol level we need to define how consumers and providers actually communicate. That's the event protocol, and once we have this, implementations can freely use any interfaces they want, include ones like yours. Regarding |
Can this be used to implement constructor dependency injection? I'm not fluent enough to read the proposal or infer from the information, but a dependency injection scenario would be interesting to me. One good article on using React State API for constructor dependency injection, via Locator pattern, is at https://blog.testdouble.com/posts/2021-03-19-react-context-for-dependency-injection-not-state/ as far as I could find. Now with reactive controllers, this injecting a HttpClient as described in the article to a repository and that repository to reactive controllers (and handling state in SQL database in browser) would look an interesting pattern to me at least. |
@veikkoeeva I don't think it could be used for 'constructor dependency injection' since construction happens in the DOM for many web component usage scenarios, however, it can indeed be used to create other dependency injection patterns. We internally at Adobe have some usages where we are doing 'property injection'. Which actually is one of the most common usages of the Context API I forsee. I very much like the pattern of components having properties which can be set directly, or which can be provided via Context events being satisfied via the protocol as this pattern seems very versatile. |
Now that #10 is merged, let's close this and take discussions to individual issues. Thanks @benjamind ! |
/me looking where to watch the discussion online on that API. (oh, right |
Most discussion should now be happening in individual issue tickets filed against the proposal in this repo. This way people can make PRs for changes to the proposed API and we can iterate from there. It's also fine to just open issue tickets for points of discussion if there are concerns that you don't immediately have solutions for. |
Yes, that's right. I was scrolling around to see currently opened tickets for Context API, and I've found |
And you can open new issues prefixed with [context] if you have a new topic to raise. |
The Lit team has been prototyping a Context-like API based on events.
The basic idea is that components that need some contextual data will fire an event to request the data. The event will carry a callback use the pass the data to the requesting component. Components that can provide the data will respond to the event and call the callback with the data. The callback can then trigger arbitrary work in the requesting component, including a re-render.
The details needed for interop are things like the event name, callback property name and signature, and any keys that are used to identify the context and or data object.
The text was updated successfully, but these errors were encountered: