@oscar_spen
Reactivity is a style of programming that allows us to declaratively react to changes.
If we store the sum of two variables and then reassign those variables, the original sum doesn't change.
let x = 5
let y = 7
let sum = x + y
console.log(sum) // sum is 12
y = 19
console.log(sum) // sum is still 12
We can make sure that we always update the sum
value whenever we update one of the operands, but doing so would be doing so imperatively.
Having the ability to declare that the sum
value is always equal to the sum of the two operands is declarative.
let x = 5
let y = 7
let sum = x + y
function updateY(value) {
y = value
sum = x + y
}
console.log(sum) // sum is 12
updateY(19)
console.log(sum) // sum is now 24!
How would we make something reactive in JS?
Proxies allow us to intercept actions performed on objects.
user code </>
target object {}
Proxies allow us to intercept actions performed on objects.
user code </>
target object {}
Proxy
Handlers define what code should run when object properties are accessed or changed.
let person = {
name: 'Bob Smith',
age: 35
}
let handler = {
get(target, prop) {
return target[prop]
}
}
person = new Proxy(person, handler)
let person = {
name: 'Bob Smith',
age: 35
}
let handler = {
get(target, prop) {
if (prop === 'age') {
console.warn("It's rude to ask someone's age, but sure.")
}
return target[prop]
}
}
person = new Proxy(person, handler)
console.log(person.name) // Bob Smith
console.log(person.age) // insightful message, then 35
let handler = {
get(target, prop) {
if (prop === 'age') {
console.warn("It's rude to ask someone's age, but sure.")
}
return target[prop]
}
set(target, prop, value) {
if (prop === 'name' && !value) {
return target[prop] = 'Anonymous'
}
return target[prop] = value
}
}
You can specify actions for just about anything that can be done to an Object.
apply construct defineProperty deleteProperty get getOwnPropertyDescriptor getPrototypeOf has isExtensible ownKeys preventExtensions set setPrototypeOf
Quick reminder of our three steps to reactivity:
We don't have to explicitly do this since Proxies allow us to perform an action right when the change happens.
/* the best code
is no code at all */
We can track a calling function whenever one of our fields are accessed.
const handler = {
get(target, prop) {
track(target, prop)
return target[prop]
},
set(target, key, value) {
trigger(target, key)
return target[key] = value
}
}
We can trigger an effect whenever one of our fields are changed.
const handler = {
get(target, prop) {
track(target, prop)
return target[prop]
},
set(target, key, value) {
trigger(target, key)
return target[key] = value
}
}
We now have all of the parts we need to register effects and have them be triggered whenever its dependencies are updated.
This yields a great API, since the user can just use objects normally.
{
data() {
return {
foo: 'Vue'
}
}
)
Vue will recursively create Proxies for the object you return from your data function, and track when there have been changes.
Formerly known as Vue.observable, this function converts any object into a reactive one.
Vue.reactive
import { reactive } from 'vue'
reactive({
name: 'Bob Smith',
age: 35
})
// store.js
const state = Vue.reactive({
foo: 'Vue'
})
const getters = {
theFoo() {
return state.foo
}
}
const actions = {
setFoo(value) {
state.foo = value
}
}
export {
getters,
actions
}
// VueComponent.js
import store from './store.js'
export default {
computed: {
...store.getters
},
methods: {
...store.actions,
doSomethingWithFoo() {
return this.theFoo
}
}
}
Vue.reactive
observable -> reactive effect -> watchEffect ref computed watch
Refs make standalone values reactive.
When used in Vue templates, there's no need to access .value
.
import { ref } from 'vue'
const lightsOn = ref(true)
console.log(lightsOn.value) // true
lightsOn.value = false
console.log(lightsOn.value) // false
You can now directly make computed values.
const lightsOn = ref(true)
const msg = computed(() => {
if (lightsOn.value) {
return 'The lights are on!'
} else {
return 'The lights are off.'
}
}
console.log(msg.value) // lights on
lightsOn.value = false
console.log(msg.value) // lights off
const counter = reactive({ num: 0 })
let currentNumber
watchEffect(() => {
currentNumber = counter.num
})
console.log(currentNumber) // 0
counter.num++
console.log(currentNumber) // 1
watchEffect
allows us to register effects that will run whenever the dependencies of that effect change.
const myMap = reactive({ foo: 1 })
let keys
watchEffect(() => {
keys = Object.keys(myMap)
})
console.log(keys) // [ 'foo' ]
myMap.bar = 2
console.log(keys) // [ 'foo', 'bar' ]
It also tracks changes for most of the ways that you can interact with an object, including doing something like listing its keys.
const counter = reactive({ num: 0 })
watch(() => counter.num, (num, oldNum) => {
// do your thing, homie
})
watch
allows us to explicitly declare when an effect should run, via functions or refs directly.
const counter = ref(0)
watch(counter, (num, oldNum) => {
// do what you gotta do
})
(show everyone your bad gorgeous code)
Software Engineer @
Twitter: @oscar_spen
GitHub: ospencer
grain-lang.org