vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Amendment proposal to Function-based Component API

yyx990803 opened this issue · comments

This is a proposal for an amendment to RFC #42. I'm posting it here separately because the original thread is too long, and I want to collect feedback before updating the original RFC with this.

Please focus on discussing this amendment only. Opposition against the original RFC is out of scope for this issue.

Motivation

This update aims to address the following issues:

  1. For beginners, value() is a concept that objectively increases the learning curve compared to 2.x API.
  2. Excessive use of value() in a single-purpose component can be somewhat verbose, and it's easy to forget .value without a linter or type system.
  3. Naming of state() makes it a bit awkward since it feels natural to write const state = ... then accessing stuff as state.xxx.

Proposed Changes

1. Rename APIs:

  • state() -> reactive() (with additional APIs like isReactive and markNonReactive)
  • value() -> binding() (with additional APIs like isBinding and toBindings)

The internal package is also renamed from @vue/observer to @vue/reactivity. The idea behind the rename is that reactive() will be used as the introductory API for creating reactive state, as it aligns more with Vue 2.x current behavior, and doesn't have the annoyances of binding() (previously value()).

With reactive() now being the introductory state API, binding() is conceptually used as a way to retain reactivity when passing state around (hence the rename). These scenarios include when:

  • returning values from computed() or inject(), since they may contain primitive values, so a binding must be used to retain reactivity.
  • returning values from composition functions.
  • exposing values to the template.

2. Conventions regarding reactive vs. binding

To ease the learning curve, introductory examples will use reactive:

setup() {
  const state = reactive({
    count: 0
  })
  
  const double = computed(() => state.count * 2)
  
  function increment() {
    state.count++
  }
  
  return {
    state,
    double,
    increment
  }
}

In the template, the user would have to access the count as {{ state.count }}. This makes the template a bit more verbose, but also a bit more explicit. More importantly, this avoids the problem discussed below.

One might be tempted to do this (I myself posted a wrong example in the comments):

return {
  ...state // loses reactivity due to spread!
}

The spread would disconnect the reactivity, and mutations made to state won't trigger re-render. We should warn very explicitly about this in the docs and provide a linter rule for it.

One may wonder why binding is even needed. It is necessary for the following reasons:

  • computed and inject may return primitive values. They must be wrapped with a binding to retain reactivity.
  • extracted composition functions directly returning a reactive object also faces the problem of "lost reactivity after destructure / spread".

It is recommended to return bindings from composition functions in most cases.

toBindings helper

The toBindings helper takes an object created from reactive(), and returns a plain object where each top-level property of the original reactive object is converted into a binding. This allows us to spread it in the returned object in setup():

setup() {
  const state = reactive({
    count: 0
  })
  
  const double = computed(() => state.count * 2)
  
  function increment() {
    state.count++
  }
  
  return {
    ...toBindings(state), // retains reactivity on mutations made to `state`
    double,
    increment
  }
}

This obviously hinders the UX, but can be useful when:

  • migrating options-based component to function-based API without rewriting the template;
  • advanced use cases where the user knows what he/she is doing.

I think that's a great way to mitigate the confusion around value(). I personally didn't have a problem with value() but introducing this distinction between reactive objects and simple bindings makes a lot of sense.

I was also hesitant about the use of 'value' (because it is also used as the reactive property of a now-binding) and 'state' (because it is very commonly used as a variable name), so I think this is a welcome change. Personally I'm really excited about this API.

Beyond that though, I question why there are separate toBindings and reactive functions. As an alternative, can this simply be a second argument to reactive? i.e.

  const state = reactive({
    count: 0
  }, true) // setting to true wraps each member in a binding and allows the object to be spread and retain reactivity

Is there a use-case where you would expose the whole reactive object as a binding as well as its members? i.e. why might someone do this?

  return {
    state,
    ...toBindings(state)
  }

I can't see the advantage of an extra function other than "just in case".

Another drawback which I've seen raised, which is closely related to this API (i.e. exposing the reactive variables to the render context) is that this is more verbose because of the need to 1) declare the variables and then 2) expose the variables. This is a very small thing, so it's certainly no deal-breaker, but is there a way around this?

I asked this in the other thread actually but it got lost (it relates directly to this API):

A few more questions that aren't clear to me from the RFC:

  1. How "deep" does the reactive() function actually make the object reactive? e.g. is it just one level deep (the immediate members of the object)

  2. Does the reactive() function make an array and/or the members of an array reactive (including push, pop, accessing a member, accessing the property of a member if it were an object, etc.)?

@tigregalis With reactive, your data is already an object (not a primitive), so you don't need xxx.value when using it in the script.

@tigregalis if you directly create an object of bindings, you'd have to access internal values as state.count.value. That defeats the purpose.

@Akryum I'm not sure what you mean, sorry. My comment was more around the ergonomics of spreading and exposing reactive state to the render context.

Wouldn't toBindings completely eliminate the need for binding function and leave us with just reactive?

Also, I personally find it very frustracting that you have to remember which one is which and always keep that in mind when working with reactive values. React has solved this very elegantly with two exports: getter and a setter. I'd much rather have this, then constantly check if I'm working with a binding or with a reactive object.

const [counter, setCounter] = toBinding(0);

const increment = () => setCounter(++counter);

return { counter, increment };

counter in that case is an object with a valueOf property, that is reactive.

@CyberAP

The need for binding() is already mentioned:

  • returning values from computed() or inject(), since they may contain primitive values, so a binding must be used to retain reactivity.
  • returning values from composition functions.
  • exposing values to the template.

In your example, counter is a plain value and cannot retain reactivity when returned. This would only work if the whole setup is invoked on every render - which is exactly what this RFC is avoiding.

@yyx990803 I haven't worked with proxies so excuse my ignorance, but is it possible to forward the get/set of state.count to state.count.value?

@tigregalis spread internally uses get. So when forwarding you break the spread (and thus disconnect the reactivity)

Yes, I've forgotten to add that counter is an object with a valueOf property that provides an actual reactive value. @LinusBorg said that it has been discussed internally but there's been no feedback on that proposal since.

Maybe with a valueOf we can somewhat mitigate the need to use .value to access reactive value?

const counter = binding(0);

console.log(counter);  // uses valueOf()

const increment = () => counter.value++; // as initially proposed

return { counter };

@CyberAP binding may also need to contain non-primitive values so I don't think valueOf would cover all use cases. It's also too implicit - I'm afraid it will lead to more confusions than simplification.

What if this

  return {
    ...toBindings(state), // retains reactivity on mutations made to `state`
    double,
    increment
  }

would automatically be done if you did this:

  return {
    state, // retains reactivity on mutations made to `state`
    double,
    increment
  }

ie. if you directly set state in the object

@dealloc we did discuss that option internally, but overall we want to avoid "magic keys" and be as explicit as possible.

@yyx990803 could you please elaborate more on the non-primitive values in binding? What's the usecase for this when we have reactive? Except for use in an object spread within toBinding helper.

Not a very mature thought, but since const state = reactive({ a: 0}) is going to be more recommended than const state = { a: binding(0) }, how about (and is it possible to do) this:

import { reactive, computed, injected, merge } from 'vue'

setup() {
  const state = reactive({
     a: 0,
     b: 1,
  })

  // computed accepts an object, not function, 
  // so that the returned computedState doesn't need to be wrapped into `computedState.value`
  const computedState = computed({ 
    c: () => state.a + 1,
    d: () => computededState.c + state.b,
  })

  // same for injectedState
  const injectedState = injected({ 
    e: ...
  })

  // { state: {a, b}, computedState: {c, d}, injectedState: {e} }
  return { state, computedState, injectedState } 

  // or

  // merged into { a, b, c, d, e }, still reactive, no .value needed.
  return merge(state, computedState, injectedState) 
}

If this is feasible, I can see two major advantages:

  1. Eliminate the concept of ".value wrapper" at all from the new API. Much simpler.
  2. More like the old object-based API and allowing people to group reactive computed inject things together if they like, and at the same time allowing some other people to call reactive computed multiple times to group logic in features.

@CyberAP as mentioned, anything returned from a computed property / dependency injection has to be a binding to retain reactivity. These bindings may contain any type of values.

@CyberAP Also there is value in using bindigs.

Take those examples:

https://github.com/vuejs/function-api-converter/blob/5ac41d5ad757f1fb23092e33faee12a30608a168/src/components/CodeSandbox.vue#L52-L53
https://github.com/vuejs/function-api-converter/blob/5ac41d5ad757f1fb23092e33faee12a30608a168/src/functions/code.js#L18

It would make such usage more complicated since we would have to pass the entire object to keep reactivity. And also document each time what keys should be used or even provide accessor "callbacks"...

commented

A blind man asking what might be a stupid question......

If the objective is to make both object and primitive (and non-primitive) assignments reactive, couldn't it be just one method for both and have the method....reactive()(???).....type check what is being offered as an argument and do it's reactive magic accordingly? I think the whole idea of data() being split up into two different things is the confusing and seemingly unnecessary addition. 😄

Btw, I love you are trying to make the value and state methods a bit more elegant. Thanks for that!!!

Edit: Oh, and if it is possible, then the toBinding method could be maybe something like stayReactive. Ah, naming is one of the hardest things to do in programming. 😁

Scott

@yyx990803 But if it were possible(?):

The object would be represented by:

// `state`
{
  count: {
    value: 0
  }
}

In the setup() you could do state.count++ because it would effectively be running state.count.value++. After setup, the increment() method would still have a reference to state.

After spreading the state object in the return of the setup(), you break the reactivity of state in the render context, but its member count would still be reactive in the render context because it's internally represented by { value: 0 } and accessed by its value property.

So in the component template, you could still do count++ because Vue would unwrap it into count.value++.

Does any of that sound right?

@beeplin this seems to create more API surface (another category of "options" for the return value of setup()). The user can already do this if desired:

setup() {
  const state = reactive({ ... })
  const computeds = {
    foo: computed(...),
    bar: computed(...)
  }
  const injected = {
     baz: inject(...)
  }
  const methods = {
     qux() {}
  }
  return {
    ...toBindings(state),
    ...computeds,
    ...injected,
    ...methods
  }
}

A merge helper was also considered, but the only thing that really needs special treatment is reactive state. With merge it's less obvious as to why we need to merge objects like this (why can't we just spread? why can't we just use Object.assign?), whereas with toBindings the intention is clearer.

@tigregalis with getter forwarding, when spread into the render context, count is no longer an object. It's just a plain number that no longer has anything to do with the original value wrapper.

Put it another way - the render context only receives a plain number (instead of a "binding", which is trackable via the .value access) so the render process won't be tracking anything.

@yyx990803 Yes I know the 'grouping by type' thing can be done like you said. But more importantly:

  1. Eliminate the concept of ".value wrapper" at all from the new API. Much simpler.

If we make computed() and inject() accept an object rather than a function/bare value, we could just eliminate the need for the 'value wrapper' concept -- every reactive thing must be in an object, and no need to use .value wrapper to keep reactivity when spreading/passing around.

So, no .value, no binding(), no toBinding()... just one more merge().

I don't think I am an expert on proxy or JS reactivity, so I might be wrong.

In that case I'm thinking that reactive is now more confusing, since most of the time we'll be working with bindings, extracting and sharing logic between components. These will always return bindings and they are actually the core of the new reactivity, not the reactive. I understand that for those who migrate from 2.x constantly using .value to get and set values would be irritating, but maybe it's less irritating than getting confused between of those two. The main point of confusion is not that you have to deal with .value, but with choosing between of those two. I can easily imagine lots of questions about why this doesn't work. So maybe getting rid of reactive can solve this?

import { reactive } from 'vue';

export default (counter) => {
  const data = reactive({ counter });
  return { ...data }
}

@beeplin that won't work when you start to extract logic into composition functions. No bindings means you will always be returning objects even in extracted functions - when you merge them you won't see the properties they exposed, and it becomes mixins all over again.

@yyx990803 would it be technically possible (keeping all the reactivity, TS support etc) to create a reactive data sets that contain computeds and methods as well?

const state = reactive({
  count: 1,
  get doubled() {
    return state.count * 2
  },
  increment: () => state.count++
});

return state;
// or
return {
  ...toBindings(state),
  ...injected,
  ...
}

@jacekkarczmarczyk what problem would that solve though? I feel like I'm missing the point

edit: made wording more neutral

For me it seems more logical to group related things in one object instead of declaring separate variables/methods. Such object could be also used outside of the component scope (including tests)

commented

We still need to use .value in these situations:

  • returning values from computed() or inject()
  • returning values from composition functions

People do will excessively use of composition functions, which be equivalent to excessive use of value, which we don't want. toBindings could not help much here, but introduce another fatigue.

setup() {
  const state = reactive({
    count: 1,
  });
  const double = computed(() => state.count * 2)

  return {
    should I use toBindings, both seem to work. help me... ? state : ...toBindings(state),
    double,
  };
}

@yyx990803 Ah, I see what you're saying. Thanks.

Alternative proposal then. Instead of ...toBindings(state), use ...state.toBindings(). It doesn't seem like much, but it's one less import, for a function that only ever does one thing, on only ever one type of argument. I guess the disadvantages are that it's not tree-shakeable (but given how frequently you're likely to use it, how often would it be omitted?) and less minifiable (can't reduce to ...f(x), best case ...x.toBindings()).

@tigregalis Don't think state.toBindings() has any advantages. Also your IDE should auto import it!

I'm still not 100% sold on the ...toBindings(state) story.
I think @yyx990803 might have misunderstood my comment, I did not mean that Vue would automatically unwrap the state key, I meant that Vue would automatically call toBindings for all keys where needed, hiding the implementation detail of toBindings to the Vue internals.

@dealloc that would work, but here's something that you could do:

setup() {
  const primitiveCounter = 0;
  const counter = binding(primitiveCounter);
  const primitiveIncrement = () => primitiveCounter++;
  const increment = () => counter.value++;
  return {
    primitiveCounter, // internally made reactive, exposed to the render context, so you could call `primitiveCounter++`
    counter, // already reactive
    primitiveIncrement // has reference to internal non-reactive primitive value, calling it will update the internal value but not change the reactive value
    increment, // has reference to reactive counter object
  }
}

Could be a source of confusion.

@dealloc we did discuss that option internally, but overall we want to avoid "magic keys" and be as explicit as possible.

@yyx990803 I don't think automatically handling reactive objects directly returned from setup() is any more "magical" than the current data property on 2.x. You are explicitly creating the object with reactive({}) and you are explicitly returning it, that is explicit enough for me and I would expect for it to remain reactive. The fact that Vue has to call ...toBindings(state) under the hood is an implementation detail.

Also, I cannot think of a reason why you would want to return a reactive object from setup() and have it lose its reactivity. But, if there is such a use case, having to call markNonReactive is preferred to calling ...toBindings all the time.

@dealloc

On second reading, those are different ideas...

Your example was

return {
  state
}

But I thought you meant

return {
  ...state
}

You're saying any reactive objects that are returned from setup are automatically spread? I don't quite like that.

I think one of the problems is the logical disconnect between the template and the setup function.

<template><button @click="inc">{{ count }}</button></template>
<script>
export default {
  setup() {
    const counter = useCounter();
    return { ...toBindings(counter) };
  }
}
</script>

what this actually means is something like this:

export default {
  setup() {
    const { count, inc } = useCounter();
    return COMPILE_TEMPLATE_TO_RENDER_FUNCTION(`<button @click="inc">{{ count }}</button>`);
  }
}

the template is a closure inside setup and has access to its local variables.

edit:
and here is what actually is produced:

const render = COMPILE_TEMPLATE_TO_RENDER_FUNCTION(`<button @click="inc">{{ count }}</button>`);
export default {
  setup() {
    const { count, inc } = useCounter();
    return render({ count, inc });
  }
}

i find the closure variant much more appealing

JSX turns HTML like syntax into a series of functions calls.
what we need for vue is a system that turns HTML like syntax into a closure (function).

@jacekkarczmarczyk @dealloc I'm actually in favor of being able to define getters/methods within reactive. I see it (personally) as a sort of semantic middleground between object api and function api (just posted my thoughts on that), since the way it reads is more declarative. I of course have no clue implementation feasibility or if there are other gotchas.

@dealloc

I don't think automatically handling reactive objects directly returned from setup() is any more "magical" than the current data property on 2.x.
I also imagine that in smaller components, tutorials, etc there might only be one anyways (ie, if there's no logical grouping to be done). This is an even closer analogue to 2.x if you can define other things on reactive.

@backbone87 I imagine this should be an optional stylistic thing - per the other stuff I've been responding to, there are some cases where you might just be returning one thing, especially if it's possible to throw everything onto one reactive for a small component.

Makes me wonder if you could define everything on one reactive, whether you could just return a reactive.

@yyx990803

I am concerning that there comes considerably asymmetry among reactive, computed and inject. Conceptually, computed, inject are similar to binding because they all return a binding object with .value. When people need to access them in other parts of setup(), it is consistent to remember adding .value after them. But since you are planning to make reactive the first-class API prior to binding(), it feels somehow strange because people have to remember that they can access state.xxx directly but have to do computedResult.value. (People can access a computed value as frequently as a state value in setup(), like in other computed functions, watch functions and life cycle hooks.)

IMO, it would be better either using .value in all cases, or in none.

That's why I consider if there is a way to require people to always wrap their reactive values (state, computed, inject, all composition functions, etc.) in objects in order to get rid of the puzzling .value binding completely.


The following part might make no sense. ;)

you will always be returning objects even in extracted functions - when you merge them you won't see the properties they exposed, and it becomes mixins all over again.

As far as I know at least in VSCode (both for TS and JS), type inference will be working for functions returning objects.

image

So we may have some users who prefer use binding() to achieve consistency with computed() and inject() (always using .value), while some other users might like to write like this (wrap all in objects and use the objects as namespaces) to avoid .value:

import { computed, createComponent, reactive } from "vue";

function useA() {
  const state = reactive({
    a: 1
  });

  const computedState = computed({
    b: () => state.a + 1
  });

  return { state, computedState }; 
}

const component = createComponent({
  setup() {
    const { state: stateFromA, computedState: computedStateFromA } = useA();

    const state = reactive({
      a: 1
    });

    const computedState = computed({
      b: () => state.a + 1
    });

    return { stateFromA, computedStateFromA, state, computedState };
  }
});

So, I wonder if it is desirable to suppport both styles, by making computed and inject support two kinds of parameters: when passing an object to them, no need to convert it into a .value wrapper; when passing a function/non-object value, convert it into a wrapper.

Again, maybe I am making things complicated too much. In short, I prefer keep consistency when deciding whether to use .value.

EDIT: I am not advocating getting rid of .value by forcing people to wrap all things in objects. In fact I prefer the original proposal before this amendment - making value (binding) the first-class API and let people always use .value.

@backbone87 Is that how that works? I think it's rather that what you return has access to what gets exposed (returned) from the closure and it is sort of a curated API, and the render function has access to that API but not the closure itself. Otherwise, if the render function had direct access to the closure (i.e. was inside the closure), then based on that approach, you wouldn't need to return anything at all from setup really.

@tigregalis from what i know, vue compiles a template to a render function, that receives a context (this) and arguments. it has only access to a few known "global" functions, everything else is passed in.
what i think will be much more benefitting is actually creating a closure from a template inside setup. this most likely requires a build step (like TS).

#42 allows to return render functions from setup, which then has access to the setup scope:

export default {
  setup() {
    const { count, inc } = useCounter();

    return () => h('button', { onclick: inc }, count);
  }
}

you could replace the h call with JSX, (because JSX replaces the markup with h calls).

but a much better DX (imho) would be to convert markup into a closure itself:
this should compile exactly to the code above.

export default {
  setup() {
    const { count, inc } = useCounter();

    return COMPILE_TEMPLATE_TO_CLOSURE_AT_BUILDTIME(`<button @click="inc">{{ count }}</button>`);
  }
}

what happens now is:

const render = COMPILE_TEMPLATE_TO_RENDER_FUNCTION(`<button @click="inc">{{ count }}</button>`);
export default {
  setup() {
    const { count, inc } = useCounter();
    return { count, inc }; // vue passes this to the render function
  },
  render,
}

I personally feel that having to remember to use toBindings will add a cognitive burden which is not on par with Vue's mission. My thought process is similar to this:

I create my state with const state = reactive({...});

Then when I'm about to return it from the setup, I just spread my reactive state with

return {
 ...state
};

Because that just feels right.
Suddenly I get bugs about values not updating because I lost reactivity from something my brain thinks it's reactive because I explicitly told that object to be reactive.

As much as I am quite happy with the funcional RFC, this very issue would be a total blocker for me and I would discard it due to the confusion and possible negative impact it has.

Yeah, as much as I like the function-based API, I agree that this is getting confusing.

Actually, I think that at this point it'd make more sense if there only were value wrappers, so you'd always need .value. (within the JS parts)
This might seem less ergonomic (why do I need the .value?!?1?!), but makes the rule way easier to reason with:

  • Always use .value when accessing something within <script>
  • Never use .value when accessing something within <template>

That rule is very very similar to what we have right now with the current API:

  • Always use this. when accessing something from within <script>
  • Never use this. when accessing something from within <template>

Not really sure about all of this though, feels like there just isn't a perfect solution to this.

I agree with @jonaskuske while it might seem annoying at first having to use .value, it'll be far more consistent which seems better in the long run.
When I was playing around with the functional syntax, the .value didn't even bother me once I got used to it, I was more annoyed that the name state was already taken and I could not assign a variable to it

@dealloc

I was more annoyed that the name state was already taken and I could not assign a variable to it

I mean, that at least is solved by renaming state to reactive, as is proposed in this amendment.

For now, you could already do

import { state as reactive } from "vue-function-api"

@jonaskuske Indeed, though I'm personally not 100% convinced about the new names.
That might just be that I got myself used to value and state, and even if I disagree in the end as you said I could always rename my imports ;)

Besides, I couldn't come up with a better name myself so it doesn't bother me as much

Knowing to use toBindings just for state (even if in one place) might be even less intuitive than remembering .value (I know these are completely different, just comparing). At least if I assign a value wrapper to a const I will get errors when trying to assign to it without .value, and probably clear runtime errors as well if I forget .value when trying to use the value. With toBindings you need to be aware of the helper and understand more about how reactivity works first. Linters can help of course, but it throws no errors otherwise. In any case, it defies what might be expected, so it's actually not a bad place to add some runtime "magic," if possible.

The upside of the helper is that it makes it very clear and explicit what is happening (so if it ends up in the API, I probably wouldn't mind). It's a solution, but it will always feel similar to having to use $vm.set() for a reactivity corner case.


Some rough ideas/questions

  1. Is there no way to make a reactive object spreadable (where it returns wrapped values) by implementing Symbol.iterator? I know with Proxies this is tricky (perhaps not possible without modifying the Proxy prototype globally). Not sure why I thought this would work with object spread ...

  2. Not sure if this would work, but another idea I had was when creating reactive, keep an object with the binding values assigned to some symbol on the object, like Symbol('REACTIVE'). When spread, if the returned object from setup contains that symbol, it takes the values there and spreads them again overwriting the non-reactive values with reactive ones. Downsides: this wouldn't solve returning spread or destructured reactive objects from useXXX function, which could be confusing.

Or if there is no solution there, then does it work to do return Object.assign(data, { ...computed, ...methods }). i.e. more a of a pattern of assigning everything to reactive state and returning that. If that does work, I'm not sure it's any different than using a helper.

The strange thing is that value is straightforward, while state is meant to be ergonomic to use by unwrapping values, but by unwrapping values, state then necessitates a helper to use it the way you'd expect. Almost indicates that it's not worth having state at all. I don't want to advocate for removing state, but if you think about it, the API might actually be less confusing without it. Is there a broader advantage to using state / reactive that I have missed?


Update: the main use case I see for state is actually in creating a Vuex-like store in which case I would write my own helper that exposes everything in the state as readonly computeds. State wouldn’t be exported or spread at any point in that case.

(sorry for the long comment, just trying to think through this new api ...)

EDIT: toBindings() do it for top-level property and can use toBindings() in user recursively function
May be use reactiveDeep() that is recursively converted object to reactive() for reference properties and binding() for primitives properties. And use value for all primitives in setup().
For example:

setup() {
  const state = reactiveDeep({
    count: 0
    subObject: {
      foo: 'bar'
    }
  })
  // where reactiveDeep() is:
  // const state = reactive({
  //   count: binding(0)
  //   subObject: reactive({
  //     foo: binding('bar')
  //   })
  // });
  
  //error:
  const double = computed(() => state.count * 2) // work if return {state}, but does not work if return {...state}
  //success:
  const double = computed(() => state.count.value * 2) // work if return {state} or {...state}

  //error:
  function increment() {
    state.count++  // work if return {state}, but does not work if return {...state}
  }
  //success:
  function increment() {
    state.count.value++ // work if return {state} or {...state}
  }

  return {
    state, // works because reactive() was used for object
    double,
    increment
  }
  //or
  return {
    ...state, // works because binding() or reactive() was used for every property in object
    double,
    increment
  }
  //or
  return {
    state.subObject,
    double,
    increment
  }
  //or
  return {
    ...state.subObject,
    double,
    increment
  }
}

You can replace reactive() with reactiveDeep() and add reactiveLazy() instead of reactive().
Did not find a solution for watch(state, value => {...}) if return {...state}

P.S. Sorry for the bad language and if I do not understand the work of this API and the proxy

I could be missing something but what's the problem with auto-detect we are exposing a reactive object to the render context and let Vue unwrap it to avoid nested structures? Example:

setup () {
    // reactive() function could simply write a read-only flag for internal uses that
    // mark this object as reactive (for example, $$isReactive = true)...
    const state = reactive({
        count: 0
    })

    const plusOne = () => state.count++

    // ... then, Vue internals, can unwrap the state for us an let us use <div>{{ count }}</div>
    // in the template.
    return { state, plusOne }
}

Honest question: why this wouldn't work?

Honest question: why this wouldn't work?

Because it's doing something you didn't ask to. What if you have more state objects? What you're actually pretending to use it as <div>{{ state.count }}</div>?

Well, it's not doing something you didn't ask to. I mean, you are creating a reactive object. Passing that object to render context has its (desired) consequences that IMO you should know. It's not like you don't know what you are doing.

If you would like to pass a non-reactive object, simply don't use the function.

Or, in case you want to pass an object that has reactive properties, you have the binding() function.

const state = {
    count: binding(0)
}

return { state }

This last example would accomplish your last example <div>{{ state.count }}</div>

That seems pretty confusing to me 😕, and would be concerned about the runtime overhead (Vue would need to deeply traverse all the objects to search for bindings).

Edit: I think that your solution should be implemented in user land!

I don't see why Vue should deep scan all the objects. It should only scan the first level because I don't expect Vue to unwrap for me values that I've declared explicitly as non-reactive (The case you proposed above, for example), so if Vue doesn't detect is reactive (via flag, in this case), just will move on to the next item in the object returned.

I don't see why Vue should deep scan all the objects.

Agree, although still looks very confusing behavior to me. Also, not sure how could I return a reactive object without it being expanded into the context.

There's still a problem that you didn't address: what about name clashing if multiple state objects are returned? That problem outweights the necessity to use the toBindings helper, IMHO.

Yeah, as much as I like the function-based API, I agree that this is getting confusing.

Actually, I think that at this point it'd make more sense if there only were value wrappers, so you'd always need .value. (within the JS parts)
This might seem less ergonomic (why do I need the .value?!?1?!), but makes the rule way easier to reason with:

  • Always use .value when accessing something within <script>
  • Never use .value when accessing something within <template>

That rule is very very similar to what we have right now with the current API:

  • Always use this. when accessing something from within <script>
  • Never use this. when accessing something from within <template>

Not really sure about all of this though, feels like there just isn't a perfect solution to this.

I think @jonaskuske makes a good point. A lot of the opposition to the original RFC was the need for .value though, so that's a mark against this approach, but I think a big part of that opposition was that there were two rules and two APIs with their own individual limitations that need to be managed in different ways:

  1. reactive makes the object reactive so that you can use state.count++ in the setup, and in the template if you return it directly from setup, which is convenient. But you can't spread it from the return of the setup, unless you use a helper function, which just (as I understand it) wraps each member in a binding anyway.

  2. binding wraps a primitive value so that you have to use count.value++, but you can return it directly from setup and use it as count++ in the template.

I think it does make sense to have a single function, reactive, which, if passed an object, wraps the members and exposes a .value property on each member, and if passed a primitive, wraps it and exposes a .value property, unifying the API.

So this:

<template>
  <div>
    <span>count is {{ state.count }}</span>
    <span>double is {{ double }}</span>
    <button @click="increment">count++</button>
    <button @click="state.count++">inline count++</button>
  </div>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0
    })
    
    const double = computed(() => state.count * 2)
    
    function increment() {
      state.count++
    }
    
    return {
      state,
      double,
      increment
    }
  }
}
</script>

Becomes this (expose the state object directly):

<template>
  <div>
    <span>count is {{ state.count }}</span>
    <span>double is {{ double }}</span>
    <button @click="increment">count++</button>
    <button @click="state.count++">inline count++</button>
  </div>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0
    })
    
//  const double = computed(() => state.count * 2)
    const double = computed(() => state.count.value * 2)
    
    function increment() {
//    state.count++
      state.count.value++
    }
    
    return {
      state,
      double,
      increment
    }
  }
}
</script>

Or this (spread the state object):

<template>
  <div>
    <span>count is {{ count }}</span>
    <span>double is {{ double }}</span>
    <button @click="increment">count++</button>
<!--<button @click="state.count++">inline count++</button>-->
    <button @click="count++">inline count++</button>
  </div>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0
    })
    
//  const double = computed(() => state.count * 2)
    const double = computed(() => state.count.value * 2)
    
    function increment() {
//    state.count++
      state.count.value++
    }
    
    return {
//    state,
      ...state,
      double,
      increment
    }
  }
}
</script>

Or this (work with the wrapped primitive only):

<template>
  <div>
    <span>count is {{ count }}</span>
    <span>double is {{ double }}</span>
    <button @click="increment">count++</button>
<!--<button @click="state.count++">inline count++</button>-->
    <button @click="count++">inline count++</button>
  </div>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
//    const state = reactive({
//      count: 0
//    })
    const count = reactive(0)

//    const double = computed(() => state.count * 2)
    const double = computed(() => count.value * 2)

    function increment() {
//      state.count++
        count.value++
    }

    return {
//    state,
      count,
      double,
      increment
    }
  }
}
</script>

Perhaps because of how often you'd use .value in this new world, can it be shortened to .val or .v? Perhaps this could even be a config option set up at the component or app level, e.g.

export default {
  setup() { ... },
  valueKey: 'v' // default `.value`
}

How about adding a custom iterator to the object returned from reactive (by automatically adding the Symbol.iterator key to it), so that spreading it directly would automatically convert it to bindings?

This does have the caveat of making for..of loops deal with bindings instead of primitives, but I believe it's a worthy trade off.

I thought about this very thing when I first saw the RFC… but will that work with object spread? I didn't think it would.

@thecrypticace Yeah, you're right that it doesn't work with object spread.

This is actually pretty easily solved with a Babel plugin that auto detects a state object being spread and implicitly adds the helper, but then we are getting into Svelte territory and there would be a split between compiled vs. non-compiled code... 😅

@JosephSilber

Object rest spread is internally executed as an ownKeys + a bunch of get operations. It does not go through the iterator.

A more complete thought on using symbols as flag to indicate whether to spread reactive bindings. If it doesn't make sense, disregard ... I just really want it to be spreadable :)

import { state as s, computed as c, onMounted, onUnmounted } from "vue-function-api";

const toBindings = s => {
  return s;
}; // imaginary helper

const spreadableSymbols = []; // to prevent clashes between multiple states

const isSpreadable = () => {
  const symbol = Symbol("IS_SPREADABLE");
  spreadableSymbols.push(symbol); // register symbol
  return symbol;
};

function reactive(target) {
  const _target = s(target);
  Object.defineProperty(_target, isSpreadable(), {
    get() {
      // returns reactive bindings
      return () => toBindings(_target);
    },
    enumerable: true
  });
  return _target;
}

// called insternally on returned object from setup
// for any objects assigned to a spreadable symbol
// spread that object over the target
function spreadBindings(target) {
  for (const symbol of spreadableSymbols) {
    if (symbol in target) {
      target = { ...target, ...target[symbol]() };
    }
  }
  return target;
}

function useMouse() {
  const position = reactive({ x: 0, y: 0 })
  const update = e => {
    position.x = e.pageX
    position.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return position // destructuring will not be reactive without toBindings but spreading into setup will
}

export default {
  name: "App",
  setup() {
    // const count = useCount(0);
    const data = reactive({ count: 0 });
    const read = c(() => data.count + 5);
    const increment = () => data.count++;

    return {
      // isSpreadable is assigned to the object with product of toBindings
      // and internally, `spreadBindings` is called on the returned object from setup
      ...data,
      ...useMouse,
      read,
      increment
    };
  }
};

@aztalbot the fundamental difficulty here is that there is no reliable way to detect the intention of the code at runtime:

const state = reactive({
  foo: 123
})

// 1. state may be spread in other situations
// here we do NOT want to call toBindings
function doSomething() {
  callAPI({ ...state, id })
}

// 2. here we do want toBindings
return {
  ...state
}

In the above example, runtime code won't be able to tell (1) apart from (2).

@yyx990803 This is true. Good point. IF the solution I outlined were feasible, and instead of returning the result of toBindings from the symbol getter, we return a callback that resolves to the result of toBindings, we could resolve those callbacks in a spreadBindings helper, and in other cases like callAPI example it wouldn't really make a difference (would it?). I edited my comment to reflect this.

A short summary of this thread so far:

  • Most of us seem to be in favor of the renaming
  • Some prefer unifying the concept (.value everywhere, or no .value at all). Unfortunately both seems difficult with technical constraints.
  • There is technical constraints to make direct spreading "just work", mostly due to spreading can happen not just when returning
  • ...toBindings is not ideal

One possible solution I did not mention is allowing setup() to return an array:

setup() {
  const state = reactive({ ... })
  
  function foo() {}
  const bar = computed(() => ...)

  return [
    state,
    {
      foo,
      bar
    }
  ]
}

The array gets merged and exposed on the render context (just like the merge function suggested by @beeplin) - this behavior is also pretty similar to how array of objects passed to class/style bindings are merged.

The downside of this approach is that, again, the user needs to understand why they need to do this instead of just spreading, since with this API the intention is less explicit compared to ...toBindings.

commented

but then we are getting into Svelte territory and there would be a split between compiled vs. non-compiled code...

Is that a bad thing? Really? 😃

If a compilation step is required to make this the most elegant, coolest and accepted RFC as possible, then how about the idea of making what we are discussing here a caveat of the UMD version? Or, just don't allow function-based components in the UMD version at all?

Asked another way, how many advanced programmers are using the UMD version and also needing this kind of extensive code composition capability and TS typing?

As I understood it, the whole idea of the UMD version is to get beginners to learn Vue and legacy web applications owners to incrementally add Vue to their projects. If they want or should go to the next best step, then they should end up with the full blown Vue CLI later anyway, hopefully, right?

Sure, jsfiddle and codepen couldn't directly support the more "elegant" way or even offer function-based components, depending on your decision. And that maybe a small issue for teaching and offering examples of code for the function-based component approach. But, there is a site, codesandbox, which can offer compilation for those teachings and example code. 😄

In the name of "this is very advanced Vue programming", I think that is a fair trade-off.

Just an idea. 😁

Scott

@smolinari requiring a compilation step would break Vue's progressive nature (i.e. being able to use it without a build step). The UMD build still supports a lot of legit use cases and it wouldn't make sense to not support the function API in it.

@yyx990803 I still want to address the conceptual consistency thing:

Some prefer unifying the concept (.value everywhere, or no .value at all). Unfortunately both seems difficult with technical constraints.

Suppose we don't provide reactive()/state(), only binding()/value(), letting people always use .value for state/computed/injected variables, wouldn't that just solve the how-to-spread-state-when-returning problem? Am I missing something?

@beeplin

Suppose we don't provide reactive()/state(), only binding()/value(), letting people always use .value for state/computed/injected variables, wouldn't that just solve the how-to-spread-state-when-returning problem? Am I missing something?

Yes. But IMO it will be a tough sell from the adoption perspective.

is there any way we can prevent using reactive()/state() and binding()/value() alongside each other?
That would enforce either always using .value or nowhere, no?

Just a wild idea, but what if we bind the context of setup() to an empty object, and instead of returning an object you just assign your values to the this inside setup?
So instead of this:

setup() {
  const state = reactive({
    count: 0
  })
  
  const double = computed(() => state.count * 2)
  
  function increment() {
    state.count++
  }
  
  return {
    state,
    double,
    increment
  }
}

You'd end up with something like this:

// Setup is automatically bound to {}
setup() {
  this.state = reactive({
    count: 0
  })
  
  this.double = computed(() => this.state.count * 2)
  
  this.increment = () => {
    this.state.count++
  }
}

Assigning to this would work the exact same as returning.

edit: I suppose that for UMD bundles when you can't use lambda's this syntax would not be as clean though, as you'd end up sprinkling .bind(this) everywhere (unless Vue would automatically bind functions but that could have annoying side effects)

commented

requiring a compilation step would break Vue's progressive nature (i.e. being able to use it without a build step). The UMD build still supports a lot of legit use cases and it wouldn't make sense to not support the function API in it.

Ok. I understand. Although progressive means to me, going from simple to advanced. But, if there are use cases, ok.

Nonetheless, how about making what we are discussing here caveats of the UMD version? The function-based APIs would still be there to be used in the UMD version, albeit somewhat "uglier" than with the CLI??? <- big question marks!!! 😁

Scott

I might be saying nonsense here, but what if reactive returned a closure that when called converts its elements to bindings?

const state = reactive({
  count: 0
});

// here we indicate that state should not be converted to bindings 
callAPI({...state(false)}); 

return {
  ...state()
}

Trying to spread state without calling it would give you a runtime error, right?

What I'm inclined to do now:

  • Use reactive only in introductory documentation, return { state } directly, and access properties as state.xxx in templates.
  • Only introduce binding() when explaining computed properties.
  • toBindings is still provided, but only for those who understand what it's for and think they need it
  • A linter rule that prevents user from spreading state objects when returning from setup()
  • General advice: if you use reactive(), nest it as state, don't spread it. Otherwise use bindings.

@yyx990803 while I understand the reasoning from move from state() to reactive() I'm not 100% convinced about the name.
If it's usually intended to create a state object (that seems to be the example I recurring all the time), wouldn't it be clearer to call it createState() or similiar? (I liked state(), but can see the naming collision being annoying).
This should especially make initial adoption easier for new users who don't fully grasp the concept of "reactive" yet, but will probably know intuitively what state is

If reactive properties in the template will be prefixed with state., then for the sake of consistency I might set up my components so that for example all methods are prefixed with methods. and all computed properties are prefixed with computed..

Will it be possible to set up a state object which is initially empty, e.g. const state = reactive({}) and then add properties to it, e.g. state.message = 'hello' throughout the setup function?

If so, I might organise my components like this:

<div>
    <input type="text" v-model="state.forename" />
    <input type="text" v-model="state.surname" />
    Hello {{ computed.fullName }}
    <button v-on:click="methods.showName">Show name</button>
    Age is {{ state.age }}
    <button v-on:click="methods.showAge">Show age</button>
</div>
import { createComponent, reactive, computed as c } from 'vue'

export default createComponent({
    setup() {
        const state = reactive({}), methods = {}, computed = {};

        // Name
        state.forename = 'john',
        state.surname = 'smith'
        computed.fullName = c(() => state.forename + " " + state.surname);
        methods.showName = function() { alert(computed.fullName.value); }

        // Age
        state.age = "30";
        methods.showAge = function() { alert(state.age) }

        return {
            state,
            computed,
            methods
        }
    }
});

Advantages:

  • Only have to return 3 things from setup(), so the return statement isn't a huge list repeating everything that has come before it.
  • Each line in setup is prefixed with the type, e.g. methods.. This (possibly?) is easier to scan because it's more similar to the Vue 2 idoim of having the type on the left, compared idiomatic Vue 3 code where most lines begin with const.
  • Still able to keep related code together, e.g. in the example above there's a section relating to "Name" and another section relating to "Age". This wouldn't be possible if for example all the methods were wrapped in a methods object, all the computeds were wrapped in computed object, etc.

Edit: If you don't like the idea of prefixing all the methods with methods. in the template, then you can still use the above approach, but spread ...methods in the return statement.

Edit 2: Disadvantage - this doesn't work well with TypeScript:

image

@benrwb

Will it be possible to set up a state object which is initially empty, e.g. const state = reactive({}) and then add properties to it

That depends on the reactivity system. With the current system it's not possible, as Vue has to manually add getter and setter functions for all of the properties, and can only do that if it knows about the properties beforehand. So you need to use Vue.set(obj, 'property', value) to make it work.

With the new, Proxy-based reactivity system it should be possible, as the entire Object is watched, not the individual properties. So it'd work, but only if you don't need to support legacy browsers, as they can't run the new system built on Proxies.

@yyx990803

I think this amendment is the best proposal so far. It is not too hard to remember using .value for bindings and toBindings for spreading. I think the thing that bugs me most in the past few days is that, it is not obvious which variable is what, especially when using "custom hooks".

setup() {
  const mousePos = useMouse(); // (a)
  const { x, y } = useMouse(); // (b)
}

It is hard to tell if mousePos a reactive object (in which case (b) won't work) or an object with x and y bindings (in which case (a) would require us doing mousePos.x.value). It is not a good DX if the developer needs to dig into the source/docs just to find out whether something is a value wrapper or reactive object.

The same problem do not exist in React since their values are just values and setters are usually prepended with set, which makes things very clear.

It might be nice if there is an official naming convention which help distinguish between reactive object and value wrapper. E.g. prepend with Binding for value wrapper or something more robust.

The same should apply to "custom hooks". We need to be able to distinguish "custom hooks" from "just functions". Since custom hooks are not supposed to be used outside of setup(). (React uses the prefix use?)

When you call a function, don't you inherently need to know what it will return?

I don't think I'd like to prepend all my bindings with Binding, as that just adds cognitive overhead for me without any real return. Especially when using Typescript (though I'm aware there's plenty people here not using Typescript / linting)

As for the naming convention about hooks, I do agree that an official naming convention here would be useful to distinguish between "regular" functions and hooks. Maybe the use prefix of React isn't such a bad idea (though we'll get people complaining it "looks like react" as if it's not inspired by React in the first place)

@yyx990803

Use reactive only in introductory documentation, return { state } directly, and access properties as state.xxx in templates.

I know this is only a documentation thing, but have you considered the general case? i.e. only support state/reactive?
i think having classes or objects as return values would be much more sensitive, because it actually encapsulates functionality. benefits: we can use this internally, no need for reactive wrappers. negatives: tempting to apply inheritance over composition inside the data layer

@State
class Mouse {
  public constructor(
   public x = 0,
   public y = 0,
  ) {
   // register lifycycle
  }
  
}

have you considered the "template to function" vs "template to closure" thing? #63 (comment)

have you experimented with setup as a generator?

function* setup() {
  const mouse = useMouse();
  yield* mouse;

  const acceleration = useAcceleration(mouse);
  yield* acceleration;
}

@ycmjason

Prefixing binding names makes it awkward when you are returning them:

return {
  foo: fooBinding
}

On the other hand, I have no issue with the useXXX convention. I think that makes sense.

So:

  • only recommend using reactive directly inside setup()
  • when you are extracting logic into a composition function:
    • always name the function as useXXX
    • always return a binding, or an object of bindings.

I think this would make the cognitive overhead minimal: you only deal with plain reactive objects directly in setup(). When you use a computed or injected property, or whenever you see a function named useXXX, they always return bindings and never plain reactive objects, so you can just pass them around or return them in setup().

@backbone87

You sure can use class with reactive() for logic encapsulation - but should be a userland pattern. As mentioned, requiring everything to be an object brings along the same issue of mixins: template property sources are unclear since it's multiple objects being merged together.

Re generators: I think that would make the entry barrier way too high. Very few beginners even know how generators work.

You sure can use class with reactive() for logic encapsulation - but should be a userland pattern. As mentioned, requiring everything to be an object brings along the same issue of mixins: template property sources are unclear since it's multiple objects being merged together.

@yyx990803 I don't agree with this. You don't need to merge different states (moreover, you've written "don't spread it"), so there would be no confusion where does the property come from (cursor.x, photo.x, cursor.moveTo(10), photo.moveTo(10)).
Also I don't see a userland pattern here, all we need is reactive class props and cached getters. If we can achieve that using recactive(new SomeClass()) then it would solve a lot of commons use cases.

EDIT: even the quite big example provided by @Akryum (https://gist.github.com/Akryum/05964e81d09fb5088b7769cff15f5e7c) separates (with maybe 1 or 2 exceptions) just data and the exceptions where he's mixing data and view layer (refs.pathInput.focus()) sound more a like an antipattern for me

@yyx990803

yeah @jacekkarczmarczyk explained what i meant: dont spread. one can apply normal OO patterns (composition, delegation, ...). there can still be the other "reactive particles", but i really dont see the need for it.

Do you think that an ESLint plugin could detect if someone is spreading a state created using reactive? That would indeed help a lot. If that's possible, I wouldn't mind at all having to use toBindings to spread a reactive state.

I've been thinking about this, but what would toBindings actually do?

Say you had defined a reactive object,

const state = reactive({
  count: 0
})

Then you defined a method to update a member of it:

const increment = () => state.count++

When returning from the setup, you wrap state in toBindings, then spread it and also return the method:

return {
  ...toBindings(state),
  increment
}

Your template looks something like

<button @click="increment">{{ count }}</button>

You clicked this button once, calling the increment method: Internally, state.count would increase to 1.

However, toBindings effectively turned state into

{
  count: {
    value: 0
  }
}

which means if I understand this correctly, that the render context is aware of the object count and its member count.value.

How does the render context know what state and state.count is? How does it know that state.count and count.value are the same thing and should hold the same value? How would the {{ count }} within the vDOM update?

@tigregalis think of count as a pointer and count.value as dereferencing the pointer.

@tigregalis think of count as a pointer and count.value as dereferencing the pointer.

*count

🤣

@yyx990803

  • always name the function as useXXX

Should the official "hooks" be prepended with use then? if we decide to set the convention of prepending use before "hooks"?

Should the official "hooks" be prepended with use then? if we decide to set the convention of prepending use before "hooks"?

@ycmjason the official hooks will be used very frequently and I think making their names short is important.

That's been said, I do think we should unify name styles for similar hooks. For example: reactive, computed and injected (instead of inject), all in adjective terms.

Maybe interesting to post some of the examples in the original RFC with the modifications proposed here and in the comments.
It's easy to lose track at this point

What I'm inclined to do now:

  • Use reactive only in introductory documentation, return { state } directly, and access properties as state.xxx in templates.
  • Only introduce binding() when explaining computed properties.
  • toBindings is still provided, but only for those who understand what it's for and think they need it
  • A linter rule that prevents user from spreading state objects when returning from setup()
  • General advice: if you use reactive(), nest it as state, don't spread it. Otherwise use bindings.

Ultimately, I think this is generally the right approach. It's clear, explicit, and makes it easier and less confusing to learn, in my opinion. We could also have a lint rule that warns if you ever return non-bindings from a useXXX function (you can also return a plain reactive object but can't spread it when returning from useXXX).

I don't actually mind that this API seems intentionally less "magical." I think it's a good thing. When teaching people, following your approach will probably work well and lead to a better understanding of the reactivity API in the end.

Only main downside is that the names are long and I anticipate seeing a lot of import { reactive as r, computed as c, binding as b } from 'vue'. I find the names clear, though, and that's probably more important.

commented

@beeplin

That's been said, I do think we should unify name styles for similar hooks. For example: reactive, computed and injected (instead of inject), all in adjective terms.

Sounds good to me, but what about binding(). Ummm......would it be...... bound()???

export default {
  setup() {
    // reactive state
    const count = bound(0)

Hmm..... 🤔

Scott

Sounds good to me, but what about binding(). Ummm......would it be...... bound()???

techincally binding is an adjective (as in the contract is binding), but I don't think we are using it that way 🤔

What about this as a (slightly more advanced) pattern (similar to what was suggested in the other thread here #42 (comment)):

setup() {
  const state = reactive({
    count: 0
  })
  
  const double = computed(() => state.count * 2)
  
  function increment() {
    state.count++
  }
  
  return {
    // cannot modify state from the template
   // won't work with v-model, in that case use toBindings
    ...toReadonly(state),
    double,
    increment // force all mutations to go through methods
  }
}

And then for a simplistic Vuex-like store:

/**
 * mutation helper wraps a function to notify any subscribers as
 * soon as the mutation is complete
 */
function mutation(name, cb) {
  return new Proxy(cb, {
    async apply(target, _, args) {
      let res = target(...args);
      if (res instanceof Promise) {
        res = await res; // wait on result if it’s an async mutation
      }
      notify(name, args); // notify anything subscribing to the store with the name and payload of the mutation
      return res;
    }
  });
}

const store = reactive({
  count: 0,
  user: null
})

export const getters = {
  ...toReadonly(store),
  doubled: computed(() => store.count * 2)
}

export const mutations = {
  increment: mutation('INCREMENT', () => { store.count++ }),
  saveCount: mutation('SAVE_COUNT', async () => { 
     const res = await callAPIForUser() 
     store.user = res.user
 })
}

Or hooks-styled store

const store = reactive({
  somethingShared: [1,2,3,4,5,6,7, 8, 9]
})

export function useSharedPrimes() {
  const primes = computed(() => store.somethingShared.filter(x => isPrime(x))
  const addPrime = mutation('ADD_PRIME', val => {
    if (isPrime) store.somethingShared.push(val)
  })
  return { primes, addPrime }
}

export function useSharedEvens() {
  const evens = computed(() => store.somethingShared.filter(x => isEven(x))
  const addEven = mutation('ADD_EVEN', val => {
    if (isEven) store.somethingShared.push(val)
  })
  return { evens, addEven }
}

export function useSharedStore() {
  return toReadonly(store)
}

This is kind of how I envisioned I would do centralized state management with this API. I could be way off, though.

@aztalbot there is a built-in API called immutable which does (I think) what you want. But now that I think about it, readonly seems more accurate.

commented

On the naming.....I like reactive more and more listening to the conversation here and reading the code examples being presented. In my mind, it's short for "make it reactive" or "make object Foo reactive and assign it to variable Bar". But, binding isn't jiving the same way. "Make it binding"???....hmm.....

There's......bind(). "Bind value Foo (too make it reactive) and assign it to variable Bar".... . 🤔

export default {
  setup() {
    // reactive state
    const count = bind(0)

Reading it "bind '0' (to make it reactive) and assign to count ". I think that's more in line with what's happening? And toBindings will still fit. Oh, and it's 3 characters shorter too. 😁

🤔

Edit: I have a feeling the answer is going to be, bind() is already reserverd/taken......

Scott

@smolinari in the ECMAScript spec, "binding" is used as the term for something that binds a name to an underlying value. In particular, module imports create "bindings":

// a.js
export let a = 1

a = 2
// b.js
import { a } from './a.js'

console.log(a) // 2

Although a contains a primitive type, when imported into another file it is imported as a binding that points to its value in the original file. So mutating a inside a.js reflects in b.js.

I think this resembles the usage of these object wrappers we are talking about pretty well. (Another obvious term choice is "reference" as "refs", but that has been used for another purpose already)

commented

@yyx990803 - Granted. And thanks for taking the time to reply.

I'm certain other people like me, I'd even venture to say most people, aren't correlating ECMA script specs and how bindings are mentioned to what is happening in Vue. That mental picture isn't there. And to be honest, knowing it is there isn't helping my mental picture. And it's the "mental picture" of the options object API that has made Vue great. That needs to continue and I'm very much trying to help with that.

So, back to my mental picture. My thinking of "binding" and "reactive" is, they are making something reactive. I see it as something that needs to happen and not necessary an action that already has happend or is already a thing. And, from my learning, methods should be verbs or express action in programming in general.

Although "reactive" isn't a verb, but thinking "make it reactive" is an action that needs to happen. Vue, make it reactive! 😄 Theoretically, it should be something more like "setReactive". And yes, "binding" is also a verb, but it is the present continues tense of the verb, which doesn't really jive for method naming. Methods do something and finish (don't continue...forever....), so verbs should be, and in almost all cases are, future tense.

In the end, because "make it reactive" is the clearest expression and offers the best mental picture to me and, I believe, for the readers of my code later, I'll more than likely use it all the time, despite it being a bit more verbose. So, no worries.

Thanks again for replying and for Vue!!! I probably would have never gotten so close to front-end development without it. 😁

Scott

@smolinari How about thinking of it as "make it a binding (for this value)"?

That's exactly how I map the binding meaning in my mind: "create a binding between this value and this variable"

It mentaly works, at least with me :)

How about wrap or wrapped? It might not explain the purpose of the wrapped value, but it does actually help with understanding that the value is being "wrapped" by a reactive object, which is why you have to access it using .value.