reaction
Core Idea
reaction is the subscriber side of an observable. It accepts a tracker function. When the tracker reads observable properties, those properties become dependencies. Once any of them is written elsewhere, the tracker runs again.
Unlike autorun, reaction also performs a dirty check on the tracker result. The subscriber is called only when the tracked value actually changes, which makes reaction more suitable for precise side effects.

Like autorun, reaction recollects dependencies on every run, so it should be disposed manually when it is no longer needed.
Description
Receive a tracker function and a callback response function. If there is observable data in the tracker, the tracker function will be executed repeatedly when the data changes, but the callback execution must be executed when the tracker function return value changes.
Signature
interface IReactionOptions<T> {
name?: string
equals?: (oldValue: T, newValue: T) => boolean // Dirty check
fireImmediately?: boolean // Is it triggered by default for the first time, bypassing the dirty check
}
interface reaction<T> {
(
tracker: () => T,
subscriber?: (newValue: T, oldValue: T) => void,
options?: IReactionOptions<T>
): void
}Example
<script setup lang="ts">
import { batch, observable, reaction } from '@formily/reactive'
import { onBeforeUnmount, ref } from 'vue'
import { pushLog } from '../observable/shared'
const obs = observable({
aa: 1,
bb: 2,
})
const rawAa = ref(obs.aa)
const rawBb = ref(obs.bb)
const trackerValue = ref(obs.aa + obs.bb)
const subscriberValue = ref<number | null>(null)
const trackerRuns = ref(0)
const subscriberRuns = ref(0)
const logs = ref<string[]>([])
const dispose = reaction(() => {
trackerRuns.value += 1
const next = obs.aa + obs.bb
trackerValue.value = next
pushLog(logs, `tracker #${trackerRuns.value}: sum = ${next}`)
return next
}, (next, prev) => {
subscriberRuns.value += 1
subscriberValue.value = next
pushLog(logs, `subscriber #${subscriberRuns.value}: ${prev} -> ${next}`)
})
function syncRawState() {
rawAa.value = obs.aa
rawBb.value = obs.bb
}
function swapWithBatch() {
batch(() => {
obs.aa = 2
obs.bb = 1
})
syncRawState()
}
function setAaToFour() {
obs.aa = 4
syncRawState()
}
function reset() {
obs.aa = 1
obs.bb = 2
rawAa.value = 1
rawBb.value = 2
trackerValue.value = 3
subscriberValue.value = null
trackerRuns.value = 0
subscriberRuns.value = 0
logs.value = []
}
syncRawState()
onBeforeUnmount(() => dispose())
</script>
<template>
<div class="playground">
<p class="hint">
<code>reaction</code> always runs the tracker, but it only calls the subscriber when the
tracked return value actually changes. Swapping <code>aa</code> and <code>bb</code> inside a
batch keeps the sum unchanged, so the subscriber does not fire.
</p>
<div class="toolbar">
<button class="btn" @click="swapWithBatch">
batch swap (2 / 1)
</button>
<button class="btn" @click="setAaToFour">
obs.aa = 4
</button>
<button class="btn secondary" @click="reset">
reset display
</button>
</div>
<div class="metrics">
<div class="metric">
<div class="label">
aa / bb
</div>
<div class="value">
{{ rawAa }} / {{ rawBb }}
</div>
</div>
<div class="metric">
<div class="label">
tracker value
</div>
<div class="value">
{{ trackerValue }}
</div>
</div>
<div class="metric">
<div class="label">
subscriber value
</div>
<div class="value">
{{ subscriberValue ?? '-' }}
</div>
</div>
<div class="metric">
<div class="label">
tracker / subscriber
</div>
<div class="value">
{{ trackerRuns }} / {{ subscriberRuns }}
</div>
</div>
</div>
<div>
<div class="sectionTitle">
Run Log
</div>
<ul class="logs">
<li v-for="(log, index) in logs" :key="`${index}-${log}`">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<style scoped src="../observable/shared.css"></style>reaction always runs the tracker, but it only calls the subscriber when the tracked return value actually changes. Swapping aa and bb inside a batch keeps the sum unchanged, so the subscriber does not fire.
- tracker #1: sum = 3
Example Code
import { batch, observable, reaction } from '@formily/reactive'
const obs = observable({
aa: 1,
bb: 2,
})
const dispose = reaction(() => {
return obs.aa + obs.bb
}, console.log)
batch(() => {
// Won't trigger because the value of obs.aa + obs.bb has not changed
obs.aa = 2
obs.bb = 1
})
obs.aa = 4
dispose()