vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.87k stars 548 forks source link

Amendment proposal to Function-based Component API #63

Closed yyx990803 closed 5 years ago

yyx990803 commented 5 years ago

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:

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:

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:

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:

vberlier commented 5 years ago

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.

tigregalis commented 5 years ago

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.)?

Akryum commented 5 years ago

@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.

yyx990803 commented 5 years ago

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

tigregalis commented 5 years ago

@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.

CyberAP commented 5 years ago

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.

yyx990803 commented 5 years ago

@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.

tigregalis commented 5 years ago

@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?

yyx990803 commented 5 years ago

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

CyberAP commented 5 years ago

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 };
yyx990803 commented 5 years ago

@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.

dealloc commented 5 years ago

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

yyx990803 commented 5 years ago

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

CyberAP commented 5 years ago

@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.

beeplin commented 5 years ago

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.
yyx990803 commented 5 years ago

@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.

Akryum commented 5 years ago

@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"...

smolinari commented 5 years ago

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. :smile:

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. :grin:

Scott

tigregalis commented 5 years ago

@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?

yyx990803 commented 5 years ago

@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.

yyx990803 commented 5 years ago

@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.

beeplin commented 5 years ago

@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.

CyberAP commented 5 years ago

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 }
}
yyx990803 commented 5 years ago

@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.

jacekkarczmarczyk commented 5 years ago

@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,
  ...
}
dealloc commented 5 years ago

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

edit: made wording more neutral

jacekkarczmarczyk commented 5 years ago

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)

liximomo commented 5 years ago

We still need to use .value in these situations:

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,
  };
}
tigregalis commented 5 years ago

@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()).

Akryum commented 5 years ago

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

dealloc commented 5 years ago

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.

tigregalis commented 5 years ago

@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.

tochoromero commented 5 years ago

@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.

tigregalis commented 5 years ago

@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.

backbone87 commented 5 years ago

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

backbone87 commented 5 years ago

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).

luxaritas commented 5 years ago

@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.

beeplin commented 5 years ago

@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.

tigregalis commented 5 years ago

@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.

backbone87 commented 5 years ago

@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,
}
vjoao commented 5 years ago

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.

jonaskuske commented 5 years ago

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:

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

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

dealloc commented 5 years ago

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

jonaskuske commented 5 years ago

@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"
dealloc commented 5 years ago

@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

aztalbot commented 5 years ago

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 ...)

emironov-via commented 5 years ago

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

Aferz commented 5 years ago

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?

skyrpex commented 5 years ago

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>?

Aferz commented 5 years ago

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>