Reactivity Fundamentals
API Preference
This page and many other chapters later in the guide contain different content for Options API and Composition API. Your current preference is Composition API. You can toggle between the API styles using the "API Preference" switches at the top of the left sidebar.
Declaring Reactive State
We can create a reactive object or array with the reactive()
function:
import { reactive } from 'kdu'
const state = reactive({ count: 0 })
Reactive objects are JavaScript Proxies and behave just like normal objects. The difference is that Kdu is able to track the property access and mutations of a reactive object. If you are curious about the details, we explain how Kdu's reactivity system works in Reactivity in Depth - but we recommend reading it after you have finished the main guide.
See also: Typing Reactive
To use reactive state in a component's template, declare and return them from a component's setup()
function:
import { reactive } from 'kdu'
export default {
// `setup` is a special hook dedicated for composition API.
setup() {
const state = reactive({ count: 0 })
// expose the state to the template
return {
state
}
}
}
<div>{{ state.count }}</div>
Similarly, we can declare functions that mutate reactive state in the same scope, and expose it as a method alongside the state:
import { reactive } from 'kdu'
export default {
setup() {
const state = reactive({ count: 0 })
function increment() {
state.count++
}
// don't forget to expose the function as well.
return {
state,
increment
}
}
}
Exposed methods are typically used as event listeners:
<button @click="increment">
{{ state.count }}
</button>
<script setup>
Manually exposing state and methods via setup()
can be verbose. Luckily, it is only necessary when not using a build step. When using Single-File Components (SFCs), we can greatly simplify the usage with <script setup>
:
<script setup>
import { reactive } from 'kdu'
const state = reactive({ count: 0 })
function increment() {
state.count++
}
</script>
<template>
<button @click="increment">
{{ state.count }}
</button>
</template>
Top-level imports and variables declared in <script setup>
are automatically usable in the template of the same component.
For the rest of the guide, we will be primarily using SFC +
<script setup>
syntax for Composition API code examples, as that is the most common usage for Kdu developers.
DOM Update Timing
When you mutate reactive state, the DOM is updated automatically. However, it should be noted that the DOM updates are not applied synchronously. Instead, Kdu buffers them until the "next tick" in the update cycle to ensure that each component needs to update only once no matter how many state changes you have made.
To wait for the DOM update to complete after a state change, you can use the nextTick() global API:
import { nextTick } from 'kdu'
function increment() {
state.count++
nextTick(() => {
// access updated DOM
})
}
Deep Reactivity
In Kdu, state is deeply reactive by default. This means you can expect changes to be detected even when you mutate nested objects or arrays:
import { reactive } from 'kdu'
const obj = reactive({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// these will work as expected.
obj.nested.count++
obj.arr.push('baz')
}
It is also possible to explicitly create shallow reactive objects where the reactivity is only tracked at the root-level, however they are typically only needed in advanced use cases.
Reactive Proxy vs. Original
It is important to note that the returned value from reactive()
is a Proxy of the original object, which is not equal to the original object:
const raw = {}
const proxy = reactive(raw)
// proxy is NOT equal to the original.
console.log(proxy === raw) // false
Only the proxy is reactive - mutating the original object will not trigger updates. Therefore, the best practice when working with Kdu's reactivity system is to exclusively use the proxied versions of your state.
To ensure consistent access to the proxy, calling reactive()
on the same object always returns the same proxy, and calling reactive()
on an existing proxy also returns that same proxy:
// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true
// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true
This rule applies to nested objects as well. Due to deep reactivity, nested objects inside a reactive object are also proxies:
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
Limitations of reactive()
The reactive()
API has two limitations:
It only works for object types (objects, arrays, and collection types such as
Map
andSet
). It cannot hold primitive types such asstring
,number
orboolean
.Since Kdu's reactivity tracking works over property access, we must always keep the same reference to the reactive object. This means we can't easily "replace" a reactive object because the reactivity connection to the first reference is lost:
let state = reactive({ count: 0 }) // the above reference ({ count: 0 }) is no longer being tracked (reactivity connection is lost!) state = reactive({ count: 1 })
It also means that when we assign or destructure a reactive object's property into local variables, or when we pass that property into a function, we will lose the reactivity connection:
const state = reactive({ count: 0 }) // n is a local variable that is disconnected // from state.count. let n = state.count // does not affect original state n++ // count is also disconnected from state.count. let { count } = state // does not affect original state count++ // the function receives a plain number and // won't be able to track changes to state.count callSomeFunction(state.count)
Reactive Variables with ref()
To address the limitations of reactive()
, Kdu also provides a ref()
function which allows us to create reactive "refs" that can hold any value type:
import { ref } from 'kdu'
const count = ref(0)
ref()
takes the argument and returns it wrapped within a ref object with a .value
property:
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
See also: Typing Refs
Similar to properties on a reactive object, the .value
property of a ref is reactive. In addition, when holding object types, ref automatically converts its .value
with reactive()
.
A ref containing an object value can reactively replace the entire object:
const objectRef = ref({ count: 0 })
// this works reactively
objectRef.value = { count: 1 }
Refs can also be passed into functions or destructured from plain objects without losing reactivity:
const obj = {
foo: ref(1),
bar: ref(2)
}
// the function receives a ref
// it needs to access the value via .value but it
// will retain the reactivity connection
callSomeFunction(obj.foo)
// still reactive
const { foo, bar } = obj
In other words, ref()
allows us to create a "reference" to any value and pass it around without losing reactivity. This capability is quite important as it is frequently used when extracting logic into Composable Functions.
Ref Unwrapping in Templates
When refs are accessed as top-level properties in the template, they are automatically "unwrapped" so there is no need to use .value
. Here's the previous counter example, using ref()
instead:
<script setup>
import { ref } from 'kdu'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }} <!-- no .value needed -->
</button>
</template>
Note the unwrapping only applies if the ref is a top-level property on the template render context. As an example, foo
is a top-level property, but object.foo
is not.
So given the following object:
const object = { foo: ref(1) }
The following expression will NOT work as expected:
{{ object.foo + 1 }}
The rendered result will be [object Object]
because object.foo
is a ref object. We can fix that by making foo
a top-level property:
const { foo } = object
{{ foo + 1 }}
Now the render result will be 2
.
One thing to note is that a ref will also be unwrapped if it is the final evaluated value of a text interpolation (i.e. a {{ }}
tag), so the following will render 1
:
{{ object.foo }}
This is just a convenience feature of text interpolation and is equivalent to {{ object.foo.value }}
.
Ref Unwrapping in Reactive Objects
When a ref
is accessed or mutated as a property of a reactive object, it is also automatically unwrapped so it behaves like a normal property:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
If a new ref is assigned to a property linked to an existing ref, it will replace the old ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// original ref is now disconnected from state.count
console.log(count.value) // 1
Ref unwrapping only happens when nested inside a deep reactive object. It does not apply when it is accessed as a property of a shallow reactive object.
Ref Unwrapping in Arrays and Collections
Unlike reactive objects, there is no unwrapping performed when the ref is accessed as an element of a reactive array or a native collection type like Map
:
const books = reactive([ref('Kdu 3 Guide')])
// need .value here
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// need .value here
console.log(map.get('count').value)
Reactivity Transform
Having to use .value
with refs is a drawback imposed by the language constraints of JavaScript. However, with compile-time transforms we can improve the ergonomics by automatically appending .value
in appropriate locations. Kdu provides a compile-time transform that allows us to write the earlier "counter" example like this:
<script setup>
let count = $ref(0)
function increment() {
// no need for .value
count++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
You can learn more about Reactivity Transform in its dedicated section. Do note that it is currently still experimental and may change before being finalized.