API
observer returns a higher-order component that wraps whatever you pass in, so calling useObserver from inside a component still inserts an extra level in the tree. If you are building your own components, prefer calling useObserver directly so the reactive syncing stays local. The observer helper exists in @formily/vue primarily to keep Vue 2 compatibility, but for Vue 3-centric codebases the hook should be your first choice.
Internally, useObserver rewires Vue 3 reactivity with a non-public API. If you would rather avoid that hack, reach for formilyComputed, which exposes the Formily reaction as a standard Vue ComputedRef so most custom wrappers never need to touch useObserver.
observer
Description
Turns the component render function into a Formily Reaction. Every re-render collects dependencies, and only the exact reactive fields are tracked for updates.
Signature
interface IObserverOptions {
scheduler?: (updater: () => void) => void // optionally control when updates run
name?: string // name of the wrapped component
}
interface observer<T extends VueComponent> {
(component: T, options?: IObserverOptions): T
}Usage
<script>
import { observable } from '@formily/reactive'
import { observer } from '@silver-formily/reactive-vue'
export default observer({
data() {
// 能与 vue 的响应系统共存
const obs = observable({
value: 'Hello world',
})
return {
obs,
}
},
})
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="(e) => {
obs.value = e.target.value
}"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template><script>
import { observable } from '@formily/reactive'
import { observer } from '@silver-formily/reactive-vue'
export default observer({
data() {
// 能与 vue 的响应系统共存
const obs = observable({
value: 'Hello world',
})
return {
obs,
}
},
})
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="(e) => {
obs.value = e.target.value
}"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template>
useObserver Recommended
The hook that powers observer. Prefer calling it in setup/<script setup> to avoid extra wrapper components.
Signature
// Same options as observer
interface IObserverOptions {
scheduler?: (updater: () => void) => void
name?: string
}
interface useObserver {
(options?: IObserverOptions): void
}Usage
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { useObserver } from '@silver-formily/reactive-vue'
useObserver()
const obs = observable({
value: 'Hello world',
})
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template><script setup lang="ts">
import { observable } from '@formily/reactive'
import { useObserver } from '@silver-formily/reactive-vue'
useObserver()
const obs = observable({
value: 'Hello world',
})
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template>
formilyComputed 1.0.0
Converts a @formily/reactive computation into a Vue 3 ComputedRef. You can use it anywhere Vue expects a computed value (Pinia, props, etc.).
Signature
import type { IReactionOptions } from '@formily/reactive'
import type { ComputedRef } from 'vue'
// options default to { fireImmediately: true }
interface formilyComputed {
<T>(tracker: () => T, options?: IReactionOptions): ComputedRef<T>
}Usage
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { formilyComputed } from '@silver-formily/reactive-vue'
const obs = observable({
value: 'Hello formilyComputed',
})
const uppercaseValue = formilyComputed(() => obs.value.toUpperCase())
const charCount = formilyComputed(() => obs.value.length)
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>原始:{{ obs.value }}</div>
<div>formilyComputed(大写):{{ uppercaseValue }}</div>
<div>formilyComputed(长度):{{ charCount }}</div>
</div>
</template><script setup lang="ts">
import { observable } from '@formily/reactive'
import { formilyComputed } from '@silver-formily/reactive-vue'
const obs = observable({
value: 'Hello formilyComputed',
})
const uppercaseValue = formilyComputed(() => obs.value.toUpperCase())
const charCount = formilyComputed(() => obs.value.length)
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>原始:{{ obs.value }}</div>
<div>formilyComputed(大写):{{ uppercaseValue }}</div>
<div>formilyComputed(长度):{{ charCount }}</div>
</div>
</template>
autorunEffect 1.1.0
Wraps autorun as a Vue composable. It subscribes immediately inside setup() or any active effect scope, automatically calls dispose when that scope is disposed, and still returns a stopper so you can end it earlier when needed.
Signature
import type { Dispose, Reaction } from '@formily/reactive'
interface autorunEffect {
(tracker: Reaction, name?: string): Dispose
}Usage
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { autorunEffect } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
count: 0,
})
const writtenCount = ref(0)
const trackedCount = ref(0)
const runCount = ref(0)
const active = ref(true)
const stop = autorunEffect(() => {
runCount.value += 1
trackedCount.value = state.count
})
function syncWrittenCount() {
writtenCount.value = state.count
}
function increment() {
state.count += 1
syncWrittenCount()
}
function addTen() {
state.count += 10
syncWrittenCount()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="increment">
count + 1
</button>
<button class="demoButton" @click="addTen">
count + 10
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>手动写入值:{{ writtenCount }}</div>
<div>autorun 同步值:{{ trackedCount }}</div>
<div>autorun 执行次数:{{ runCount }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style><script setup lang="ts">
import { observable } from '@formily/reactive'
import { autorunEffect } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
count: 0,
})
const writtenCount = ref(0)
const trackedCount = ref(0)
const runCount = ref(0)
const active = ref(true)
const stop = autorunEffect(() => {
runCount.value += 1
trackedCount.value = state.count
})
function syncWrittenCount() {
writtenCount.value = state.count
}
function increment() {
state.count += 1
syncWrittenCount()
}
function addTen() {
state.count += 10
syncWrittenCount()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="increment">
count + 1
</button>
<button class="demoButton" @click="addTen">
count + 10
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>手动写入值:{{ writtenCount }}</div>
<div>autorun 同步值:{{ trackedCount }}</div>
<div>autorun 执行次数:{{ runCount }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style>
reactionWatch 1.1.0
Wraps reaction as a Vue composable. It keeps the same tracker, subscriber, and options API, but wires the disposer into Vue scope disposal so subscriptions are released automatically after unmount or scope teardown.
Signature
import type { Dispose, IReactionOptions } from '@formily/reactive'
interface reactionWatch {
<T>(
tracker: () => T,
subscriber?: (value: T, oldValue: T) => void,
options?: IReactionOptions<T>,
): Dispose
}Usage
<script setup lang="ts">
import { batch, observable } from '@formily/reactive'
import { reactionWatch } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
aa: 1,
bb: 2,
})
const rawAa = ref(state.aa)
const rawBb = ref(state.bb)
const trackerValue = ref(state.aa + state.bb)
const subscriberValue = ref<number | null>(null)
const subscriberRuns = ref(0)
const active = ref(true)
const stop = reactionWatch(
() => {
const next = state.aa + state.bb
trackerValue.value = next
return next
},
(next) => {
subscriberRuns.value += 1
subscriberValue.value = next
},
)
function syncRawState() {
rawAa.value = state.aa
rawBb.value = state.bb
}
function swapValues() {
batch(() => {
state.aa = 2
state.bb = 1
})
syncRawState()
}
function setAaToFour() {
state.aa = 4
syncRawState()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="swapValues">
batch swap
</button>
<button class="demoButton" @click="setAaToFour">
aa = 4
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>aa / bb:{{ rawAa }} / {{ rawBb }}</div>
<div>tracker 结果:{{ trackerValue }}</div>
<div>subscriber 结果:{{ subscriberValue ?? '-' }}</div>
<div>subscriber 次数:{{ subscriberRuns }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style><script setup lang="ts">
import { batch, observable } from '@formily/reactive'
import { reactionWatch } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
aa: 1,
bb: 2,
})
const rawAa = ref(state.aa)
const rawBb = ref(state.bb)
const trackerValue = ref(state.aa + state.bb)
const subscriberValue = ref<number | null>(null)
const subscriberRuns = ref(0)
const active = ref(true)
const stop = reactionWatch(
() => {
const next = state.aa + state.bb
trackerValue.value = next
return next
},
(next) => {
subscriberRuns.value += 1
subscriberValue.value = next
},
)
function syncRawState() {
rawAa.value = state.aa
rawBb.value = state.bb
}
function swapValues() {
batch(() => {
state.aa = 2
state.bb = 1
})
syncRawState()
}
function setAaToFour() {
state.aa = 4
syncRawState()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="swapValues">
batch swap
</button>
<button class="demoButton" @click="setAaToFour">
aa = 4
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>aa / bb:{{ rawAa }} / {{ rawBb }}</div>
<div>tracker 结果:{{ trackerValue }}</div>
<div>subscriber 结果:{{ subscriberValue ?? '-' }}</div>
<div>subscriber 次数:{{ subscriberRuns }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style>