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

React context-like feature #4029

Closed
sebastiandedeyne opened this issue Oct 25, 2016 · 32 comments · Fixed by pinterest/teletraan#1117
Closed

React context-like feature #4029

sebastiandedeyne opened this issue Oct 25, 2016 · 32 comments · Fixed by pinterest/teletraan#1117

Comments

@sebastiandedeyne
Copy link

sebastiandedeyne commented Oct 25, 2016

Would a React context-like feature ever be added?

Context is an implicit way to pass down data from a component without having to pass them through props on every level.

https://facebook.github.io/react/docs/context.html

It's generally not a good idea to use context for application-specific logic, but it's very useful for objects like stores.

Currently working on a helper mixin that achieves something similar, but would prefer to see this baked in the core.

@posva
Copy link
Member

posva commented Oct 25, 2016

Why would you need contexts with Vue?

@sebastiandedeyne
Copy link
Author

Afaik, there currently isn't a way to implicitly pass down props.

Current options:

  • Explicitly passing down props isn't clean for object like stores for your application state
  • Plugins kind of solve the issue, but they can only be applied globally
  • this.$root is an option, but then you're forced to set the data on the root component

@sebastiandedeyne
Copy link
Author

Possibly interesting alternative to context: plugins / mixins that apply to a component and all of it's descendants, instead of globally

@fnlctrl
Copy link
Member

fnlctrl commented Oct 25, 2016

@sebastiandedeyne Functional components already have this context feature. (http://vuejs.org/guide/render-function#Functional-Components) Though currently normal components don't.
I guess it can be useful if normal components can get access to it too.

@sebastiandedeyne
Copy link
Author

Context in functional components only provides data from the direct parent (I don't think context provides anything more than a normal component would have?), it doesn't solve the problem of passing data to deeper descendants—which is what context in React does, it's the same name for something different.

@ktsn
Copy link
Member

ktsn commented Oct 26, 2016

I think this feature is nice to have in Vue.
Because some libraries actually implement this behavior (vue-router, vuex) their own global mixin.
And also, there is a use case on userland, e.g. passing an event bus instance to all descendants.

@sebastiandedeyne
Copy link
Author

To get the ball rolling;

I needed this feature in a recent project, and solved it by writing two mixins: expose and inject. You'd apply expose to a parent component, and it would create an $exposes object on the vm with whatever data you want. Next, calling inject on a child component would recursively look for a parent component that exposes a certain property, and set it on the child component (prefixed by a $).

Vue.component('parent', {

    template: '<div><child></child></div>',

    data() {
        return {
            user: {
                name: 'Sebastian',
            },
        };
    },

    mixins: [
        expose(vm => ({
            user: this.user,
        })),
    ],
});

Vue.component('child', {

    template: '<div>{{ $user.name }}</div>',

    mixins: [
        inject('foo'),
    ],
});

@simplesmiler
Copy link
Member

@sebastiandedeyne that is quite an interesting approach.

Maybe something like:

var parent = {
  ...,
  expose: ['user'],
};

var child = {
  ...,
  inject: ['user'],
};

@posva
Copy link
Member

posva commented Oct 26, 2016

That would be the same as passing props.
I'm still unsure about the utility of a context on Vue.
To me it looks like the context issue is either solved with a state management library or by injecting something to every instance.
Is a plugin that inherits the context from the parent overriding properties what you're looking for @sebastiandedeyne ?

@sebastiandedeyne
Copy link
Author

Let's say that, hypothetically, Vuex wouldn't be implemented as a global plugin, but as something that can be applied on a per-component basis.

  • Passing down the store as a prop to every single child component would be counter-intuitive
  • There's no other way to inject an object that's set on a specific component instance

I'm looking for a mechanism to have a property based on something that a parent—an ancestor—component defines, not necessarily the direct parent.

@ktsn
Copy link
Member

ktsn commented Oct 26, 2016

Of course we can do the same thing by explicitly passing values to props but I think we sometimes need such implicit data propagation.

In my case, for example, I am writing canvas rendering abstraction library and it has components for shapes (rect, circle, etc...) rendered on canvas. The shape components have to be descendants of context component that has canvas DOM element.

Example:

<!--
  context has canvas element and it will render all shape components in its descendants.
-->
<context width="300" height="300">
  <rectangle x="10" y="20" width="50" height="50"></rectangle>
  <group x="100" y="100">
    <triangle v-for="t in triangle" :x="t.x" :y="t.y"></triangle>
  </group>
</context>

When the data of shape components are updated, I have to notify to context component through an event bus to re-render the canvas. To do so, I need such react's context-like feature to propagate an event bus because 1) it is quite verbose to write props for all shape components, 2) it needs its own event bus instance for each context component instance and 3) I don't want to manage this on global state management as this is local view's concern.

@HerringtonDarkholme
Copy link
Member

Context is a convenient construct for library author and coupled components(for example tabs and tab-panes). For a library, it is quite strange and lame to encapsulate library local context in a separate global store. Library author might be able to take the burden of passing props all over, but components may also be an interface for end users. @ktsn has a good example and good arguments for context feature.

But I have some questions about after reading React's context document. (disclaimer: I'm not a React user.)

It seems the cooperation of context and shouldComponnentUpdate is not a problem in Vue because context is also reactive. So there is no need to workaround context update as described in this article.

But I still have questions on how context should be found. For example, should context only be found as a child's parent? Or ancestors? Or only closest ancestor?
If we resolve context as ancestors, how can we handle context key conflict? It seems React's context implementation only chooses the closest ancestor. Example.
And this will easily cause problem when multiple libraries provide different contexts with the same name, say, one component receives context from another library component. Or we can collect all context properties from ancestors and pack these properties into an array or a map?

Also, should context in parent component be visible to all its children, in templates and slots? Or only to children in its template? Or only to children in slots? Or provide an option for users to choose?

@HerringtonDarkholme
Copy link
Member

I have tried some toy implementation. It seems to be fully doable in user land without touching vue core.
A working example here and source here.

It requires a token for each expose and injection to avoid conflict. Other parts look like api suggested by @simplesmiler and @sebastiandedeyne

var user = createToken('user')
var parent = {
  ....
  expose: (vm) => ({[user]: this.user})  // computed property key for token
}
var child = {
  inject: {user} 
  // {user: user},the first user is for property name, second is token value
}

Context visibility is fully controlled by token. If parent component does not expose token, context is not visible to any child. Another helper method $inject can let children decide to inject only closest context or inject contexts from ancestors, all the way up to $root.

I agree with @posva that context is very delicate and can be replaced by passing props. Including it by default promotes bad practice to beginner. If advanced users do need it in library code, the implementation overhead is just few lines of code.

@sebastiandedeyne
Copy link
Author

I have my doubts about the token. While I understand that it's useful to avoid conflicts, it's an object that needs to be passed around throughout components—which is exactly what we're trying to avoid.

What would this look like assuming components live in separate files (so you can't just declare the token variable on top, but need to pass it around somehow)?

Or is the token not required?

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Nov 15, 2016

Because token is just a primitive value (symbol in ES6 environment or string otherwise), it can be declared outside of component and imported. So only component used the context feature will need to import token. Intermediate components will be agnostic about token and context. No need to passing around token.

For example, the mitm, man in the middle component, does not refer to User token, but child component can still get user info by importing user token.

A Token itself is globally unique, but context object retrieved by the token is component specific and stored only in relevant components.

@sebastiandedeyne
Copy link
Author

Makes sense. I still believe strings are good enough in most situations though, but being compatible with symbols is a good addition for those that want something more 'strict'.

I'm working on a package for this to use throughout my own projects (will be open sourced too), but I'd still prefer to see something official for this, either in the core or as an official package :)

@vinicius73
Copy link

I think it's an excellent proposal. Of course something like this should be used with caution.
I believe that if they deem it necessary this can be an additional package, not part of the core of the vue

@yurimorini
Copy link

In addition to the example by @ktsn this approach can be useful to act similar to a Service Locator to decouple components from services and avoid to "require()" a service instance (as I read as solution in some examples)

import fetcher from "Fetcher" 

// I want to configure the Fetcher or use a fetcher "interface"
// I want to avoid to mock the require in testing

const app = new Vue({
  methods: {
    useService: function() {
      fetcher.fetch()
    }
  }
}); 

Using the approach from @HerringtonDarkholme I actually solved the issue with this proof of concept implementation

// app .js   
const app = new Vue({
  el: config.el,

  // declares services in a component
  // or at root level
  services: {
    fetcher: Fetcher(config.endpoints),
    dispatcher: channel("app")
  }
});

// component.js
export default {
  template: template,

  // inject only the needed service in a property
  // or in a $services object visible only from the component
  // asking for the service id or other valuable mechanism
  inject: {
    disp: 'dispatcher'
  },
  
  methods: {
    dispatch: function() {
      this.disp.publish("commands", {});
    }
  }
}

Beside the implementation as a plugin, which uses an awful global container, defining the services as a key value pairs remind me the data props definition and open the more complex features (like querying for a feature or interface).

@sebastiandedeyne
Copy link
Author

In case anyone's interested, in the mean time I created a package based on what has been discussed for our own projects: https://github.com/spatie/vue-expose-inject

@hector-humberto
Copy link

I support this feature request. Definitely something I could have used on a past project. Something native to Vue would be really cool.

@jaredramirez
Copy link

+1

@OEvgeny
Copy link

OEvgeny commented Feb 4, 2017

Since it is doable in user land, I think there is no need to do it in core. See comment abowe for additional information.

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Feb 13, 2017

I started to think this feature should be desirably supported by official. (Either builtin or by official plugin).

The usage is quite different from those mentioned above.

In testing, I want to replace some dependencies imported from other files. Now the vue-loader official doc recommends to use inject-loader. However, it depends on specific build system like webpack. And inject-loader is quite of black magic. If context feature is supported, it can act like a dependency injection system, naturally. And in test we can inject mock into component easily and universally, without hacking build system.

Another issue is discovered in SSR. Currently bundled-render uses vm module to evaluate code in sandbox. This might be performance bottleneck. On the other hand, we cannot use global state in non-bundled render. Context feature can be the recommended way to manage global state. New state can be injected for every new request.

@OEvgeny
Copy link

OEvgeny commented Feb 13, 2017

As for me Angulars' DI is very flexible but complicated. On the other hand Reacts' context is more simple to use. If this feature will be implemented, I would expect to have the best from two worlds in Vue. But I still think that we don't need something simple enough which can be implemented in user land.

@sebastiandedeyne
Copy link
Author

It may be simple to implement in user land, but the main reason I want this in core or an official package is so we have a standardised solution.

@indirectlylit
Copy link

FYI: this can now be accomplished using provide / inject

@trusktr
Copy link

trusktr commented Jul 23, 2018

Whatever solution is arrived at, please please, avoid matching contexts by string name. Instead, match context by reference. This prevents naming collisions/conflicts, and is one major thing React fixed from their first context implementation to their second.

For reference, here's my small context implementation for SkateJS (but fairly generic and usable anywhere outside SkateJS): https://codepen.io/trusktr/project/editor/XbEOMk

The app.js is the important file containing the sample usage. The main thing to note is the static provides and consumes variables (named similarly to the React context API Provider and Consumer). This is similar to @sebastiandedeyne's expose/inject idea.

Notice in particular that the providers are passed by reference. This makes them entirely unique and clash free (like with React's new Contexts).

In my sample, contexts are not used declaratively like they are in React's JSX version.

In Vue, because it compiles to references, it could be made to be declarative, though personally I don't like having to declare contexts in markup as we will be viewing the JavaScript already anyways, and don't benefit much from the markup in my opinion.

In my sample, everything is in the JavaScript code, similar to @sebastiandedeyne's idea. Notice also that to we get a specific context by reference: this.contexts.get(SomeContext).foo.


Avoid strings. When you start putting contexts in the middle of a large app and there's a name conflict, it will be a headache, especially if the props inside the context are different. References in React JSX completely get around this problem 100%.

@trusktr
Copy link

trusktr commented Jul 23, 2018

Ah shoot, I see Vue already made the mistake with provide/inject. I guess it's easy to use, but let's see what happens in big apps...

@trusktr
Copy link

trusktr commented Jul 23, 2018

Another thing to note, in my SkateJS implementation, components react to specific props, rather than reacting to a context as a whole value.

This is nice because it prevents unnecessary re-renders of components.

Some components may only care about a subset of props of a context, and in my implementation only components subscribed to those props will update. This prevents needing a shouldUpdate method in my example (equivalent of shouldComponentUpdate in React).

By doing prop-based subscription, you can also prevent from creating new Objects every update. This is especially good for rapidly updating components (f.e. during animations that are intended to be 60fps), so that we don't have running memory growth causing garbage collection and jank.

In my implementation sample, the context object never changes, so memory use can stay constant during animations if the props are all primitive values.

It'd be great for provide/inject to be reference-based, and for components to be able to subscribe to specific properties.


In React apps that I've seen, contexts are usually Objects, not primitive values. People end up recreating whole new objects just to trigger updates and these updates update all components that don't even care about all changed props.

This is unnecessary CPU/Memory waste.

@leoyli
Copy link

leoyli commented Apr 29, 2019

I have published an NPM package for abstracting the provide/inject into createContext in Vue (thanks v-slot directive in v2.6), which can be considered as a React-like sugar, but I think such API design is indeed much intuitive and easier to reuse. Not sure how it would be involved in V3, maybe the advanced reactivity observer API may make provide/inject obsolete. Anyway, please have a look in my repo:

https://github.com/leoyli/vue-create-context

@mrabaev48
Copy link

mrabaev48 commented May 27, 2021

I have published an NPM package for abstracting the provide/inject into createContext in Vue (thanks v-slot directive in v2.6), which can be considered as a React-like sugar, but I think such API design is indeed much intuitive and easier to reuse. Not sure how it would be involved in V3, maybe the advanced reactivity observer API may make provide/inject obsolete. Anyway, please have a look in my repo:

https://github.com/leoyli/vue-create-context

Hey, leoyli
I have tried your lib Vue createContext but unfortunately I've got some problems... I can't understand how to use context in children components in my project.

Could you please give me some tips?

I'm using Vue2, Typescript and JSX and trying to use your library.

Thank you and best wishes.

@mrabaev48
Copy link

In case anyone's interested, in the mean time I created a package based on what has been discussed for our own projects: https://github.com/spatie/vue-expose-inject

Hi!
Have some questions about your library.
Could I use it with tsx?

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

Successfully merging a pull request may close this issue.