Skip to content
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

Contexts #58

Open
Tracked by #61
jeswin opened this issue Apr 23, 2022 · 0 comments
Open
Tracked by #61

Contexts #58

jeswin opened this issue Apr 23, 2022 · 0 comments
Labels
enhancement New feature or request

Comments

@jeswin
Copy link
Member

jeswin commented Apr 23, 2022

See previous discussion at #54

Copying some parts of it below:

Initial Proposal by @spiffytech

How could an app be offered a Context API?

Jeswin said: So, would it suffice to do a regular import (of a file such as the following) into the component?

This is how I'm handling it now (typically with forgo-state so I don't have to track what has to rerender), but static exports only work if the use case is amenable to a singleton. If you need something reusable (a repeatable component hierarchy, or a reusable application pattern), current Forgo requires prop drilling. Context APIs allow context to be created at arbitrary points in the component hierarchy, rather than as app globals.

I think it's also suboptimal to make state an app-wide singleton as a default. Sometimes that turns out to be a mistake, and then it's a whole big thing to change how it works. If the default is to make a Context, then if you suddenly need two independent copies of some component hierarchy, that's no problem.

The one big hassle with Contexts is types. A component expects to be run with certain Context values available, and I'm not sure how to express that in a type-safe way, guaranteeing a component is only instantiated in the correct context.

Example use case: in my app, I have a big list of cards with buttons on them. Click a button and refetch updated data from the server. I don't want the user to click buttons too fast, because when the network request finishes the layout in the affected region will shift, so whats under their thumb could change as they're reaching to press it.

So I want to disable all buttons in the affected region while the request is in flight, plus 500ms afterwards. There are 3-4 levels of component in between the top of the region and all of the buttons, so prop drilling the disabled state + the disable function would be a pain. And once I implement this, I'd like it to be reusable across parts of my app.

I don't want to make this a global singleton, because I want to gray out disabled buttons, and graying out the whole UI would be confusing, and also the user may want to go click things that have nothing to do with the affected region while the request is in-flight.

With Contexts I could make a MagicButton component that watches the context, replace all my stock s, and nothing else has to change.

I don't think Forgo needs Contexts to come with magic auto-rerender behavior. Since rerendering an ancestor rerenders all descendants, descendants don't usually need to be reactive to Context state. And if they do, that's what forgo-state is for. It's just avoiding the prop drilling that's needed.

Jeswin said: I am in favor of coming up with a proposal and then adding this feature; unless you think we can avoid prop drilling with some other technique.

I can't think of an a way around it with the existing API. Everything comes down to, if you want an out-of-band way to do this, you still have to communicate which specific out-of-band instance (key, symbol, etc.) you want to reference. Which comes back down to prop drilling or asking the framework to do the drilling for you.

To toss out an alternative, instead of putting contexts into core, a library could implement the feature if forgo had an API to walk the component ancestry and a stable component identifier.

const contexts = new WeakMap<ChildNode,unknown>();
export function set(ref, key, value) {
  contexts.set(args, value);
}
export function lookup(args, key) {
  return contexts.get(args.element) ?? lookupContext(forgo.getParentRef(args, key))
}

function MyComponent(_props, args) {
  libcontext.set(args, 'foo', 123);
  const bar = libcontext.lookup(args, 'bar');
}

I don't like that this leaks args.element to userland. I'd prefer a stable opaque identifier like a Symbol. But then Forgo would have to bookkeep a Symbol<->ChildNode lookup.

I do like that removing implementation details from forgo-state already calls for an API to walk the component ancestry, and that this aligns with the vision of an extensible core, rather than putting another feature into core.

Btw, can this (your example of disabling buttons) be done with forgo-state?

Sorta. In the current form of my scenario, there are known, hardcoded regions that need to be managed, so I think forgo-state would work for the case in front of me (where Foo & Bar are each full of cards):

164844109-c641d5a2-45a8-4b69-b910-57904d562f9f

But if I wanted to independently affect dynamic regions it'd get trickier (where each Foo is still full of cards, but now there's a bunch of separate Foos):

164843897-02e020ab-aa5b-42ff-a97d-9689e706f7f6

forgo-state might do it using bindToStateProps, but only if the dynamic regions had obvious keys that were globally unique across my app. I'll bet a bindToStateProps approach would be okay-ish for many scenarios, but it amounts to the developer inventing an ad-hoc approximation of scoping rules which will break down in complex scenarios.

@jeswin jeswin added the enhancement New feature or request label Apr 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant