API
observer wraps your function component with React.memo and calls useObserver inside that wrapper, which inserts one extra level into the tree (static members of the original component are hoisted automatically). In React, observer should be your first choice; reach for useObserver directly only when you need finer control over dependency collection, for example inside a custom render pipeline.
observer / useObserver create a Tracker during rendering, retain it with useCompatFactory, and use GC as a fallback for uncommitted renders. useComputed, useAutorunEffect, and useReactionWatch subscribe in ordinary useEffect callbacks and unsubscribe synchronously during cleanup; StrictMode replay cleans up before rebuilding. Only useCompatEffect retains the same setup and cleanup across synchronous replay.
observer Recommended
Description
Turns a function component into a reactive one: dependencies are collected on every render, and updates re-render only the components that actually depend on the changed fields. The returned component is wrapped in React.memo and hoists the statics of the original component.
Signature
interface IObserverOptions {
forwardRef?: boolean // forward refs to the wrapped component
scheduler?: (updater: () => void) => void // optionally control when updates run
displayName?: string // displayName of the wrapped component
}
// The package declaration contains the full overloads, including generic and ref inference.
declare const observer: typeof import('@silver-formily/reactive-react').observerComponent and ref types
Accepts plain function components, memo(Component), forwardRef(render), and nested combinations. Existing memo comparators are preserved; reactive dependency changes can still trigger updates. Plain function component generic call signatures and custom statics are retained. Declare children explicitly in component props.
React 19 allows ref props directly. For React 18 compatibility, pass a forwardRef component or set { forwardRef: true }; the latter merges the ref into the original function's props. Declare a specific ref element type to reject mismatched refs.
import type { Ref } from 'react'
import { observer } from '@silver-formily/reactive-react'
const Input = observer<{ value: string, ref?: Ref<HTMLInputElement> }>(
props => <input ref={props.ref} value={props.value} readOnly />,
{ forwardRef: true },
)Usage
import { observable } from '@silver-formily/reactive'
import { observer } from '@silver-formily/reactive-react'
import './demoStyles.css'
const obs = observable({
value: 'Hello world',
})
// observer 返回的组件在渲染时收集依赖,依赖变化只触发当前组件重渲染
const ObservedInput = observer(() => {
return (
<div>
<input
className="demoInput"
value={obs.value}
onChange={(event) => {
obs.value = event.target.value
}}
/>
<div className="demoText">{obs.value}</div>
</div>
)
})
export default ObservedInputimport { observable } from '@silver-formily/reactive'
import { observer } from '@silver-formily/reactive-react'
import './demoStyles.css'
const obs = observable({
value: 'Hello world',
})
// observer 返回的组件在渲染时收集依赖,依赖变化只触发当前组件重渲染
const ObservedInput = observer(() => {
return (
<div>
<input
className="demoInput"
value={obs.value}
onChange={(event) => {
obs.value = event.target.value
}}
/>
<div className="demoText">{obs.value}</div>
</div>
)
})
export default ObservedInput
Observer
Description
A render component accepting a render function or static children. It is handy for subscribing only a slice of JSX: when children is a function, the reactive fields read inside it only re-render the Observer subtree, leaving the parent untouched.
Signature
interface IObserverProps {
children?: (() => React.ReactNode) | React.ReactNode
}Usage
import { observable } from '@silver-formily/reactive'
import { Observer } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({
count: 0,
})
// Observer 只订阅 children 区域:obs.count 变化只重渲染 Observer 内部,
// 外层组件的渲染次数不会增加
export default function ObserverDemo() {
const [outerRenders, setOuterRenders] = useState(1)
return (
<div>
<div className="demoToolbar">
<button className="demoButton" onClick={() => obs.count++}>
count + 1
</button>
<button
className="demoButton secondary"
onClick={() => setOuterRenders(value => value + 1)}
>
强制重渲染外层
</button>
</div>
<div>
外层组件渲染次数:
{outerRenders}
</div>
<div className="demoText">
Observer 订阅值:
<Observer>{() => <b>{obs.count}</b>}</Observer>
</div>
</div>
)
}import { observable } from '@silver-formily/reactive'
import { Observer } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({
count: 0,
})
// Observer 只订阅 children 区域:obs.count 变化只重渲染 Observer 内部,
// 外层组件的渲染次数不会增加
export default function ObserverDemo() {
const [outerRenders, setOuterRenders] = useState(1)
return (
<div>
<div className="demoToolbar">
<button className="demoButton" onClick={() => obs.count++}>
count + 1
</button>
<button
className="demoButton secondary"
onClick={() => setOuterRenders(value => value + 1)}
>
强制重渲染外层
</button>
</div>
<div>
外层组件渲染次数:
{outerRenders}
</div>
<div className="demoText">
Observer 订阅值:
<Observer>{() => <b>{obs.count}</b>}</Observer>
</div>
</div>
)
}
useObserver
The hook that powers observer. It tracks the reactive dependencies of the view function inside the current component and returns its result.
Signature
interface IObserverOptions {
scheduler?: (updater: () => void) => void
displayName?: string
}
interface useObserver<T extends () => unknown> {
(view: T, options?: IObserverOptions): ReturnType<T>
}Usage
import { observable } from '@silver-formily/reactive'
import { useObserver } from '@silver-formily/reactive-react'
import './demoStyles.css'
const obs = observable({
value: 'Hello world',
})
// useObserver 是 observer 的内部实现,直接在组件内追踪 view 函数的依赖
function ObservedInput() {
return useObserver(() => {
return (
<div>
<input
className="demoInput"
value={obs.value}
onChange={(event) => {
obs.value = event.target.value
}}
/>
<div className="demoText">{obs.value}</div>
</div>
)
})
}
export default ObservedInputimport { observable } from '@silver-formily/reactive'
import { useObserver } from '@silver-formily/reactive-react'
import './demoStyles.css'
const obs = observable({
value: 'Hello world',
})
// useObserver 是 observer 的内部实现,直接在组件内追踪 view 函数的依赖
function ObservedInput() {
return useObserver(() => {
return (
<div>
<input
className="demoInput"
value={obs.value}
onChange={(event) => {
obs.value = event.target.value
}}
/>
<div className="demoText">{obs.value}</div>
</div>
)
})
}
export default ObservedInput
useAutorunEffect 1.0.0
Creates an autorun after commit, runs the tracker initially and on reactive dependency changes, and disposes synchronously during effect cleanup. Development StrictMode may run the initial tracker more than once, but does not retain duplicate subscriptions.
The tracker uses the latest committed closure. Pass options.deps when props/state change the expression or observable being tracked; refreshing the callback alone does not run the tracker. Omitting deps subscribes on mount and reacts to observable changes without rebuilding on every render. name is read when subscribing.
Use useReactionWatch to notify only when a value changes.
Signature
import type { Reaction } from '@silver-formily/reactive'
import type { DependencyList } from 'react'
interface IAutorunEffectOptions {
name?: string // passed to the underlying autorun when subscribing
deps?: DependencyList // non-observable inputs that rebuild tracking
}
declare function useAutorunEffect(tracker: Reaction, options?: IAutorunEffectOptions): voidUsage
import { observable } from '@silver-formily/reactive'
import { useAutorunEffect } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({ count: 0 })
function AutoRunner({ onLog, multiplier }: { onLog: (message: string) => void, multiplier: number }) {
// observable 变化自动重跑;props 改变表达式时通过 deps 重建追踪
useAutorunEffect(() => {
onLog(`autorun:${obs.count} × ${multiplier} = ${obs.count * multiplier}`)
}, { name: 'count-times-multiplier', deps: [multiplier] })
return <div className="demoText">useAutorunEffect 挂载中</div>
}
export default function AutorunEffectDemo() {
const [active, setActive] = useState(true)
const [multiplier, setMultiplier] = useState(1)
const [logs, setLogs] = useState<string[]>([])
const log = (message: string) => {
setLogs(prev => [...prev, message])
}
return (
<div>
<div className="demoToolbar">
<button
className="demoButton"
onClick={() => {
obs.count++
}}
>
count +1
</button>
<button
className="demoButton"
onClick={() => {
setActive(value => !value)
}}
>
{active ? '卸载子组件' : '重新挂载子组件'}
</button>
<button className="demoButton" onClick={() => setMultiplier(value => value === 1 ? 2 : 1)}>
切换倍数(当前
{' '}
{multiplier}
)
</button>
</div>
<div className="demoText">
当前 count:
{obs.count}
</div>
{active && <AutoRunner onLog={log} multiplier={multiplier} />}
<div>
{logs.slice(-5).map((log, index) => (
<div key={index} className="demoText secondary">{log}</div>
))}
</div>
</div>
)
}import { observable } from '@silver-formily/reactive'
import { useAutorunEffect } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({ count: 0 })
function AutoRunner({ onLog, multiplier }: { onLog: (message: string) => void, multiplier: number }) {
// observable 变化自动重跑;props 改变表达式时通过 deps 重建追踪
useAutorunEffect(() => {
onLog(`autorun:${obs.count} × ${multiplier} = ${obs.count * multiplier}`)
}, { name: 'count-times-multiplier', deps: [multiplier] })
return <div className="demoText">useAutorunEffect 挂载中</div>
}
export default function AutorunEffectDemo() {
const [active, setActive] = useState(true)
const [multiplier, setMultiplier] = useState(1)
const [logs, setLogs] = useState<string[]>([])
const log = (message: string) => {
setLogs(prev => [...prev, message])
}
return (
<div>
<div className="demoToolbar">
<button
className="demoButton"
onClick={() => {
obs.count++
}}
>
count +1
</button>
<button
className="demoButton"
onClick={() => {
setActive(value => !value)
}}
>
{active ? '卸载子组件' : '重新挂载子组件'}
</button>
<button className="demoButton" onClick={() => setMultiplier(value => value === 1 ? 2 : 1)}>
切换倍数(当前
{' '}
{multiplier}
)
</button>
</div>
<div className="demoText">
当前 count:
{obs.count}
</div>
{active && <AutoRunner onLog={log} multiplier={multiplier} />}
<div>
{logs.slice(-5).map((log, index) => (
<div key={index} className="demoText secondary">{log}</div>
))}
</div>
</div>
)
}
useReactionWatch 1.0.0
Creates a reaction after commit and disposes synchronously during effect cleanup. By default the subscriber runs only when the tracker value changes. fireImmediately: true also notifies whenever a subscription is established; equals customizes equality.
Both tracker and subscriber use their latest committed closures. A changed onChange callback does not require resubscribing. Declare a changed store or props/state used by the tracker in options.deps. Dependency changes clean up the old subscription and establish a new one with the latest options and a fresh comparison baseline; oldValue is not preserved across subscriptions. Options such as equals and name are read when subscribing.
Signature
import type { IReactionOptions } from '@silver-formily/reactive'
import type { DependencyList } from 'react'
interface IReactionWatchOptions<T> extends IReactionOptions<T> {
deps?: DependencyList
}
declare function useReactionWatch<T>(
tracker: () => T,
subscriber?: (value: T, oldValue: T) => void,
options?: IReactionWatchOptions<T>,
): voidUsage
import { observable } from '@silver-formily/reactive'
import { useReactionWatch } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({ keyword: '' })
function KeywordWatcher({ onLog }: { onLog: (message: string) => void }) {
// onLog 使用最新提交的回调;无需因为回调引用改变而重建订阅
useReactionWatch(
() => obs.keyword,
(value, oldValue) => {
onLog(`keyword:${oldValue || '(空)'} → ${value || '(空)'}`)
},
)
return <div className="demoText">useReactionWatch 挂载中</div>
}
export default function ReactionWatchDemo() {
const [active, setActive] = useState(true)
const [logs, setLogs] = useState<string[]>([])
const log = (message: string) => {
setLogs(prev => [...prev, message])
}
return (
<div>
<div className="demoToolbar">
<input
className="demoInput"
value={obs.keyword}
placeholder="输入关键词"
onChange={(event) => {
obs.keyword = event.target.value
}}
/>
<button
className="demoButton"
onClick={() => {
setActive(value => !value)
}}
>
{active ? '卸载子组件' : '重新挂载子组件'}
</button>
</div>
{active && <KeywordWatcher onLog={log} />}
<div>
{logs.slice(-5).map((log, index) => (
<div key={index} className="demoText secondary">{log}</div>
))}
</div>
</div>
)
}import { observable } from '@silver-formily/reactive'
import { useReactionWatch } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({ keyword: '' })
function KeywordWatcher({ onLog }: { onLog: (message: string) => void }) {
// onLog 使用最新提交的回调;无需因为回调引用改变而重建订阅
useReactionWatch(
() => obs.keyword,
(value, oldValue) => {
onLog(`keyword:${oldValue || '(空)'} → ${value || '(空)'}`)
},
)
return <div className="demoText">useReactionWatch 挂载中</div>
}
export default function ReactionWatchDemo() {
const [active, setActive] = useState(true)
const [logs, setLogs] = useState<string[]>([])
const log = (message: string) => {
setLogs(prev => [...prev, message])
}
return (
<div>
<div className="demoToolbar">
<input
className="demoInput"
value={obs.keyword}
placeholder="输入关键词"
onChange={(event) => {
obs.keyword = event.target.value
}}
/>
<button
className="demoButton"
onClick={() => {
setActive(value => !value)
}}
>
{active ? '卸载子组件' : '重新挂载子组件'}
</button>
</div>
{active && <KeywordWatcher onLog={log} />}
<div>
{logs.slice(-5).map((log, index) => (
<div key={index} className="demoText secondary">{log}</div>
))}
</div>
</div>
)
}
useComputed 1.0.0
Bridges a Formily reactive expression into component state: it returns the latest value of the expression and re-renders when its dependencies change. Handy for consuming Formily reactive data inside non-observer components.
The getter should only read Formily observables; when it reads props/state or other non-reactive data, declare them via options.deps, otherwise the closure stays on the first render and misses later updates. The remaining options (equals, name) are read when subscribing and refreshed when deps change. The initial value is calculated synchronously; later deps changes rebuild tracking and synchronize the result in an effect, so the current render may initially receive the old value. Results can be functions; the hook does not execute them as state updaters.
fireImmediately is not configurable: rebuilding always publishes the current result. Use useReactionWatch if you need to control notifications.
Signature
import type { IReactionOptions } from '@silver-formily/reactive'
import type { DependencyList } from 'react'
interface IComputedOptions<T> extends Omit<IReactionOptions<T>, 'fireImmediately'> {
deps?: DependencyList // props/state the getter depends on; changing them rebuilds tracking with the fresh getter
}
interface useComputed<T> {
(getter: () => T, options?: IComputedOptions<T>): T
}Usage
import { observable } from '@silver-formily/reactive'
import { useComputed } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({ price: 100, count: 2 })
function Total({ discount }: { discount: number }) {
// getter 只读 Formily observable;discount 来自 props,
// 通过 deps 声明后变化时会用新 getter 重建追踪
const total = useComputed(
() => obs.price * obs.count * discount,
{ deps: [discount] },
)
return (
<div className="demoText">
合计:¥
{total}
<span className="secondary">
(
{discount}
{' '}
折)
</span>
</div>
)
}
export default function ComputedDemo() {
const [discount, setDiscount] = useState(1)
return (
<div>
<div className="demoToolbar">
<button
className="demoButton"
onClick={() => {
obs.count++
}}
>
数量 +1
</button>
<button
className="demoButton"
onClick={() => {
setDiscount(value => (value === 1 ? 0.8 : 1))
}}
>
切换折扣
</button>
</div>
<div className="demoText">
单价 ¥
{obs.price}
{' '}
× 数量
{' '}
{obs.count}
</div>
<Total discount={discount} />
</div>
)
}import { observable } from '@silver-formily/reactive'
import { useComputed } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
const obs = observable({ price: 100, count: 2 })
function Total({ discount }: { discount: number }) {
// getter 只读 Formily observable;discount 来自 props,
// 通过 deps 声明后变化时会用新 getter 重建追踪
const total = useComputed(
() => obs.price * obs.count * discount,
{ deps: [discount] },
)
return (
<div className="demoText">
合计:¥
{total}
<span className="secondary">
(
{discount}
{' '}
折)
</span>
</div>
)
}
export default function ComputedDemo() {
const [discount, setDiscount] = useState(1)
return (
<div>
<div className="demoToolbar">
<button
className="demoButton"
onClick={() => {
obs.count++
}}
>
数量 +1
</button>
<button
className="demoButton"
onClick={() => {
setDiscount(value => (value === 1 ? 0.8 : 1))
}}
>
切换折扣
</button>
</div>
<div className="demoText">
单价 ¥
{obs.price}
{' '}
× 数量
{' '}
{obs.count}
</div>
<Total discount={discount} />
</div>
)
}
useCompatEffect Advanced
Provides a retained-resource effect lifecycle for integration layers: synchronous StrictMode replay reuses the same setup and cleanup without allocating duplicate resources. Changed deps dispose the old resource synchronously before the next setup; omitted deps refresh the resource on every commit. Final unmount cleanup is deferred by one microtask. React compares dependency entries with Object.is, not deep equality.
@silver-formily/react relies on it internally to keep Field onMount / onUnmount from being triggered by StrictMode replays. Reach for it when building your own integration layer that needs precise subscription lifecycles; regular components should just use the native useEffect.
Signature
import type { DependencyList, EffectCallback } from 'react'
interface useCompatEffect {
(effect: EffectCallback, deps?: DependencyList): void
}Usage
import { useCompatEffect } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
function Subscription(
{ onStatusChange }: { onStatusChange: (status: string) => void },
) {
useCompatEffect(() => {
onStatusChange('订阅已建立')
return () => {
onStatusChange('订阅已清理(组件真实卸载)')
}
}, [])
return <div className="demoText">子组件挂载中</div>
}
// 同步 StrictMode 重放复用同一份 setup 和 cleanup;
// 最终卸载的 cleanup 延迟一个微任务
export default function CompatEffectDemo() {
const [active, setActive] = useState(true)
const [status, setStatus] = useState('尚未建立订阅')
return (
<div>
<div className="demoToolbar">
<button
className="demoButton"
onClick={() => {
setActive(value => !value)
}}
>
{active ? '卸载子组件' : '重新挂载子组件'}
</button>
</div>
<div>
当前状态:
{status}
</div>
{active && <Subscription onStatusChange={setStatus} />}
</div>
)
}import { useCompatEffect } from '@silver-formily/reactive-react'
import { useState } from 'react'
import './demoStyles.css'
function Subscription(
{ onStatusChange }: { onStatusChange: (status: string) => void },
) {
useCompatEffect(() => {
onStatusChange('订阅已建立')
return () => {
onStatusChange('订阅已清理(组件真实卸载)')
}
}, [])
return <div className="demoText">子组件挂载中</div>
}
// 同步 StrictMode 重放复用同一份 setup 和 cleanup;
// 最终卸载的 cleanup 延迟一个微任务
export default function CompatEffectDemo() {
const [active, setActive] = useState(true)
const [status, setStatus] = useState('尚未建立订阅')
return (
<div>
<div className="demoToolbar">
<button
className="demoButton"
onClick={() => {
setActive(value => !value)
}}
>
{active ? '卸载子组件' : '重新挂载子组件'}
</button>
</div>
<div>
当前状态:
{status}
</div>
{active && <Subscription onStatusChange={setStatus} />}
</div>
)
}
useCompatFactory Advanced
Creates an instance with a dispose method (for example a Tracker) inside a component and disposes it automatically when the component truly unmounts. React may abandon an uncommitted render without running effect cleanup. GC provides a fallback for those instances, with nondeterministic timing. Final unmount cleanup for committed instances is deferred by one microtask.
useFormEffects of @silver-formily/react uses it to bind effect lifecycles to the component. It fits "instance + dispose" external resources (subscriptions, observers, registration handles); plain scenarios are fine with native useEffect cleanup.
Signature
interface useCompatFactory {
<T extends { dispose: () => void }>(factory: () => T): T
}Usage
import { observable, Tracker } from '@silver-formily/reactive'
import { useCompatFactory } from '@silver-formily/reactive-react'
import { useReducer } from 'react'
import './demoStyles.css'
const obs = observable({
count: 0,
})
// useCompatFactory 是 useObserver 的内部实现:组件真实卸载时自动 dispose
// 出去的实例,StrictMode 重放渲染不会销毁它
function TrackerCounter() {
const [, forceUpdate] = useReducer((version: number) => version + 1, 0)
const tracker = useCompatFactory(() => new Tracker(forceUpdate))
// 每次渲染时收集 view 函数读取的响应式字段
const value = tracker.track(() => obs.count)
return (
<div>
<div className="demoToolbar">
<button className="demoButton" onClick={() => obs.count++}>
count + 1
</button>
</div>
<div>
track 读取值:
{value}
</div>
</div>
)
}
export default TrackerCounterimport { observable, Tracker } from '@silver-formily/reactive'
import { useCompatFactory } from '@silver-formily/reactive-react'
import { useReducer } from 'react'
import './demoStyles.css'
const obs = observable({
count: 0,
})
// useCompatFactory 是 useObserver 的内部实现:组件真实卸载时自动 dispose
// 出去的实例,StrictMode 重放渲染不会销毁它
function TrackerCounter() {
const [, forceUpdate] = useReducer((version: number) => version + 1, 0)
const tracker = useCompatFactory(() => new Tracker(forceUpdate))
// 每次渲染时收集 view 函数读取的响应式字段
const value = tracker.track(() => obs.count)
return (
<div>
<div className="demoToolbar">
<button className="demoButton" onClick={() => obs.count++}>
count + 1
</button>
</div>
<div>
track 读取值:
{value}
</div>
</div>
)
}
export default TrackerCounter