Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amendment proposal to Function-based Component API #63

Closed
yyx990803 opened this issue Jun 25, 2019 · 175 comments

Comments

@yyx990803
Copy link
Member

commented Jun 25, 2019

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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

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

@CyberAP

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

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

@CyberAP

This comment has been minimized.

Copy link

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link
Member

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

@tigregalis

This comment has been minimized.

Copy link

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

@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

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

edit: made wording more neutral

@jacekkarczmarczyk

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

This comment has been minimized.

Copy link
Member

commented Jun 25, 2019

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,
  };
}
@tigregalis

This comment has been minimized.

Copy link

commented Jun 25, 2019

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

@aztalbot

This comment has been minimized.

Copy link

commented Jul 4, 2019

Yeah that’s basically what I’m getting at. I’m not sure I’d use it either, but the existence of frameworks like Svelte makes me wonder if it would be a worthwhile tool to implement for others.

@tigregalis

This comment has been minimized.

Copy link

commented Jul 4, 2019

Personally, I really like the new vue-function-api, having played with the plugin a bit, and am disappointed that it will take a backseat to the options object in the official documentation, and be treated as an advanced concept when it really isn't: all the functionality (data binding, computed, etc.) is exactly the same, you just format it differently. Having said that you do work with the bindings differently (instead of this.count use count.value or state.count), and so my concerns at the moment relate to:

  1. the naming of the functions
  2. the ergonomics of using the api: count.value versus state.count and the need to use a toBindings wrapper etc.

i.e. the two things that this amendment proposal attempts to address, but, in my opinion, falls short of doing, for reasons already expressed (but which I'll summarise below)

For (1) I've made suggestions on naming that I think are intuitive.

For (2):

Due to technical limitations I accept that you can't simply spread a reactive and that you can't simply access/mutate a binding directly. However, I'm not yet convinced that you need both functions.

It seems nice to have both functions, but they're really doing exactly the same thing (in terms of the intent: making values reactive), but you instantiate them with different functions and they each have a different API.

At best, the reactive function appears to give you the ability to name your .value and group it in a single object with other .values but I haven't got a compelling reason for why you might want to do...

A) this:

const profile = reactive({
  visits: 0,
  name: 'John Smith',
  message: 'Hello'
})

const output = computed(() => `${profile.name} says "${profile.message}"`);

return {
  ...toBindings(profile),
  output
}

over B) this:

const visits = binding(0)
const name = binding('John Smith')
const message = binding('Hello')

const output = computed(() => `${name.value} says "${message.value}"`)

return {
  visits,
  name,
  message,
  output
}

B is more explicit and you can see exactly where things are coming from, but that can be a fairly subjective thing.

However, what if I decided I no longer wanted to expose profile.name/name or profile.message/message to the template? It's an easy change in B: just remove it from the return. not so in A: either pull those values out of profile, or "pick" the values out of toBindings(profile).

Additionally, it might be easier to run into naming collisions

const visits = reactive({
  count: 0,
  latest: null
})

const upvotes = reactive({
  count: 0,
  upvoters: []
})

return {
  ...toBindings(visits),
  ...toBindings(upvotes)
}

(Acknowledging however, that this is more of a JavaScript quirk rather than a Vue API issue, using binding instead of reactive guides the programmer into implementing a "flatter" and more explicit data architecture, so they are less likely to wonder where their visit counter disappeared)

Furthermore, given that:

  1. computed returns a binding anyway, so you immediately have to start using .value anyway
  2. binding is inevitable
  3. you recommend that plugin authors return bindings not reactives
  4. you can't create spreadable reactive objects without a helper, which just converts these to bindings anyway

bindings are already positioned as a first-class API (relative to reactive) and so should be treated as such in the documentation and in the relevant helpers.

@smolinari

This comment has been minimized.

Copy link

commented Jul 4, 2019

@tigregalis - I believe in your first example "state" should be "profile" in some places?? state.name, state.message and ...toBindings(state)? 😃

Scott

@tigregalis

This comment has been minimized.

Copy link

commented Jul 5, 2019

@smolinari good catch. Thanks for that. I initially started the example with state but as the reply grew, I thought to go back and rename it to make that data appear grouped more logically (hoping to present the best example of usage in both cases): unfortunately I missed the clean up.

@Arxcis

This comment has been minimized.

Copy link

commented Jul 5, 2019

@tigregalis Great example 👍 You nailed down exactly why I felt reactive() gotta go. If the function API is marketed as for advanced users we don't need an introductionary inferior middlestep, nameley reactive(), which just confuses and delays the inevitable binding() concept.

@smolinari

This comment has been minimized.

Copy link

commented Jul 5, 2019

Well, reactive saves a lot of "just use binding" boilerplate. And, I'd also rather be seeing profile.name and user.firstName than name.value and firstName.value in the code. I think the former "reactive objects" help a lot more with reasoning about the data in use.

Scott

@Arxcis

This comment has been minimized.

Copy link

commented Jul 5, 2019

@smolinari I also see the value of keeping reactive() around. If we don't, people will make their own version of reactive() which will be slightly different from project to project. A concern which others have voiced earlier.

@aztalbot

This comment has been minimized.

Copy link

commented Jul 5, 2019

It seems a number of us in this thread like @tigregalis have reached the conclusion or at least contemplated that reactive is not the right way to introduce this API. I understand the goal of having something familiar to the object API and hiding .value but for me it ended up being a sort of “false friend” and I think it will do more harm than good if we introduce to beginners early. I think it makes much more sense to lead with binding which lends itself to more natural use in most cases and clearly leads to learning computed. There are specific cases when reactive is nice to use, like with data fetching large objects or for centralized stores. Those specific cases should be introduced but only after the dev learns about bindings. Curious to see how this plays out as more people use this API.

@mimecorg

This comment has been minimized.

Copy link

commented Jul 5, 2019

I think the biggest advantage of the function-based API is that we can group related things together:

const items = binding([]);
const filteredItems = computed(() => ...);
const fetchItems = () => { ... };

const expanded = binding(false);
const expand = () => { expanded.value = true; };
const collapse = () => { expanded.value = false; };

By using reactive(), we would be tempted to put all pieces of state into one object, even if they are unrelated:

const state = reactive({
  items: [],
  expanded: false
});

This would make it harder to extract item related logic to a separate useItems() function, which was the main disadvantage of the object-based API. For me, that defeats the whole purpose of the function-based API.

That's why I think that binding() should be the central contept for managing state. The only use for reactive() would be as a shorthand to store closely related data together:

const user = reactive({ id: 123, name: 'foo' });

But in the template I would refer to it as {{ user.id }} and {{ user.name }}, so no need for destructuring spreading. Putting unrelated things together in one object just to spread it later is wrong imo.

@smolinari

This comment has been minimized.

Copy link

commented Jul 5, 2019

A lot of data lives and breathes in the form of objects. It would be a mess and downright silly to break an object down just to put a binding around each property to make it reactive. Thus, it is why I believe state or reactive is needed and valuable. I couldn't care beans about if it is a binding in the end under the hood.

Also, if you have general "state" data within your component which needs to be reactive, which might be passed on or used throughout the different logic sections of the component, but not necessarily always related, why not group it up in a single object? That way, you can just use state.foo and state.bar, instead of foo.value and bar.value.

If you ask me, if anything is still an eyesore in this new API? It's needing to add .value to get the value from any binding. To me, it is worse than having to write this. in the options API. But, I also understand it is necessary too.

I'm absolutely certain a list of best practices will be needed for code built on this API, because of the flexibility it affords. Like naming functions external to setup useXXX(). Can we not do that?? It's too "react-like". 😁

A set of best practices might also be something the Vue team could put together for the Style Guide docs?? 😁

Scott

@doncatnip

This comment has been minimized.

Copy link

commented Jul 5, 2019

I too tend to think binding as being more or less redundant when it is basically the same as reactive with a 'value' property.

I was about to write why not initialize a reactive with its .value set depending on wether you pass an object or not.

const my_binding = reactive(5)  // resulting in my_binding.value = 5

But then of course you'll get some inconsistencies when you really want to bind an actual Object and not an associative array. In those moments, I find it quite unfortunate that JS does not provide a native Dict-like.

@tigregalis

This comment has been minimized.

Copy link

commented Jul 5, 2019

I think that although reactive is sort of touted as an introductory piece or a bridge between the options object API and the function API, the problem is that it just doesn't take you very far. Rather than teach you anything useful or give you a tool or pattern to use, it seems like it would add one more thing to learn (with limited utility) and simply confuse and mislead you, as it's a detour from where you'll end up (.value).

I totally understand and respect that you might want to group related data in an object, but for me, rather than take the form (1)

const profile = reactive({
  visits: 0,
  name: 'John Smith',
  message: 'Hello'
})

it would be more like (2)

const profile = {
  visits: binding(0),
  name: binding('John Smith'),
  message: binding('Hello')
}

Perhaps you could import a helper function so that you could write (1) or something similar, and get (2). Maybe that helper is called reactive, maybe it is called toBindings, or maybe something else entirely, mapBindings? And along the same lines you could have mapComputeds.

That would start to look more like the object API, and be a bit of a bridge in understanding between the two: the difference from reactive however is that you use the .value API to access and manipulate the data (profile.visits.value), rather than have the object itself reactive (the object is simply just a container), so you are already prepared for the binding .value bonanza.

The question is, though, where would you insert the .value in a more deeply nested object like the following:

const profile = {
  visits: {
    count: 0,
    latest: null,
    items: []
  },
  name: 'John Smith',
  message: 'Hello'
}
@tochoromero

This comment has been minimized.

Copy link

commented Jul 5, 2019

@tigregalis I personally hate the idea to have to use .value everywhere. I would much rather do:

const state = reactive({
   visits: 0,
   name: 'John Smith',
   message: 'Hello'
})

And I would only use binding when it is absolutely necessary.

And even though this is a bit more verbose

return {
   ...toBinding(state)
}

it is a small price to pay to do not use .value. And you always have the option to just return the reactive object directly and prefix it in your template.

It is a shame we cannot just have a compilation step that makes everything inside setup reactive, similar to how Svelte does it. I know one of the main reasons is because of the UMD version, but one could argue that people using the UMD version most likely don´t need "advanced" features such as the functional api. But I know this is a tough sell.

@skyrpex

This comment has been minimized.

Copy link

commented Jul 5, 2019

And you always have the option to just return the reactive object directly and prefix it in your template.

Exactly, I think people will go that way more often than not.

return {
    state,
};
@boonyasukd

This comment has been minimized.

Copy link

commented Jul 7, 2019

After spending some time taking a closer look at API of frameworks which make use of observer pattern to perform data-binding and reactivity, I start to see repeating pattern. In short, what Vue 3 is doing now has already been done before:

  • In Java FX, you have Observable for making an object of arbitrary shape reactive, but you also have ObservableValue that specifically notifies changes in its .value property.
  • In Flutter, you have ChangeNotifier for making an object of arbitrary shape reactive, but you also have ValueNotifier that specifically notifies changes in its .value property.
  • In Mobx, if you decide to declare reactive elements outside of a class, you can use observable.object() to create a reactive object, and observable.box() to create a boxed primitive value.
    • Interestingly, Mobx also provides a catch-all observable() function that can return reactive element of any type (be it object, boxed value, array, etc.).
  • In fact, if you trace it all the way back to Xerox Parc days, in Smalltalk MVC, you also have a class called ValueHolder, which again has .value property, and it notifies all of its dependencies when .value changes.

So, to me, the question shouldn't be about whether or not we should keep one and ditch the other: both are useful, and both have been available in other frameworks for ages. A more pressing concern I have now is that, we keep drawing a beginner/expert line between state() and value(). And I believe we should stop doing that because in reality one isn't technically more advanced than the other: it's all about wrapping what we're interested in behind ES6 Proxy, be it primitive or object:

  • with an object, all properties are reactive; you access any of them as .xxx
  • with a primitive value, there's one reactive property, you access it as .value

IMHO, this (imaginary) beginner/expert line we draw actually makes the API less cohesive. And with one function being named with a simple term (reactive), while the other being named with a more advanced term (binding), it makes people starting to believe that one function is more suited to particular groups of developers than the other. And, to me, that's completely unnecessary and a wrong direction to go. Beginners shouldn't view computed() as more advanced than state(). And experts shouldn't view value() as being more of "a true friend" than state(). If we spend enough time to write proper documentation, with diagrams showing how ES6 Proxy works for both state() and value(), I believe readers will find both functions to be equally easy to understand. I mean, if other frameworks (shown above) can introduce their API cohesively, I couldn't see why Vue 3 cannot do the same. This is even more true considering that function-based API will be an advanced topic in documentation just like mixin anyway, so why should we divide among "advanced developers" even further?

Now, regarding the function naming issue, as stated above, cohesive names are important. It doesn't have to be the same as what Mobx does, but the names should at least read/sound like they belong together than this reactive/binding pair. Earlier in this thread I suggested data()/dataBinding()/toDataBindings(). But I think data()/dataValue()/toDataValues() would work as well.

@tandroid1

This comment has been minimized.

Copy link

commented Jul 8, 2019

I'm coming into this a little late but I think the reactive() function will quickly become something that teams use a linter to prevent. The possibility of someone running into namespacing issues because the object is being spread at the end is too big of a risk on a large team. If some users want to write a function that does the same thing, so be it.

Aside from the namespacing and it likely being considered "bad practice" by the community, I think that having this function would cause some confusion when it comes to logically grouped data. Consider the case where you actually want to bind an object like so:

const user = binding({
  name: "jane",
  age: 35
});

I believe this is a totally valid use of binding, but if we're talking about "beginner" vs "advanced", it's confusing why you would use this over reactive().

Introducing a "simple" concept that could almost immediately require a deeper explanation as to when and where it should be used sounds like a step backwards. I'm all for shortening the name of .value to .val or even .v as mentioned before but in the end I think it's just something we'll need to deal with.

@smolinari

This comment has been minimized.

Copy link

commented Jul 8, 2019

@tandroid1 - it's not possible to bind an object with binding or value, because you'd have no access to the properties (tested it with the function-based-api plugin). binding or value only work with primitives. (Would be nice if Vue would throw an error if someone tries to offer an object as an argument to binding/ value though... 😃)

As for namespacing, and spreading objects, how can that be an issue? Only the objects/ variables returned from the setup function are available in the template. So, there is only one source of truth on what is being offered as "reactively bound" data.

Or am I misunderstanding you completely?

Scott

@tandroid1

This comment has been minimized.

Copy link

commented Jul 8, 2019

Hey @smolinari - Thanks for the clarification! I definitely misunderstood.

For the namespacing, I'm referencing a comment by @tigregalis where they point out that you could do something like this...

const visits = reactive({
  count: 0,
  latest: null
})

const upvotes = reactive({
  count: 0,
  upvoters: []
})

return {
  ...toBindings(visits),
  ...toBindings(upvotes)
}

This might not seem like a common case but I would be tempted to do it so that data is grouped logically.

That being said, maybe it can be taken care of with documentation.

@smolinari

This comment has been minimized.

Copy link

commented Jul 8, 2019

From my understanding, the toBindings helper method would only be needed to return bindings of a more generic "state" object, not objects with specific purposes. So, specific objects would be returned as they are. So in your example:

const visits = reactive({
  count: 0,
  latest: null
})

const upvotes = reactive({
  count: 0,
  upvoters: []
})
// would be returned as objects, not as bindings.
return {
  visits,
  upvotes
}

Which means, in the template you would be using them as {{ visits.count }}, {{ visits.latest }}, {{ upvotes.upvoters }}, etc.

In order to use toBindings you need a few properties within your component, which would normally be simple primitives or an array. Instead of using binding or value to create each reactive variable, you put them in a generic "state" object.

const state = reactive({
  count: 0,
  text: '',
  foo: '',
  bar: []
})

And you use this object throughout your component like so.

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

In other words, as an object, you can use state.count. With a binding, you'd have this....

let count = binding(0)
const double = computed(() => count.value * 2)

And here you can see, we need to add the .value property to get the value. The fact count is some generic state variable isn't quite as clear either. I'm certain something like using a generic "state" object as a container for unrelated data will end up a best practice.

Also with bindings, you'd have to name all the bindings singularly in the return object, along with the boilerplate of having to write out all of the binding assignments.

setup () {

...
  return {
    count,
    text,
    foo,
    bar
  }
}

Whereas with toBindings all you'd have to do is

setup () {

...
  return {
    toBindings(state)
  }
}

And you'd have {{ count }}, {{ text }}, {{ foo }} and {{ bar }} available in your template.

I hope that is making sense. 😊

Scott

@skyrpex

This comment has been minimized.

Copy link

commented Jul 8, 2019

Whereas with toBindings all you'd have to do is

setup () {

...
  return {
    toBindings(state)
  }
}

I think it should be:

setup () {
   return {
        ...toBindings(state)
    }
}
@thenikso

This comment has been minimized.

Copy link

commented Jul 10, 2019

I fully agree that having "magic" names is not the best thing but I am going to suggest this anyway :D

I am both inspired by the new SwiftUI thingy and in retaining the data name Vue users are used to. So, what if:

state()/reactive() -> data()

Where the returned object has a special getter for properties starting with $ so that:

setup() {
  const state = data({ count: 0 });

  const countBinding = state.$count; // access to a property binding
  // equivalent
  const countBinding = state.$.count;

  return {
    ...state.$, // access to a binding version of the object
  };
}

value()/binding() no longer needed

As you'd instead write:

const countBinding = data({ count: 0 }).$count;

I can already see issues with the approach I am proposing such as using keys starting with $ in the initialization object or forgetting the $ when accessing a binding.

To answer the original question I think that reactive/binding are better names than state/value.

@Timkor

This comment has been minimized.

Copy link

commented Jul 24, 2019

Why not just:

const state = useState({
   count: 1
});

And:

const value = useValue(1);

Same for computed. Looks for me more inline with hooks and it also does prevent variable name clashing.

@o-w-o

This comment has been minimized.

Copy link

commented Aug 5, 2019

convention: access or tranfer data can only by observable data itself methods.

setup() {
    const state = reactive({
      num: 0,
      arr: ['a', 'b', 'c'],
      str: 'abc',
    })

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

    function increment() {
      state.num++
    }

    return {
      ...state.$pick(["num"]) // retains reactivity on mutations made to `state`
      double,
      increment
    }

    return {
      ...state.$pickAll()
    }

    return {
      ...state.$omit(["arr"])
    }
  }
@smolinari

This comment has been minimized.

Copy link

commented Aug 5, 2019

@o-w-o - Isn't the spread on the state.$pick superfluous?

I also believe you are missing the greater picture. A state variable would only be needed for any kind of general data within the component. Everything else would be defined in their own reactive objects in their own modules and "imported" in and "used" in the setup function. It would totally defeat the nature of this new API to define all state in one object in the setup function (which is the issue of the options object) and of course, only if that is what you were thinking.

Scott

@luxaritas

This comment has been minimized.

Copy link

commented Aug 6, 2019

As a belated note per #63 (comment)

I'm a proponent for always returning a reactive.

The potential issues Evan mentioned were

  • avoid spreading the object (loses reactivity)
  • avoid destructuring the object (also loses reactivity)
  • always nest property access in template
  • use toBindings if they want to avoid nesting

I'd think these would actually be best practices. If some state is coming from a composition function, IMO it's more readable to always keep it "grouped"/"scoped", so that you always know where a given property is coming from (as opposed to having to reference the destructure statement). You could think of it similarly to following the principle of preferring composition over inheritance.

When I posted #63 (comment) , I didn't realize that you could put computeds/methods/etc on reactive/state. This actually resolves a big readability issue I had which I discussed in #42 (comment). Knowing this, I might actually be willing to drop class API for the function API - and I'm wondering if many "object API proponents" would feel the same way (though not all of them, I'm sure).

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

feels significantly simpler and easier to reason about from the perspective of "what's in my state" than

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
  }
}
@luxaritas

This comment has been minimized.

Copy link

commented Aug 6, 2019

For clarity

I'm a proponent for always returning a reactive.

My intent with that isn't necessarily "I think returning a reactive should be the only option", but "I think returning a reactive should be the preferred option, and the one used in tutorials, etc" (particularly because it appears to me to be more beginner friendly due to its simplicity). If it's the only option I won't personally complain, but I'm sure others prefer the current proposed approach.

@tiepnguyen

This comment has been minimized.

Copy link

commented Aug 7, 2019

Can I expect createComponent to take first argument as component name if it is a string?

import { createComponent } from 'vue'

export default createComponent('MyComponent', (props: {msg: string}) => {
  ...
  return () => <SomeTSX/>
})

Because I prefer TypeScript-only props, and mainly use TSX / render function, I don't need any other options but setup(), however I still need component name (for devtool debug purpose?)

@yyx990803

This comment has been minimized.

Copy link
Member Author

commented Aug 17, 2019

Closing in favor of #78

@yyx990803 yyx990803 closed this Aug 17, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.