Hook 是组件的响应式契约。一个 Hook 就是一个按位置排列的槽位——当 Render() 运行时,Microsoft.UI.Reactor(以下简称 Reactor)会依次走过每一个 Use* 调用,并在保存在 RenderContext 上、按组件划分的槽位表中查找对应的格子。第一个 UseState 调用是槽位 0,第二个是槽位 1,依此类推;Hook 返回的 setter 会闭包捕获自己的槽位索引,并在你调用它时写回那个格子。Hook 取代了经典 XAML / WPF 应用用 DependencyProperty、
INotifyPropertyChanged、视图模型和生命周期方法所搭建的四套机制——你不再需要它们,而是用 UseState 保存状态、用 UseMemo 派生值、用 UseEffect 执行副作用、用 UseContext 免去逐层传递 props 地共享数据、用 UsePersisted 在卸载/重新挂载之间保留值。本页上的每一个 Hook 读起来都是 Render() 内部的一次函数调用,并通过一个 setter 闭包写回;理解了这一个形态,Reactor 的其余部分就都以组合的形式自然展开。
Hook(Hooks)¶
Hook 是你在 Render() 内部调用的函数,用于管理状态、副作用与记忆化。它们让你不再需要视图模型、事件处理器和生命周期方法。
速查¶
| Hook | 返回 | 用途 |
|---|---|---|
| UseState | (T value, Action<T> set) |
响应式状态——调用 set 时重新渲染。 |
| UseReducer | (T value, Action<Func<T,T>> update) 或 (TState, Action<TAction>) |
函数式或 Redux 风格的更新。 |
| UseEffect | void |
提交之后执行副作用。使用 Func<Action> 重载时,会在下一次副作用运行前以及卸载时先执行清理。 |
| UseMemo | T |
缓存的计算结果;任一 deps 项比较不相等时重新计算。 |
| UseRef | 带可变 .Current 的 Ref<T> |
跨渲染保留,且改变时不会触发重新渲染。 |
| UseCallback | Action |
跨渲染保持稳定的委托身份。 |
| UseContext | T |
读取环境 Context 的值。 |
| UseObservable | T |
当被追踪的 INotifyPropertyChanged 数据源发出变更时重新渲染。 |
| UseExternalStore | TSnapshot |
把"订阅/取快照"式的外部存储桥接进 Reactor,只在快照变化时才重新渲染。 |
| UseResource | AsyncValue<T> |
带缓存的异步读取(参见异步资源)。 |
| UsePersisted | (T, Action<T>) |
能在进程内的卸载/重新挂载之间存活的 UseState——存于内存,退出时清空(参见持久化)。 |
本页的每一个 Hook 在自动生成的 Hooks 参考中还会有一次摘要;本页余下部分是叙述性内容。
UseState¶
最常用的 Hook。返回当前值与一个 setter 函数:
class StateDemo : Component
{
public override Element Render()
{
var (color, setColor) = UseState("#0078D4");
var (size, setSize) = UseState(20.0);
return VStack(12,
SubHeading("UseState"),
TextBlock("Sample text").FontSize(size).Foreground(color),
TextBox(color, setColor, placeholderText: "#hex color")
.AutomationName("Sample text color")
.Width(150),
HStack(8,
TextBlock("Size:"),
Slider(size, 10, 48, setSize)
.AutomationName("Sample text size")
.Width(200)
)
);
}
}

调用 setColor("#FF0000"),Reactor 就会用新值重新渲染该组件。setter 是一个 Action<T>——它接收的是新值,而不是一个基于旧值的函数。当一次更新派生自上一个值(或你在同一个事件里多次调用 setter)时,请改用
UseReducer——它的更新器拿到的实时的上一个值。
UseReducer(函数式)¶
当你的新状态依赖于旧状态时,UseReducer 比 UseState 更干净。它的更新器接收一个 Func<T, T>——一个把旧值转换成新值的函数:
class ReducerDemo : Component
{
public override Element Render()
{
var (items, updateItems) = UseReducer(new List<string>());
var (input, setInput) = UseState("");
return VStack(12,
SubHeading("UseReducer"),
HStack(8,
TextBox(input, setInput, placeholderText: "Add item")
.AutomationName("Item text")
.Width(180),
Button("Add", () =>
{
if (string.IsNullOrWhiteSpace(input)) return;
updateItems(list =>
new List<string>(list) { input });
setInput("");
}),
Button("Clear", () =>
updateItems(_ => new List<string>()))
),
ForEach(items, (item, i) => TextBlock($" - {item}").WithKey($"{i}-{item}"))
);
}
}

updateItems(list => new List<string>(list) { input }) 通过创建一份新副本往列表里追加。这规避了修改带来的 bug——你总是从旧值产出一个新值。
UseReducer(Redux 风格)¶
对于带有多种动作类型的复杂状态,请使用 Redux 风格的重载。定义一个状态 record、若干动作类型以及一个 reducer 函数:
record CounterState(int Count, string LastAction);
abstract record CounterAction;
record Increment : CounterAction; record Decrement : CounterAction;
record Reset : CounterAction;
class ReduxReducerDemo : Component
{
public override Element Render()
{
var (state, dispatch) = UseReducer(
(CounterState s, CounterAction a) => a switch {
Increment => s with { Count = s.Count + 1, LastAction = "+" },
Decrement => s with { Count = s.Count - 1, LastAction = "-" },
Reset => new(0, "reset"), _ => s
}, new CounterState(0, "none"));
return VStack(8,
SubHeading("UseReducer (Redux-style)"),
TextBlock($"Count: {state.Count} (last: {state.LastAction})")
.FontSize(18).Bold(),
HStack(8,
Button("-", () => dispatch(new Decrement()))
.AutomationName("Decrement count"),
Button("Reset", () => dispatch(new Reset())),
Button("+", () => dispatch(new Increment()))
.AutomationName("Increment count")
)
);
}
}

(state, action) => newState 这个 reducer 是一个纯函数。每种动作类型映射到一次状态转换。这个模式的扩展性很好——新增动作不会改动既有逻辑。
UseEffect¶
在渲染之后执行副作用(定时器、订阅、异步工作)。依赖项数组决定该副作用何时重新运行:
class EffectDemo : Component
{
public override Element Render()
{
var (seconds, updateSeconds) = UseReducer(0);
var (running, setRunning) = UseState(false);
UseEffect(() =>
{
if (!running) return () => { };
var cts = new CancellationTokenSource();
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
var token = cts.Token; // capture once — the loop must not re-read cts.Token
_ = Task.Run(async () =>
{
try
{
while (await timer.WaitForNextTickAsync(token))
updateSeconds(s => s + 1);
}
catch (OperationCanceledException) { /* expected on cleanup */ }
});
return () => { cts.Cancel(); timer.Dispose(); };
}, running);
return VStack(8,
SubHeading("UseEffect"),
TextBlock($"Elapsed: {seconds}s").FontSize(18),
HStack(8,
Button(running ? "Stop" : "Start", () => setRunning(!running))
.AutomationName(running ? "Stop timer" : "Start timer"),
Button("Reset", () => updateSeconds(_ => 0))
)
);
}
}

关键细节:
- 副作用在渲染完成之后运行,而不是在渲染期间。
- 返回一个清理函数以释放资源。Reactor 会在重新运行该副作用之前、以及组件卸载时调用它。
- 空依赖项
UseEffect(() => { ... })— 只在挂载时运行一次。 - 带依赖项
UseEffect(() => { ... }, running)— 当running变化时运行。 - 按元数定类型的重载 — 对于一个、两个或三个依赖项,
UseEffect(body, a, b)以位置参数而非params object[]传递依赖项。在依赖项未变的路径上,它不分配数组,也避免了对值类型依赖项的装箱;其余行为完全一致。UseMemo与UseCallback也有同样的重载。
UseMemo¶
缓存一次高开销的计算,让它只在输入变化时才重新计算:
class MemoDemo : Component
{
public override Element Render()
{
var (input, setInput) = UseState("Hello, Reactor!");
var stats = UseMemo(() => new
{
Chars = input.Length,
Words = input.Split(' ',
StringSplitOptions.RemoveEmptyEntries).Length,
Upper = input.ToUpperInvariant()
}, input);
return VStack(8,
SubHeading("UseMemo"),
TextBox(input, setInput)
.AutomationName("Text to analyze")
.Width(250),
TextBlock($"Characters: {stats.Chars}, Words: {stats.Words}"),
Caption($"Uppercased: {stats.Upper}")
);
}
}

UseMemo 会在渲染之间比较依赖项的值。如果它们没有变化,就返回缓存的结果。把它用于字符串处理、筛选大型列表,或任何你不想每次渲染都重算的计算。
对于一个到三个依赖项,按元数定类型的重载
UseMemo(factory, a, b) 能避免 params object[] 的分配以及未变路径上对值类型的装箱——当某个记忆化处在频繁的渲染路径上时,这一点很实用。
UseRef¶
存放一个跨渲染保留、且不会触发重新渲染的可变值:
class RefDemo : Component
{
public override Element Render()
{
var (value, setValue) = UseState("");
var renderCount = UseRef(0);
renderCount.Current++;
return VStack(8,
SubHeading("UseRef"),
TextBlock($"Render count: {renderCount.Current}").SemiBold(),
TextBox(value, setValue, placeholderText: "Type to trigger renders")
.AutomationName("Render trigger text")
.Width(250),
Caption("UseRef persists across renders without causing them")
);
}
}

UseRef 返回一个带有 .Current 属性的 Ref<T>。修改 .Current
不会引发重新渲染。它适用于:
- 统计渲染次数
- 存放用于比较的上一个值
- 持有定时器或取消令牌的引用
UseCallback¶
稳定一个回调的引用,使子组件不会不必要地重新渲染:
class CallbackDemo : Component
{
public override Element Render()
{
var (count, updateCount) = UseReducer(0);
var (label, setLabel) = UseState("Click me");
var stableIncrement = UseCallback(
() => updateCount(c => c + 1), Array.Empty<object>());
return VStack(8,
SubHeading("UseCallback"),
TextBlock($"Count: {count}").FontSize(18),
TextBox(label, setLabel, placeholderText: "Button label")
.AutomationName("Button label")
.Width(200),
Button(label, stableIncrement)
.AutomationName("Increment count"),
Caption("The callback identity stays stable across renders")
);
}
}

如果没有 UseCallback,那个 lambda () => updateCount(c => c + 1) 每次渲染都会是一个新对象。只要依赖项没变,UseCallback 就会返回同一个委托实例。在把回调传给经过记忆化的子组件时,这一点很重要。
与 UseEffect 和 UseMemo 一样,按元数定类型的重载
UseCallback(callback, a, b)(1–3 个依赖项)也能跳过未变路径上 params object[] 的分配与值类型装箱。
外部存储¶
有些状态位于 Reactor 之外,但依然具备干净的订阅形态:订阅通知,然后索取最新快照。对于这类存储,UseExternalStore 能替你省掉通常那套 UseEffect 加 UseReducer 的样板代码:
record SessionSnapshot(string Title);
sealed class SessionStore
{
private SessionSnapshot _snapshot = new("Untitled");
public event Action? Changed;
public SessionSnapshot Snapshot => _snapshot;
public Action Subscribe(Action onChanged)
{
Changed += onChanged;
return () => Changed -= onChanged;
}
public void Rename(string title)
{
_snapshot = new SessionSnapshot(title);
Changed?.Invoke();
}
}
class ExternalStoreDemo : Component
{
private static readonly SessionStore _store = new();
public override Element Render()
{
// `subscribe` 是一个方法组——一个稳定的委托,
// 因此副作用不会在每次渲染时拆除并重建订阅。
var snapshot = UseExternalStore(
_store.Subscribe,
() => _store.Snapshot);
return VStack(8,
SubHeading("UseExternalStore"),
TextBlock(snapshot.Title),
Button("Rename", () => _store.Rename($"Doc {Random.Shared.Next(100)}"))
);
}
}
UseExternalStore 在渲染期间读取快照,在副作用中订阅,并且只在某次通知产出了不同快照时才排队一次重新渲染。当快照类型需要与 EqualityComparer<T>.Default 不同的值语义时,请传入自定义比较器。
有两条稳定性规则让这个 Hook 保持良好行为——与 React 对 useSyncExternalStore 给出的指导相同:
subscribe必须是一个稳定的委托。 它是那个决定"订阅是否被拆除并重建"的副作用依赖项,因此请传一个方法组(_store.Subscribe)或一个用UseCallback记忆化的委托。一个新鲜捕获的 lambda(onChanged => _store.Subscribe(onChanged))每次渲染都是新委托,会迫使每次渲染都退订再订阅。getSnapshot必须返回一个已缓存的值。 它只应在底层数据变化时才改变身份。若它每次调用都返回一个全新的、永不相等的值(例如() => items.ToArray()),再加上一个不稳定的subscribe,就可能陷入自旋——重新渲染会重跑副作用,紧随其后的复检看到"变化",于是又迫使下一次渲染。请把快照记忆化,或在什么都没变时返回一个会被比较器判为相等的值。
从后台工作更新状态¶
宿主启动完成之后,UseState 与 UseReducer 的 setter 从任意线程调用都是安全的。当你在后台任务中调用 setter 时——在 Task.Run 内部、从 PeriodicTimer 循环里、从网络回调中,或在 await ... ConfigureAwait(false) 之后——setter 会自动把这次写入以及随之而来的重新渲染封送到 UI 调度器上。你写的还是和 UI 线程上一样的代码:
public override Element Render()
{
var (seconds, updateSeconds) = UseReducer(0);
UseEffect(() =>
{
var cts = new CancellationTokenSource();
var token = cts.Token; // capture once — the loop must not re-read cts.Token
_ = Task.Run(async () =>
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
try
{
while (await timer.WaitForNextTickAsync(token))
updateSeconds(s => s + 1); // auto-marshals to the UI thread
}
catch (OperationCanceledException) { /* expected on cleanup */ }
});
// Cancel only, and deliberately so. The fire-and-forget worker shares ownership of the
// source: disposing here while it is still inside WaitForNextTickAsync can surface an
// ObjectDisposedException on the token. Nothing leaks — a CTS with no timer and no
// WaitHandle access holds no unmanaged resource, so dropping the reference is enough.
// Dispose only where a single owner can prove the worker has finished.
return () => { cts.Cancel(); };
});
return TextBlock($"Elapsed: {seconds}s");
}
每次跨线程 setter 调用的开销是一次 DispatcherQueue.TryEnqueue——以微秒计,并非免费,但远比你在工作线程里直接写字段所会遇到的那些 bug 便宜得多。如果你需要大量并发的 setter 就地生效、而不是都串行经过 UI 线程(典型的场景是多个生产者猛击同一个 Hook 的数据摄取循环),就给 Hook 传 threadSafe: true:
var (count, setCount) = UseState(0, threadSafe: true);
var (sum, addToSum) = UseReducer(0, threadSafe: true);
threadSafe: true 会把该 Hook 切换为每格一把锁:并发写入者在锁上串行化,而不是通过 UI 调度器排队;setter 内部的读取(reducer 的 prev 参数)看到的是最近一次已提交的写入,而不是上一次 UI tick 时的快照。
自动封送帮不上忙的场景。 setter 需要一个已捕获的
ReactorApp.UIDispatcher才能封送过去。在直接驱动RenderContext的单元测试/无头环境中,或在第一个宿主启动完成之前,跨线程调用 setter 会抛出InvalidOperationException,而不是静默地竞态。如果调度器拒绝了这次封送调用(例如关闭过程中),setter 也会抛出。请在副作用的清理逻辑里取消后台生产者,让它们在窗口关闭之前停下。
Hook 规则¶
每次渲染都必须以相同的顺序调用 Hook。Reactor 按调用序列中的位置来追踪 Hook——第一个 UseState 调用永远映射到第一个状态槽位,第二个映射到第二个,依此类推。_hookIndex 与 _hooks[currentIndex] 的内部遍历过程在
Hook 内部机制中有说明。
应当这样:
public override Element Render()
{
var (a, setA) = UseState(0); // always first
var (b, setB) = UseState(""); // always second
UseEffect(() => { /* ... */ }, a); // always third
return TextBlock($"{a} {b}");
}
不要这样:
public override Element Render()
{
var (a, setA) = UseState(0);
if (a > 0)
UseEffect(() => { ... }, a); // WRONG: conditional hook
return TextBlock($"{a}");
}
把条件放到 Hook 内部:
注意: 在
if、for、while、switch或try中调用 Hook,会在任何走进该分支的渲染里改变其后每一个 Hook 的槽位索引。下一次渲染于是会向槽位N索取未分支的调用形态所期望的类型——比如ValueHookState<int>对上EffectHookState——此时RenderContext.UseState处的槽位表守卫就会抛出HookOrderException("Hook at index N is EffectHookState, expected ValueHookState<Int32> (UseState). Hooks must be called in the same order every render.")。Roslyn 分析器REACTOR_HOOKS_001会在编译期把这个字面模式——Render重写或以Use开头的自定义 Hook 中、位于控制流构造内的Use*调用——标记为警告。分析器看不见通过 lambda、名字不以Use开头的辅助函数或模式匹配派发所发生的调用,因此运行时守卫是最后的兜底。当你撞上这个异常时,去找那个被条件性地触达的Use*调用——通常就是你刚加上的那个。
模式¶
自定义 Hook¶
自定义 Hook 就是任何名字以 Use 开头、并在其中调用其他 Hook 的方法。分析器把 Use* 方法视为合法的 Hook 上下文,因此你可以把 UseState、UseEffect 等组合成一个具名的、可复用的包裹,同时又不丢掉那些规则。
// A custom hook is a RenderContext extension method whose name starts with
// `Use`. It owns three slots — two UseState and one UseEffect — and the caller
// still gets the simple (value, setter) shape they'd get from UseState.
static class DebouncedTextHook
{
public static (string Value, Action<string> Set) UseDebouncedText(
this RenderContext ctx, string initial, int ms)
{
var (value, setValue) = ctx.UseState(initial);
var (debounced, setDebounced) = ctx.UseState(initial);
ctx.UseEffect(() =>
{
var cts = new CancellationTokenSource();
_ = Task.Run(async () =>
{
try { await Task.Delay(ms, cts.Token); setDebounced(value); }
// Expected: the cleanup below cancels this delay whenever `value`
// changes again inside the debounce window. Cancelling is how the
// stale result is discarded, so there is nothing to report.
catch (OperationCanceledException) { return; }
});
return () => { cts.Cancel(); };
// Both captured values are dependencies. `ms` is easy to leave out —
// it usually comes from a constant at the call site — but omitting it
// means a caller that changes the delay keeps the already-armed timer
// running on the old interval until `value` happens to change.
}, value, ms);
return (debounced, setValue);
}
}
class CustomHookDemo : Component
{
public override Element Render() => Memo(ctx =>
{
var (debounced, setText) = ctx.UseDebouncedText("", 300);
return VStack(8,
SubHeading("Custom hook: UseDebouncedText"),
TextBox(debounced, setText, placeholderText: "Type…")
.AutomationName("Text to debounce")
.Width(250),
Caption($"Debounced: {debounced}")
);
});
}
这个 Hook 占有三个槽位——两个 UseState 和一个 UseEffect——而调用方拿到的仍然是 UseState 那种简单的 (value, setter) 形态。自定义 Hook 是 RenderContext 的扩展方法,因此它们显式接收上下文(this RenderContext ctx),并通过 Memo(ctx => …) 函数组件或类组件的 Context 上的 ctx 来调用。经过整理的
Reactor 准则一页收录了完整的自定义 Hook 约定。
状态提升¶
当父子组件都需要读取同一个值时,把 UseState 提升到父组件,再把 (value, setter) 作为 prop 传下去。
recipes/master-detail演练展示了经典的形态——主列表与详情面板都响应同一份共享的选中状态。这与 XAML 开发者用共享视图模型去解决的问题是同一个模式;只不过在这里,状态位于父组件里,子组件是响应式的消费者。
用 UseRef 保存延迟值¶
当一个值需要在渲染之间存活、却又不能触发渲染时,UseRef 就是正确的工具。为做差异比较而保存上一个 prop 值、持有 CancellationTokenSource、或为诊断统计渲染次数,都应放进 ref:
var prev = UseRef<int?>(null);
UseEffect(() => { /* compare prev.Current to current */ prev.Current = current; }, current);
setter 会立即写入 .Current,而不会调度一次渲染——对照 UseState,它的每一次 setter 调用都会经由调度器排队一次重新渲染。
常见错误¶
Hook 写在条件分支里¶
// Don't:
public override Element Render()
{
var (open, setOpen) = UseState(false);
if (open)
{
UseEffect(() => Subscribe(), Array.Empty<object>()); // REACTOR_HOOKS_001
}
return ...;
}
每当 open 翻转的那次渲染,该副作用的槽位索引就会移动一格。下一次渲染会在期望 ValueHookState 的位置找到 EffectHookState,从而抛出 HookOrderException。修复方式是无条件地调用该 Hook,并把条件放到它内部:
闭包过期¶
// Don't:
var (count, setCount) = UseState(0);
UseEffect(() =>
{
var t = new Timer(_ => setCount(count + 1), null, 0, 1000);
return () => t.Dispose();
}, Array.Empty<object>()); // captured `count` is forever 0
副作用的空依赖数组意味着它在挂载时就把 count 捕获了一次。定时器会带着这个过期的闭包永远运行下去,于是计数器卡在 1。修复方式是使用 UseReducer 的函数式更新器——声明
var (count, updateCount) = UseReducer(0) 并调用 updateCount(c => c + 1),
它读取的是格子里的实时值,而不是被捕获的那个变量。(UseState 的 setter 是一个 Action<T>;它只接收值,因此无法表达"基于上一个值更新"。)
本该用 UseReducer 的 setter 链¶
class SetterChainDontDemo : Component
{
public override Element Render()
{
// Don't — all three calls read the same captured `count`.
var (count, setCount) = UseState(0);
return Button("+3", () =>
{ setCount(count + 1); setCount(count + 1); setCount(count + 1); });
}
}
三次 setter 调用读的都是同一个被捕获的 count,写的都是 count + 1——于是按钮只前进 1,而不是 3。改用 UseReducer,让每一次函数式更新都能看到上一次的结果:
class SetterChainDoDemo : Component
{
public override Element Render()
{
// Do — each functional update sees the previous one's result.
var (count, updateCount) = UseReducer(0);
return Button("+3", () =>
{ updateCount(c => c + 1); updateCount(c => c + 1); updateCount(c => c + 1); });
}
}
这与闭包过期那一节是同一条贯穿 Reactor 的规则:当一次更新派生自上一个值时,UseReducer 的函数式更新器就是正确的形态。上文描述的跨线程更新器自动封送路径依赖的也是同一套机制——每一个排队的更新读取的都是最近一次已提交的值,而不是某个快照。
调用 setter 之后立刻读取该状态¶
class StaleReadDontDemo : Component
{
static readonly string[] SizeFitNames = ["Contain", "Cover", "Fill"];
public override Element Render()
{
var (sizeFitIdx, setSizeFitIdx) = UseState(0);
// Don't — the setter only queued a re-render.
return ComboBox(SizeFitNames, sizeFitIdx, i =>
{
setSizeFitIdx(i);
Apply(sizeFitIdx); // reads the PREVIOUS index
});
void Apply(int index) { }
}
}
setter 从不修改当前闭包里的局部变量;它只是调度了一次重新渲染,由下一次渲染产出新的 sizeFitIdx。在同一个处理器里稍后再读 sizeFitIdx,得到的是更新前的旧值——切换下拉框看起来像是激活了上一个选中项。请使用你手上已经有的那个值:
class StaleReadDoDemo : Component
{
static readonly string[] SizeFitNames = ["Contain", "Cover", "Fill"];
public override Element Render()
{
var (sizeFitIdx, setSizeFitIdx) = UseState(0);
return ComboBox(SizeFitNames, sizeFitIdx, i =>
{
setSizeFitIdx(i);
Apply(i); // use the new value directly
});
void Apply(int index) { }
}
}
REACTOR_HOOKS_008 分析器会标记"在同一个同步处理器中、调用 setter 之后又读取该状态变量"的情况——包括在辅助 lambda 或局部函数中、于下一次渲染之前被执行的读取。这适用于 UseState、UsePersisted 和 UseReducer。真正被推迟的回调(你只是交出去、并未在此刻调用的处理器)不受此限,因为它们会在之后的渲染中运行,看到的是新值。
注意:对
UseState<T>/UsePersisted<T>而言,setter 是Action<T>,因此当T本身是委托时,lambda 参数是新值,而不是函数式更新器。函数式更新器 API 是UseReducer。因此,传一个 lambda 给 setter 并不能豁免其后的过期读取。
小贴士¶
派生式更新请用 UseReducer。 当一次更新依赖于上一个值,或同一个事件里要跑好几次更新时,updateCount(c => c + 1) 比 setCount(count + 1) 更安全——函数式更新器读取的是实时值,而 UseState 的 Action<T> setter 只会存下你传给它的东西。
凡是创建了资源的副作用,都要返回清理逻辑。 定时器、订阅与事件处理器都必须被释放。清理函数是你唯一的机会。
不要过度使用 UseMemo。 像 $"{first} {last}" 这样简单的表达式开销很低。只在计算确实昂贵、或结果要作为依赖项传给别处时才做记忆化。
UseRef 不是用来放 UI 值的。 如果某个值变化后应当刷新屏幕,就用 UseState。UseRef 用于那些不影响渲染的记账工作。
让副作用保持聚焦。 一个副作用只管一件事。不要把定时器和 API 调用塞进同一个 UseEffect——把它们拆成各自带依赖数组的独立 Hook。更高级的模式见副作用与生命周期。
下一步¶
- 布局 — 下一篇:用 VStack、HStack、Grid 与响应式模式排布 UI
- 组件 — 上一篇:组件类、props 与组合
- 副作用与生命周期 — UseEffect 的高级模式、清理与异步工作
- 上下文(Context) — 在组件树中共享状态,无需逐层传递 props
- Hook 内部机制 — 表层的槽位表到底是怎么工作的
- 持久化 — 用
UsePersisted让状态在进程内的卸载/重新挂载之间存活 - Reactor 准则 — 一处汇总 Hook 规则、惯用法与反面模式