-
Notifications
You must be signed in to change notification settings - Fork 545
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
Advanced Reactivity API #22
Conversation
The |
I, personally, would like to see some heavy warning verbiage around many of these features, as well as some "don't use this in 'x' scenario, just use simpler option 'y'" examples. I think that would go a long way towards helping newer developers get a good grasp of when and when not to use this level of the api. A lot of these apis look like they could result in some real spaghetti architecture if perceived as "first-choice" candidates for state management. As someone who has experienced some angular.js pre-1.5.x trauma, I'd certainly like to avoid that. |
If Otherwise, I think that removing |
What in particular do you find confusing? I assume it's that you for a I see how this could trip up some people, and in fact, But since we can recognize that We can't do the same for "normal" reactive objects, so in their case, we have to provide a function to actually access the property(s) to watch. Do you think it would be fine if we document the |
@Aaron-Pool We definitely agree. We called this RFC Advanced Reactivity API for that reason, basically. The plan would be to thoroughly extend the |
|
I don't have much experience with them, but this looks like it could make writing render functions feel a lot more 'native' to Vue. (currently it feels a little bit like its own language). Is that an accurate assessment? I have a niggling concern about all this |
Exactly what you mentioned (sorry, I was on mobile so I couldn't expand much). Basically, this: const count = value(0)
watch(count, (countVal) => {
// countVal === count.value
})
Yes, I think so! |
This particular API won't have much of an influence on how we write render functions, no. But we'll have a separate RFC about render functions and the virtual dom soon. About the ref <-> $ref thing: i raised the same concern in internal discussions. We will certainly have to rename one or the other i think. But that's secondary i think. |
I think since the |
Do const obj = state({ count: 1 });
computed(() => obj.count + 1);
const count = state(1);
computed(() => count.value + 1); Also they don't declare action in their name. Maybe |
What do you think about pointers having custom function value(initialValue) {
return {
value: initialValue,
valueOf() {
return this.value;
},
}
}
const count = value(1);
computed(() => count + 1); That way it could interoperate with the |
I'm really excited about this new API. I've already tried my best attempting to implement it un user land, but It was a bit hacky (vuejs/vue#9509). I hope this gets approved/implemented/merged as soon as possible. Thank you for this, vue team!! |
In general, I love the overall API, it is very well thought out and designed. I do have some thoughts and questions, however:
I'm guessing that this might already be possible in the proposed API by using computed + object spread (… is it?) so this could be a nice shorthand. import { computed } from 'vue'
export default {
data() {
return computed(() => {
switch (this.$props.state) {
case 'pending':
return {loading: true}
case 'resolved':
return {data: "something"}
case 'rejected':
return {error: "oh no"}
}
})
}
}
I'm thinking of something like below (The API names below probably aren't the greatest — just for demonstration purposes): import { immutable, update } from 'vue'
export default {
data() {
return immutable({
state: null,
})
},
methods: {
loadData() {
// In `update` this components data is temporarily
// unlocked so we can mutate it freely
update(() => {
this.state = "foo bar"
})
}
},
} With this, any updates to state returned by immutable must be unlocked first. This API would ensure that Vue is still observing changes (so not like Secondary idea: The update function wouldn't be global. It'd be per immutable instance so only the thing that created the object has access to the mechanism to unlock its mutability. This is just one approach. I'm 100% open to other ideas in this area. |
Isnt this like a poor mans mobx? |
Ignoring your choice of words, the answer is: yes, in a way. MobX provides the same kind of reactivity that Vue did and does, plus some abstractations over it that Vue doesn't have in core. Since we are now exposing our reactivity system as a standalone library, the basic functionality is pretty similar. |
I like the way React hooks solved the issue with primitives being passed by value, instead of by reference. You get a getter and a setter for these values and do not have this issue anymore. Maybe the same could be applied to Vue as well. const [primitive, setPrimitive] = value(0); // You can't do primitive = 1;
watch(primitive, val => console.log(`Value is: ${val}`));
setPrimitive(1); // Value is: 1 For those who don't like this syntax expose internals, where it's possible to change value by accessing const primitive = value(0);
watch(primitive, val => console.log(`Value is: ${val}`));
primitive.value = 1; // Value is: 1 I believe internal implementation could look like this: function value(initialValue) {
let localValue = initialValue;
const getter = () => localValue;
const setter = (newValue) => localValue = newValue;
const props = [{ valueOf: getter }, setter];
props.valueOf = getter;
Object.defineProperty(props, 'value', {
get: getter,
set: setter,
})
return props;
} This would also work for the standard way in Vue 2.x to manually trigger reactivity with Vue.set(): import { setValue, value } from 'vue';
const primitive = value(0);
setValue(primitive, 1); // setValue could call a `value` setter method |
About Computed Maybe, computed pointers/states should be "depends" on state, like For example: const { age } = state({ age: 18 })
const name = value('John');
const userInfo = computed(name, age, (name, age) => `${name} is ${age} years old `); Pros:
Cons:
|
The most common use case for mixins I currently see, is to define a set of helper data/computed/methods/life-cycle actions that can be reused across components for common behaviour. For instance, a simple pagination helper might look something like this: // Vue 2.x mixin
export default {
data() {
return {
currentPage: 1,
lastPage: null
}
},
computed: {
nextPage() {
if (this.currentPage === this.lastPage) return null
return this.currentPage + 1
}
}
} With the new reactivity API, am I correct in assuming that the same could be achieved with something like the following: // Vue 3.x with reactivity helpers
import { value, computed } from '@vue/observer'
const paginationHelper = () => {
const currentPage = value(1)
const lastPage = value(null)
const nextPage = computed(() => {
if (currentPage.value === lastPage.value) return null
return currentPage.value + 1
})
return { currentPage, lastPage, nextPage }
}
export default {
data() {
return {
...paginationHelper(),
// local data here
}
}
} In a real-life scenario, the EDIT: Realized that I forget to change the helper props when rewriting it. Updated now. |
A couple of replys:
It's not part of this RFC though, and currently not externally usable I think. /cc @yyx990803 any input here?
I don't like it. We are basically taking away the convenience of automatically registering dependencies to make this function "pure", and I don't see how it's useful to have a pure function here.
Correct.
Fair enough. But this RFC is focussed on the standalone capabilites of the reactivity system. Mixins often also involve lifecycle methods, and we have another RFC for those, in #23. In that RFC, you can find examples how that RFC and this one we are discussing right now can be used to do the same that mixins do, and more/better. |
This is basically mobx, I love it 😄 |
Small point, but I had to re-read the writable pointer example a few times before I understood it. const writablePointer = computed(
// read
() => count.value + 1,
// write
val => {
count.value = val - 1
}
) Might this be more explicit? const writablePlusOne = computed(
// read
() => count.value + 1,
// write
val => {
count.value = val - 1
}
)
console.log(count.value) // 1
console.log(writablePlusOne.value) // 2
writablePlusOne.value = 5
console.log(count.value) // 4
console.log(writablePlusOne.value) // 5 |
We can pass to watch multiple values .
I find it very usefull. |
Mobx exposes API to observe an object as a whole: import {observable, observe} from 'mobx';
const person = observable({
firstName: "Maarten",
lastName: "Luther"
});
const disposer = observe(person, (change) => {
console.log(change.type, change.name, "from", change.oldValue, "to", change.object[change.name]);
}); In this API, the change callback receives the name of property that changed as well as the old and new value of the property. This is very helpful when you need to listen to all the potencial changes of an object with only one callback. It will be very practical if Vue 3 provides such an API. |
You'r obviously concerned with establishing the low level utilities first, but any plans to include a macro builder similar to how the Vue instance works now? const person = reactive({
data: {
firstName: 'Joe',
lastName: 'Smith',
nameChanges: 0,
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
},
watch: {
fullName() {
this.nameChanges++;
}
}
}) It would be easy enough to build on top of the provided low level APIs, but might be better to provide it up front so that a bunch of miscellaneous implementations don't start popping up all over the place. Also I just realized that it's called |
Seriously love that you decided to split reactivity system out, but isn't this just re-implementing MobX? Thanks for reply and your work! |
mobx also comes with a es6 proxy version |
@dodas Well, that would mean that you will have two reactivity systems in the app unnecessarily. Vue will use its reactivity system anyway, this is only a way to expose it as public API. Having this "in-house" is a good way to ensure compatibility, I doubt Vue would ever use 3rd party lib for such a core feature as reactivity system. You can already use MobX with |
Could the watchers probably be written without having to specify what you are watching? watch(() => {
console.log(`obj.a is: ${obj.a}`)
})
watch(() => {
console.log(`count is: ${count}`)
})
watch(() => {
console.log(`count plus one is: ${plusOne}`)
}) The passed function would then recalculate on changes of any used reactive value. watch(() => {
console.log(`count is: ${count}, and count plus one is ${plusOne}`)
}) |
@David-Desmaisons That's an interesting suggestion! However if we implement it, maybe we should consider making it a dedicated API, since I'm not sure that we back backport that functionality to the compatibility build which reimplements the old getter&setter based Reactivity for IE compatibility. But I'm liking the feature, definitely. |
@jhoffner I personally don't see a reason for providing such a mechanism in our own core, since it would be trivial to implement in userland while it doesn't really provide any objectively tangible advantage over doing this: const person = {
firstName: value('Joe'),
lastName: value('Smith'),
nameChanges: value(0),
fullName: computed(() =>`${person.firstName} ${person.lastName}`)
}
watch(person.fullName, () => person.nameChanges.value++)
We are aware of this and are internally debating what to do with this. Naming things one of the two hard problems in CS, you can imagine it's not easy ^^ |
@dodas Basically what @panstromek said... |
@HendrikJan Not really, for multiple reasons.
const lock = value(false)
const counter = value(1)
watch(() => {
if (lock.value && counter.value !== 0) {
counter.value++
}
}) Thee above would result in us watching the
watch(() =>{
if (somePointer) {
await someOp(() => {
if (someOtherPointer) { ... }
}
}
}) We can't collect |
Infinite loops might be easy to detect and give warnings about, but point 1 and 3 are good arguments. |
Sure, but what good would the warning be... The consequence is that you can't modify any reactive dependency from a watcher that you also need to access in that watcher. And trying to technically prevent such an infinite loop in |
Looks good. I'd like to have the same API for objects and primitives. The ability to provide an array to watch on is a must have. |
A dedicated API would be fine indeed. I have a specific scenario for Neutronium integration where I need to listen to all properties of an object and to know which property has been changed. With the current implementation, I need to register a callback for each properties. Having a single callback as in mobx would result in a potentially substantial memory optimization. |
@David-Desmaisons I agree. I'd like to have these internals exposed so I'm not adding duplicate code when I need to do things outside the framework. Things like event handlers and animations work well enough when they can be set up with HTML but when they need to be done in JS I end up adding a bunch of redundant helper functions. This is similar. |
@LinusBorg @HendrikJan note in the RFC this is actually supported: watch(() => {
console.log(obj.a)
}) There isn't anything inherently wrong with this, since mutating a watcher's own dependency in the callback would result in infinite loops too: watch(somePointer, () => {
somePointer.value++
}) |
@eladFrizi note watching multiple values can be easily done in userland: watch(
() => [currentArenaRef.value, currentUserRef.value],
([currentArena, currentUser], [prevArena, prevUser]) => {
findMatches(currentArena,currentUser ) ...
}
}) |
What's the advantage of these patterns over simply creating additional Vue instances to encapsulate what you want? Performance? Or am I missing other advantages? For example: <div id="app">
<p>app: {{ x }} {{ y }}</p>
<child></child>
</div> const mouseTracker = new Vue({
data: { x: 0, y: 0 },
created() {
window.addEventListener('mousemove', this.update)
},
destroyed() {
window.removeEventListener('mousemove', this.update)
},
methods: {
update(e) {
this.x = e.pageX
this.y = e.pageY
}
}
})
new Vue({
el: '#app',
components: {
child: {
template: `<p>child: {{ x }}, {{ y }}</p>`,
computed: {
x: () => mouseTracker.x,
y: () => mouseTracker.y,
}
},
},
computed: {
x: () => mouseTracker.x,
y: () => mouseTracker.y,
}
}) |
A direct advantage would be a smaller memory footprint as you don't have to create whole Vue instances when you only need the reactivity functionality itself. Plus, you could use that anywhere by only importing the Another advantage comes to light when combining it with #23 (Dynamic Lifecycle injection). It allows to colocate behaviours that have to be triggered at various points in a component's lifecycle in a way that is much more fluent and can be effortlessly be combined with other behaviours that are written in that way. You can find a few examples of that in that linked RFC. Together they form a pattern close to what React's new Hooks API provides. It can replace Mixins and has none of their drawbacks. |
@LinusBorg one question about the package itself. In March you wrote:
Does that mean I could install and use the reactivity system in a project without depending on Vue itself? Like |
Yes. |
Closing as part of #42. |
Rendered
API for creating and observing standalone reactive values outside components.