-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
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
[Abandoned] API for React Hooks like logic composition #23
Comments
Hey Evan! I like this. I don't have any real opinion on the details like I would like to see the risk of overwhelming users with too many new things, or rather - too many choices with 3.0 - being adressed, though. I really think there will be FUD over object vs. class syntax already. We will of course adress that by recommending for people to stick to what they have similar to how React doesn't recommend converting all class components to functional components + hooks. But just like in the React community, despite that recommendation, some level of FUD will exist. Introducing this totally new way of doing things in 3.0 will probably have the same effect:
etc, pp. We can only do so much to contain that, and I would hate for people getting the impression that 3.0 does an Angular move by changing "everything" (however wrong that impression may be). Just as an idea worth discussing - Would we maybe consider introducting this as explicitly experimental, or publish it in 3.1, after 3.0 has stabilized after release? |
Another thought crossing my mind: Since this is a completely new API, shouldn't we be able to port it to v2, or at least Would make it possible to use new packages that expose hooks in in 2.0 apps that are not ready tpo be migrated. And yes, I'm aware that this kind of contradicts my previous point about releasing hooks later :-P |
@LinusBorg I totally agree. This is in fact my biggest concern as well. But I think that really depends on how we present it. Here this internal proposal is presenting it directly as a hooks equivalent because that's what it is aiming to be - however, we don't really have to introduce it to the users that way. Notice that the reactivity APIs ( If we use The only part that is really hooks-like is the function useSomeLogic(vm) {
const count = Vue.observable({ value: 0 })
vm.$watch(() => count.value, value => {
// ...
}, { immediate: true })
vm.$on('hook:mounted', () => {
// ...
})
return count
}
export default {
template: `<div>{{ count.value }}</div>`,
data() {
return {
count: useSomeLogic(this)
}
}
} This proposal isn't really introducing anything drastically different, but rather polishing some existing APIs to make them work better when used to encapsulate and reuse logic. |
Some thoughts:
|
Would that work? function computedWithSetter(getter, setter) {
const computedRef = computed(getter)
return Object.defineProperty(res, 'value',{
get() { return computedRef.value }
set: setter,
})
} created() {
const computedWSetterRef = computedWithSetter(() => someRef.value, () => /*do whatever*/ )
return {
computedWSetterRef
}
} <input v-model="computedWSetterRef.value">
I think it's fine. There's a lot of use cases where I feel watchers would be more useful and code shorter if they were immediate by default.
Tricky ... :/ Tried to simulate it in 2.0 and failed to make it work. I like adding it to
|
@LinusBorg if a ref is returned there's no need to use
Yeah I think in that case a new hook that is called after props initialization but before computed initialization would be useful. As for watchers - I'm thinking of removing |
Considering our promise of keeping the Vue 3 API largely compatible to 2.0, I don't think we should/can remove APIs for the sole reason of shaving a few bytes of the final bundle in a few use cases. |
@LinusBorg it is trivial to support in the migration build, and in most cases can be automatically converted. The only reason
Where (3) provides a superset of (1) and (2). In this case, (1) and (2) just seem pointless and redundant. |
is it possible to hit a middleground with a compat plugin that adds those features (watch for example) with warnings an cli hints to do the automatic conversion? |
Good point. Another positive effect of this, combined with the reactivity system v. React's immutable state, it that we don't have to worry about closures breaking stuff if the wrong references are included in it, or dependency tracking in React's We should really stress this point as from what I read about problems with hooks, people really fall a lot for these kind of unwritten / hidden rules - which is why React also did an eslint plugin to keep people from shooting themselves in the foot. We don't really need that, do we? OTOH, The point about referencing hook refs from within computed etc. might be our version of such footguns. |
Yes, this proposal should work more along the lines of typical JavaScript intuitions than React hooks. Re refs: once a ref is exposed to the instance they just become normal properties. For the sake of discussion, let's call it "Vue blocks" (to differentiate from React hooks and lifecycle hooks)... we extract and encapsulate logic inside a block, and then expose it to the instance. Let's call it "the block side" and "the instance side": import { value } from 'vue'
funciton useCustomBlock() {
// the block side
return value(0)
}
export default {
created() {
return {
// expose a ref to the instance (becomes a normal root-level property)
count: useCustomBlock()
}
},
computed: {
countPlusOne() {
// the instance side
// no need to use .value
return this.count + 1
}
}
} You will only create and use refs on the blocks side, because there is not a persistent |
@yyx990803 I might have some ideas for how we could build on top of current mixins instead and avoid adding much new API or concepts, but first I want to make sure I'm understanding the full advantages this syntax provides. The ones I've seen are:
Is that everything, or are there other advantages/goals? |
I think another aspect, not necessarily related to mixins, is that you don't spread parts of a behaviour over different parts of your component, i.e. data&created&watch&destroyed. You can define the whole lifecycle of a piece of behaviour in one place. |
Another important aspect is the ability to compose multiple "logic modules" together by passing state between them (while not polluting the namespace of the host component): export default {
props: ['id'],
created() {
const position = useMousePosition()
const orientation = useDeviceOrientation()
const matrix = useMatrix(x, y, orientation)
return {
matrix
}
}
} So instead of multiple mixins all competing on the same |
Note: this has been split into two separate RFCs: #24 & #25
Summary
A set of React-hooks-inspired APIs for composing and reusing stateful logic across components.
Basic example
In Object API
In Class API
Usage in a Class-based API would be exactly the same:
Motivation
Detailed design
Summary
This proposal consists of three parts:
A set of APIs centered around reactivity, e.g.
value
,computed
andwatch
. These will be part of the@vue/observer
package, and re-exported in the mainvue
package. These APIs can be used anywhere and isn't particularly bound to the usage outlined in this proposal, however they are quintessential in making this proposal work.A set of call-site constrained APIs that registers additional lifecycle hooks for the "current component", e.g.
useMounted
. These functions can only be called inside thecreated()
lifecycle hook of a component.The ability for the
created()
lifecycle hook to return an object of additional properties to expose on the component instance.Reactivity APIs
In Vue 2.x, we already have the
observable
API for creating standalone reactive objects. Assuming we can return an object of additional properties to expose onthis
fromcreated()
, we can achieve the following:The above is a contrived example just to demonstrate how it could work. In practice, this is intended mainly for encapsulating and reusing logic much more complex than a simple counter.
Value Container
Note in the above example we had to expose an object (so that Vue can register the dependency via the property access during render) even though what we are really exposing is just a number. We can use the
value
API to create a container object for a single value, called a "ref". A ref is simply a reactive object with a writablevalue
property that holds the actual value:The reason for using a container object is so that our code can have a persistent reference to a value that may be mutated over time.
A value ref is very similar to a plain reactive object with only the
.value
property. It is primarily used for holding primitive values, but the value can also be a deeply nested object, array or anything that Vue can observe. Deep access are tracked just like typical reactive objects. The main difference is that when a ref is returned as part of the return object increated()
, it is bound as a direct property on the component instance:A ref binding exposes the value directly, so the template can reference it directly as
count
. It is also writable - note that the click handler can directly docount++
. (In comparison, non-ref bindings returned fromcreated()
are readonly).Computed State
In addition to writable value refs, we can also create standalone computed refs:
Computed refs are readonly. Assigning to its
value
property will result in an error.Watchers
All
.value
access are reactive, and can be tracked with the standalonewatch
API.Note that unlike
this.$watch
in 2.x,watch
are immediate by default (defaults to{ immediate: true }
) unless a 3rd options argument with{ lazy: true }
is passed:The callback can also return a cleanup function which gets called every time when the watcher is about to re-run, or when the watcher is stopped:
A watch returns a stop handle:
Watchers created inside a component's
created()
hook are automatically stopped when the owner component is destroyed.Lifecycle Hooks
For each existing lifecycle hook (except
beforeCreate
andcreated
), there will be an equivalentuseXXX
API. These APIs can only be called inside thecreated()
hook of a component. The prefixuse
is an indication of the call-site constraint.Unlike React Hooks,
created
is called only once, so these calls are not subject to call order and can be conditional.useXXX
methods automatically detects the current component whosesetup()
is being called. The instance is also passed into the registered lifecycle hook as the argument. This means they can easily be extracted and reused across multiple components:A More Practical Example
Let's take the example from React Hooks Documentation. Here's the equivalent in Vue's idiomatic API:
And here's the equivalent using the APIs introduced in this proposal:
Note that because the
watch
function is immediate by default and also auto-stopped on component unmount, it achieves the effect ofwatch
,created
anddestroyed
options in one call (similar touseEffect
in React Hooks).The logic can also be extracted into a reusable function (like a custom hook):
Note that even after logic extraction, the component is still responsible for:
Even with multiple extracted custom logic functions, there will be no confusion regarding where a prop or a data property comes from. This is a major advantage over mixins.
Drawbacks
Type inference of returned values. This actually works better in the object-based API because we can reverse infer
this
based on the return value ofcreated()
, but it's harder to do so in a class. The user will likely have to explicitly annotate these properties on the class.To pass state around while keeping them "trackable" and "reactive", values must be passed around in the form of ref containers. This is a new concept and can be a bit more difficult to learn than the base API. However, this isn't intended as a replacement for the base API - it is positioned as a advanced mechanism to encapsulate and reuse logic across components.
Alternatives
Compared to React hooks:
watch
has automatic dependency tracking, no need to worry about exhaustive depsAdoption strategy
TODO
Unresolved questions
We can also use
data()
instead ofcreated()
, sincedata()
is already used for exposing properties to the template. But it feels a bit weird to perform side effects likewatch
oruseMounted
indata()
.state()
? (replacesdata()
and has special handling for refs in returned value)We probably need to also expose a
isRef
method to check whether an object is a value/computed ref.Appendix: More Usage Examples
Data Fetching
This is the equivalent of what we can currently achieve via scoped slots with libs like vue-promised. This is just an example showing that this new set of API is capable of achieving similar results.
Use the Platform
Hypothetical examples of exposing state to the component from external sources. This is more explicit than using mixins regarding where the state comes from.
The text was updated successfully, but these errors were encountered: