-
-
Notifications
You must be signed in to change notification settings - Fork 8.1k
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
RFC Draft: Function-based Component API #35
Comments
😍
This is huge. What happens to In the compat build, will it be possible to call composition functions in other places than |
I agree this is quite unclear in the current RFC. Maybe something like this: import { emit } from 'vue'
export function myComposition () {
emit('change', 42)
} |
Motivation > Bundle SizeThe returned object in Detailed Design > Stateis Regarding the example with props: setup(props) {
return {
msg: `hello ${props.name}!`
}
}, Does this return a static value or is it equivalent to creating a computed property WatchersIf we watch a function that returns an array, does the watcher trigger whenever the array is modified? I'm asking because right now, passing a getter to the watcher will trigger only if the value or reference is modified, which requires the dev to serialize the information to properly trigger one watcher when one of the multiple properties we want to watch changes. // taken from https://github.com/vuejs/rfcs/pull/22#issuecomment-481084795
watch(
() => [currentArenaRef.value, currentUserRef.value],
([currentArena, currentUser], [prevArena, prevUser]) => {
findMatches(currentArena,currentUser ) ...
}
}) can we also do const currentInfo = observe({
currentArenaRef,
currentUserRef
})
watch(
currentInfo,
// what happens if the user does
// () => currentInfo
// do we warn them?
// ...
})
const currentArenaAndUser = value([
currentArenaRef,
currentUserRef
])
watch(
currentArenaAndUser,
// can we do this?
}) Watcher Callback TimingThe Adoption Strategy > Options deprecated by this RFCWhat is DrawbacksAnother drawback is having pretty much everything inside of one function, leaving to the user how to organize all the logic, which could indeed bring spaghetti code like mentioned in (vuejs/rfcs#22 (comment)). Previously, having |
Kinda agree on the drawback @posva mentioned. Thought the |
Thinking about vue router navigation guards. Since properties returned in |
I am also worried that having everything defined in With regard to being able to mutate unwrapped values in template, as shown: const MyComponent = {
setup() {
return {
count: value(0)
}
},
template: `<button @click="count++">{{ count }}</button>`
} Would that also apply when passing the unwrapped value through a scoped slot? e.g. const MyComponent = {
setup() {
return {
count: value(0)
}
},
template: `<div><slot v-bind:count="count"></slot></div>`
}
const OtherComponent = {
template: `<my-component><button v-slot="props" @click="props.count++">{{ props.count }}</button></my-component>`
} |
What also worries me that this will most likely break plugins like Vuelidate, that rely on component options and
I like the idea personally, but I fear this will make the library harder to use, especially since currently it also supports validating computed properties or Vuex getters. Also, how would we use Vuex with that API? Say using
|
Yes, this should be one of the unresolved questions. I was thinking between
No, slot props are always immutable.
This would require template references to be mangled too - which can be very hard.
It works just like returning an object from
I don't understand the question. Watchers have always been working this way. If you want to trigger watcher with nested mutations you should be using
I tend to look at this problem this way: since function-based APIs leave more flexibility to the users, this can lead to both less organized code (by beginners) AND better organized code. However, the less organized code can be largely mitigated with proper documentation of best practices, while the better organized code can never be achieved with current options-based API. With component options, your code only seem to be organized - in a complex component, logic related to a specific task is often split up between multiple options. Separation of options vs. |
This does work for me. Thank you! I also imagine that possibly, a composition function could return a render function (a functional component) that could then be used inside the components render function? Or template? |
Does that mean that it's possible to mix the two?
Not that I think this is nice, but it would be convenient for migrating bigger components piece by piece from the object notation to the setup function. |
I'm realizing I have been teaching a way of doing this that is more complicated than it should because using an array also works 😅. It may trigger more times if a value returned in the array is changed multiple times before a flush and is set to the same value it had before being changed the first time
I think that practice has shown us that people don't read the documentation, no matter how good it is. Aren't we going to complexify things for people who are starting if they learn first the object based syntax (Vue 2) and then learn the advanced version which allows them to use the smaller version of Vue? It feels like we are making the learning curve steeper. |
I don't think that's true at all. People probably don't read the API listing in full details, but they definitely read the guide when they first learn Vue. The point is - anyone learning the new API will have to learn it through something - be it docs or 3rd party content - and any 3rd party content will be some sort of spin-off of the docs. So as long as the initial v3 docs introduces it properly, most users will learn to use it properly. |
Yes. There might be some edge cases (I haven't fully implemented 2.x compat support yet) but theoretically it should work. |
Could that also be |
@KaelWD it's more like |
I like how it's much easier to switch between a read-only and writeable computed properties without having to rewrite between an object and functions. |
Summary
Expose logic-related component options via function-based APIs instead.
Basic example
Motivation
Logic Composition
One of the key aspects of the component API is how to encapsulate and reuse logic across multiple components. With Vue 2.x's current API, there are a number of common patterns we've seen in the past, each with its own drawbacks. These include:
mixins
option)These patterns are discussed in more details in the appendix - but in general, they all suffer from one or more of the drawbacks below:
Unclear sources for properties exposed on the render context. For example, when reading the template of a component using multiple mixins, it can be difficult to tell from which mixin a specific property was injected from.
Namespace clashing. Mixins can potentially clash on property and method names, while HOCs can clash on expected prop names.
Performance. HOCs and renderless components require extra stateful component instances that come at a performance cost.
The function based API, inspired by React Hooks, presents a clean and flexible way to compose logic inside and between components without any of these drawbacks. This can be achieved by extracting code related to a piece of logic into what we call a "composition function" and returning reactive state. Here is an example of using a composition function to extract the logic of listening to the mouse position:
Note in the example above:
See also:
Type Inference
One of the major goals of 3.0 is to provide better built-in TypeScript type inference support. Originally we tried to address this problem with the now-abandoned Class API RFC, but after discussion and prototyping we discovered that using Classes doesn't fully address the typing issue.
The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs.
See also:
Bundle Size
Function-based APIs are exposed as named ES exports and imported on demand. This makes them tree-shakable, and leaves more room for future API additions. Code written with function-based APIs also compresses better than object-or-class-based code, since (with standard minification) function and variable names can be shortened while object/class methods and properties cannot.
Detailed design
The
setup
functionA new component option,
setup()
is introduced. As the name suggests, this is the place where we use the function-based APIs to setup the logic of our component.setup()
is called when an instance of the component is created, after props resolution. The function receives the resolved props as its argument:Note this
props
object is reactive - i.e. it is updated when new props are passed in, and can be observed and reacted upon using thewatch
function introduced later in this RFC. However, for userland code, it is immutable during development (will emit warning if user code attempts to mutate it).State
Similar to
data()
,setup()
can return an object containing properties to be exposed to the template's render context:This works exactly like
data()
-msg
becomes a reactive and mutable property, but only on the render context. In order to expose a reactive value that can be mutated by a function declared insidesetup()
, we can use thevalue
API:Calling
value()
returns a value wrapper object that contains a single reactive property:.value
. This property points to the actual value the wrapper is holding - in the example above, a string. The value can be mutated:Why do we need value wrappers?
Primitive values in JavaScript like numbers and strings are not passed by reference. Returning a primitive value from a function means the receiving function will not be able to read the latest value when the original is mutated or replaced.
Value wrappers are important because they provide a way to pass around mutable and reactive references for arbitrary value types. This is what enables composition functions to encapsulate the logic that manages the state while passing the state back to the components as a trackable reference:
Value wrappers can also hold non-primitive values and will make all nested properties reactive. Holding non-primitive values like objects and arrays inside a value wrapper provides the ability to entirely replace the value with a fresh one:
If you want to create a non-wrapped reactive object, use
observable
(which is an exact equivalent of 2.xVue.observable
API):Value Unwrapping
Note in the last example we are using
{{ msg }}
in the template without the.value
property access. This is because value wrappers get "unwrapped" when they are accessed on the render context or as a nested property inside a reactive object.You can mutate an unwrapped value binding in inline handlers:
Value wrappers are also automatically unwrapped when accessed as a nested property inside a reactive object:
As a rule of thumb, the only occasions where you need to use
.value
is when directly accessing value wrappers as variables.Computed Values
In addition to plain value wrappers, we can also create computed values:
A computed value behaves just like a 2.x computed property: it tracks its dependencies and only re-evaluates when dependencies have changed.
Computed values can also be returned from
setup()
and will get unwrapped just like normal value wrappers. The main difference is that they are read-only by default - assigning to a computed value's.value
property or attempting to mutate a computed value binding on the render context will be a no-op and result in a warning.To create a writable computed value, provide a setter via the second argument:
Watchers
All
.value
access are reactive, and can be tracked with the standalonewatch
API, which behaves like the 2.xvm.$watch
API but with important differences.The first argument passed to
watch
can be either a getter function or a value wrapper. The second argument is a callback that will only get called when the value returned from the getter or the value wrapper has changed:Unlike 2.x
$watch
, the callback will be called once when the watcher is first created. This is similar to 2.x watchers withimmediate: true
, but with a slight difference. By default, the callback is called after current renderer flush. In other words, the callback is always called when the DOM has already been updated. This behavior can be configured.In 2.x we often notice code that performs the same logic in
mounted
and in a watcher callback - e.g. fetching data based on a prop. The newwatch
behavior makes it achievable with a single statement.Watching Props
As mentioned previously, the
props
object passed to thesetup()
function is reactive and can be used to watch for props changes:Watching Value Wrappers
Stopping a Watcher
A
watch
call returns a stop handle:If
watch
is called insidesetup()
or lifecycle hooks of a component instance, it will automatically be stopped when the associated component instance is unmounted:Effect Cleanup
Sometimes the watcher callback will perform async side effects that need to be invalidated when the watched value changes. The watcher callback receives a 3rd argument that can be used to register a cleanup function. The cleanup function is called when:
watch
is used insidesetup()
)We are registering cleanup via a passed-in function instead of returning it from the callback (like React
useEffect
) because the return value is important for async error handling. It is very common for the watcher callback to be an async function when performing data fetching:An async function implicitly returns a Promise, but the cleanup function needs to be registered immediately before the Promise resolves. In addition, Vue relies on the returned Promise to automatically handle potential errors in the Promise chain.
Watcher Callback Timing
By default, all watcher callbacks are fired after current renderer flush. This ensures that when callbacks are fired, the DOM will be in already-updated state. If you want a watcher callback to fire before flush or synchronously, you can use the
flush
option:Full
watch
OptionsLifecycle Hooks
All current lifecycle hooks will have an equivalent
onXXX
function that can be used insidesetup()
:Dependency Injection
If provided key contains a value wrapper,
inject
will also return a value wrapper and the binding will be reactive (i.e. the child will update if ancestor mutates the provided value).Drawbacks
Makes it more difficult to reflect and manipulate component definitions. (Maybe that's a good thing?)
Alternatives
Adoption strategy
The proposed APIs are all new additions and can theoretically be introduced in a completely backwards compatible way. However, the new APIs can replace many of the existing options and makes them unnecessary in the long run. Being able to drop some of these old options will result in considerably smaller bundle size and better performance.
Therefore we are planning to provide two builds for 3.0:
Compatibility build: supports both the new function-based APIs AND all the 2.x options.
Standard build: supports the new function-based APIs and only a subset of 2.x options.
Current 2.x users can start with the compatibility build and progressively migrate away from deprecated options, until eventually switching to the standard build.
Preserved Options
name
props
template
render
components
directives
filters
*delimiters
*comments
*Options deprecated by this RFC
data
(replaced byvalue
andvalue.raw
returned fromsetup()
)computed
(replaced bycomputed
returned fromsetup()
)methods
(replaced by plain functions returned fromsetup()
)watch
(replaced bywatch
)provide/inject
(replaced byprovide
andinject
)mixins
(replaced by function composition)extends
(replaced by function composition)onXXX
functions)Options deprecated by other RFCs
el
Components are no longer mounted by instantiating a constructor with
new
, Instead, a root app instance is created and explicitly mounted. See RFC#29.propsData
Props for root component can be passed via app instance's
mount
method. See RFC#29.functional
Functional components are now declared as plain functions. See RFC#27.
model
No longer necessary with
v-model
arguments. See RFC#31.inheritAttrs
Deperecated by RFC#26.
Appendix
Comparison with React Hooks
The function based API provides the same level of logic composition capabilities as React Hooks, but with some important differences. Unlike React hooks, the
setup()
function is called only once. This means code using Vue's function APIs are:useEffect
callback may capture stale variables if the user forgets to pass the correct dependency array;useMemo
is almost always needed in order to prevent inline handlers causing over-re-rendering of child components;Type Issues with Class API
The primary goal of introducing the Class API was to provide an alternative API that comes with better TypeScript inference support. However, the fact that Vue components need to merge properties declared from multiple sources onto a single
this
context creates a bit of a challenge even with a Class-based API.One example is the typing of props. In order to merge props onto
this
, we have to either use a generic argument to the component class, or use a decorator.Here's an example using generic arguments:
Since the interface passed to the generic argument is in type-land only, the user still needs to provide a runtime props declaration for the props proxying behavior on
this
. This double-declaration is redundant and awkward.We've considered using decorators as an alternative:
Using decorators creates a reliance on a stage-2 spec with a lot of uncertainties, especially when TypeScript's current implementation is completely out of sync with the TC39 proposal. In addition, there is no way to expose the types of props declared with decorators on
this.$props
, which breaks TSX support. Users may also assume they can declare a default value for the prop with@prop message: string = 'foo'
when technically it just can't be made to work as expected.In addition, currently there is no way to leverage contextual typing for the arguments of class methods - which means the arguments passed to a Class'
render
function cannot have inferred types based on the Class' other properties.The text was updated successfully, but these errors were encountered: