Skip to content

Conversation

@NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented Nov 17, 2025

Propose adding a new Public API for exploring render-tree-based scope.

Rendered

Summary

This pull request is proposing a new RFC.

To succeed, it will need to pass into the Exploring Stage, followed by the Accepted Stage.

A Proposed or Exploring RFC may also move to the Closed Stage if it is withdrawn by the author or if it is rejected by the Ember team. This requires an "FCP to Close" period.

An FCP is required before merging this PR to advance to Accepted.

Upon merging this PR, automation will open a draft PR for this RFC to move to the Ready for Released Stage.

Exploring Stage Description

This stage is entered when the Ember team believes the concept described in the RFC should be pursued, but the RFC may still need some more work, discussion, answers to open questions, and/or a champion before it can move to the next stage.

An RFC is moved into Exploring with consensus of the relevant teams. The relevant team expects to spend time helping to refine the proposal. The RFC remains a PR and will have an Exploring label applied.

An Exploring RFC that is successfully completed can move to Accepted with an FCP is required as in the existing process. It may also be moved to Closed with an FCP.

Accepted Stage Description

To move into the "accepted stage" the RFC must have complete prose and have successfully passed through an "FCP to Accept" period in which the community has weighed in and consensus has been achieved on the direction. The relevant teams believe that the proposal is well-specified and ready for implementation. The RFC has a champion within one of the relevant teams.

If there are unanswered questions, we have outlined them and expect that they will be answered before Ready for Release.

When the RFC is accepted, the PR will be merged, and automation will open a new PR to move the RFC to the Ready for Release stage. That PR should be used to track implementation progress and gain consensus to move to the next stage.

Checklist to move to Exploring

  • The team believes the concepts described in the RFC should be pursued.
  • The label S-Proposed is removed from the PR and the label S-Exploring is added.
  • The Ember team is willing to work on the proposal to get it to Accepted

Checklist to move to Accepted

  • This PR has had the Final Comment Period label has been added to start the FCP
  • The RFC is announced in #news-and-announcements in the Ember Discord.
  • The RFC has complete prose, is well-specified and ready for implementation.
    • All sections of the RFC are filled out.
    • Any unanswered questions are outlined and expected to be answered before Ready for Release.
    • "How we teach this?" is sufficiently filled out.
  • The RFC has a champion within one of the relevant teams.
  • The RFC has consensus after the FCP period.

@github-actions github-actions bot added the S-Proposed In the Proposed Stage label Nov 17, 2025
@NullVoxPopuli NullVoxPopuli mentioned this pull request Nov 17, 2025
@gossi
Copy link

gossi commented Nov 18, 2025

I'm a fan of accessing the DI container through functions. I looks a bit clunky though, when you always get the scope first, then the owner from it.

A similar sounding concept, with also similar idea (to access the DI container) is done in ember-polaris-service. Could you also take reference as prior art? I think there is a little effect onto this RFC based on that.

See: https://github.com/chancancode/ember-polaris-service

@NullVoxPopuli
Copy link
Contributor Author

@gossi done, thanks!

@rtablada
Copy link
Contributor

rtablada commented Nov 18, 2025

@gossi from my understanding scope is not the DI container but a localized scope for the current rendering context.
DI container implies (usually) a singleton for the entire application.
It is even lower level than an idea of something like context which only has a lookup and sends the closest entry (some implementations literally throw away reference to things that provide the same key)

The way context is shown here, you have access to full iterator stack so if you really wanted to you could implement something like getAncestors:

function getAncestors(component: typeof Component) {
  const scope = getScope()

  return scope.entries.filter(a => a instanceof component).toArray();
}


class TreeNode extends Component {
  get depthCount() {
    return getAncestors(TreeNode);
  }

  <template>
   <Provide @key={{TreeNode}} @value{{this}}>
   ... IDK cool stuff probably
   </Provide>
  </template>
}

This supports both class providing and consuming as well as template-based providing and consuming.

```gjs
import { Consume, Provide, consume, provide } from 'hypothetical ember-prototype-context library';
Copy link
Contributor Author

@NullVoxPopuli NullVoxPopuli Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: for exposing context, we need some limitations to prevent misuse.

Examples that should be non-controversial:

  • throw an exception of the Consume usage can't find a corresponding Provide (via matching key)
  • for types, the key should be recommended to be a class (i.e.: constructor type), so that the value can be assumed to be an instance of that type -- this isn't as flexible as the "anything goes" contexts that react has, but there isn't a whole lot of value (I think) in supporting just primitive values for the whole of a context.

This is already how ember-primitives' dom-context works, but I forgot to mention it, because I forgot that it wasn't obvious 🙈

{{a.id}} === 0
</Consume>

{{#let (consume StateA) as |a|}}
Copy link
Contributor Author

@NullVoxPopuli NullVoxPopuli Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: consume as a component has no value over let (and let is shorter (by two characters))


<template>
<Provide @key={{StateA}} @context={{ (newA) }}>
<Consume @key={{StateA}} as |a|>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: for TypeScript reasons, the key is recommended to be a class

const newB = () => new StateB();

class CustomProvide extends Component {
@provide a = new StateA();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: ditch this API, it's weird


## Alternatives

None.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

  • explain why we don't like the type of context apis that Vue and Svelte are doing (they don't have block-scope semantics, which is the key thing our components / templating is about)

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Nov 21, 2025

Something else to explore -- can we use the module hierarchy to ensure that everything can be known statically?

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Nov 21, 2025

another option for ensuring type safety:

class Foo {
  bar = 2;
}

// libraries could export `foo`, or each thing separately if they want
const foo = makeContext(Foo)

<template>
  <foo.Provide>
  
    {{#let (foo.consume) as |fooState|}}
      {{fooState.bar}} == 2
    {{/let}}
  
  </foo.Provide>

  {{ (foo.consume) }} <-- throws an exception
</template>

Clarify:

  • what is reactive? (not the value passed to makeContext)
    • reactivity must be within the context
  • reduce harm
    • throw on no provider in the hierarchy

@rtablada
Copy link
Contributor

Bringing in ideas from createContext in react as well as AutoFac from C# (which allows localized scope for factories/injections)

function makeContext<T>(defaultValueFactory: () => T) : {
	Provide: ProvideComponent<T>,
	consume: () => T
}

This allows the greatest flexibility while also ensuring better safety.

For people who want to stick to @NullVoxPopuli's a zero argument constructor as the factory this could easily be built as a higher order component:

function makeClassContext<T>(klass: new () => T) {
  return  makeContext(() => new klass);
}

Note

This makeClassContext could be an overload and this could have benefits of registering classes that may have lifecycle

But we also have a default factory function inspired by AutoFac which allows hooks for possible scenarios:

  1. Some people might be ok with value being null so can return null
  2. Some people may want their context to error when not found (this can be done and reduce null checks in user land if desired)
  3. Some teams may want to return a value for production stability, but want to have a dev assertion

@rtablada
Copy link
Contributor

rtablada commented Nov 21, 2025

Example of a "Permission" context where you may want to add user impersonation (which must be able to pass through element boundaries)

But the providers let you sparsely provide args

<template>
    <Permission @user={{session.user}}>
        <Permission @role={{adminRole}}>
            <UserCard />
        </Permission>

        {{!-- We want to impersonate and override outer scope--}}
        <Permission @user={{tempUser}}>
            <UserCard />
        </Permission>
    </Permission>
</template>

const PermissionContext = makeContext(() => {
    user: null,
    role: null,
})


class Permission extends Component {
    get permissionInfo () {
        const upperTheme = PermissionContext.consume();
        return {
            user: this.args.user ?? upperTheme.user,
            role: this.args.role ?? upperTheme.role,
        }
    }

    <template>
        <PermissionContext.Provide @value={{this.permissionInfo}}>
            {{yield}}
        </PermissionContext.Provide>
    </template>
}

@NullVoxPopuli
Copy link
Contributor Author

Some TS exploration and overloads: https://www.typescriptlang.org/play/?#code/MYGwhgzhAECyCeAVAFgSwHYHNoG8C+AUAQKYAeADgPYBOALtAGYCu6wtql60qEAcsQHcwAIxDEAFKQBc0MOngBKGaW4x0g6OIB0OsNUwQZLANbpKA9AG0AugugBeAHzRenXkxDhRxADyVhAFbEbM44BNDQAPSR0Ijw5MQAysDUqOT0yJDQ1MRgnvDQwmAAJtDk1JS0lfHE0BBM5FR00AC0LdC0yMQFxZwA5PQ5eSDw4VExQuj0DJQgxjBMEBjYnTx1NPSUDHW0TAzbbYVM9Ki0fTCZ6MUFM9RlxNTbcqXDHZm0WmPRXzHEECAYWgtYo8ERiFrqUhAgHqaAAAVoNQgKTSQL+MNokTMLRYEDADGILQAtsQicIHi0wMBgH8IGMcrtqFxSFpypVqgkAPxaYCcCC0ahMNg0Bz2ezQUgAbgIhAIzFY7E4sigD1o4iJEEwMn5qSwABoOn9aEZ0KZzOglMqIKqYLQjbgCABIVDbcQAQjt-LsnQqAmgGsw0tlgIeDCptQAChUAG6oYrEADClCJVHUUx8AEFQtx0NHKMYwcQZAKmLVg1NQ+HoEmK1CfIhQk6o5RY-GZM3W4nk6niOmG9LHbz0PUSTJxHYnLEg0RIgAqWfhWfZYiMrjRvKl6CZGDW2gAeQsD1kVytlGAqDAdoAIkaKvBCwm0CBSrvoErTpoXdwzjA5G-AsEtAKIukQkBQGyMCwbAcFwRJgMYXa1rQ9aOOI8Zhh4tAAGobsQABiVJVNQ8BjhOziIJaNZ2nW-ZgU00xQYqsHwYh1HIQ2aHEBhIDYbhBHCsRMjqH644OORlGcGxKHSmQ9GQQqMH+ixVFkOxqHoWAmE4SApYyBRMgqTRjgyeBzTytBSpwQhhlqdouj6IY0ARnoYAkna1AQD4iIJFs0AAPpWfhNB4YxMEoc4AA+TkuW5Dyed5xC+QFLF4TQ-BCN4kXRdQrkrnFXk1ElgWpdQ2mlo4dhhI6kDWnQ4gAAYAKqmmYFiyKkiIyAAJDgegGFoYhYJ0eBaEp1mSapb7oCM0CyYBMCcLU5AxXl1D1QafUQANvaYJ0origAjAo0pOmI9AaZhhYOO1BiWAADNYJ3Oq6PDpYWnHcbQhYKJVTqOgyTBMv5xVpYI70XTx30DrKz2aAlvkQ193j7dAfTmUxfS-Y6-0roDXDJQhJUhQpnAfZpkPeMdTowwDQME0FpW4WTl2UydspyqFSr00TnPoCh4gMOgpFibEElIShDqOtEsieGUqTrnasjkGsXQ5KdK7QAhBTioFzWoAAjqWNmJPAZKzOOT201wVWOh2cZFmNxB2-G1DiFrBqCwoep-UOI4O4FNZ+67Wte9TMocyT+Mg9Qb3ePzgtCRoomTvp1YTUZVVnZr3TXbr6AG0b6e0CbZsgBbRA46uku2zG9syIFzsPG73Qexa3vY77TCjo7gdd03Ift4QhDo4p3M0GVvgceuOkO6nNkS2EEQRFnWu5yxeuG6xqkl8I5tU06VvV439csY3wct9A0+lqHHd8n3J-jcOffn4og-hwQAdF+ICAoMsVOf0hcQydnAACZ-7KS-sAzQ+AfrSgAWxIBZFoDCTgEgNAWBxxUyAA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-Proposed In the Proposed Stage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants