Skip to content

Hook 是组件的响应式契约。一个 Hook 就是一个按位置排列的槽位——当 Render() 运行时,Microsoft.UI.Reactor(以下简称 Reactor)会依次走过每一个 Use* 调用,并在保存在 RenderContext 上、按组件划分的槽位表中查找对应的格子。第一个 UseState 调用是槽位 0,第二个是槽位 1,依此类推;Hook 返回的 setter 会闭包捕获自己的槽位索引,并在你调用它时写回那个格子。Hook 取代了经典 XAML / WPF 应用用 DependencyPropertyINotifyPropertyChanged、视图模型和生命周期方法所搭建的四套机制——你不再需要它们,而是用 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 带可变 .CurrentRef<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)
            )
        );
    }
}

UseState demo

调用 setColor("#FF0000"),Reactor 就会用新值重新渲染该组件。setter 是一个 Action<T>——它接收的是新值,而不是一个基于旧值的函数。当一次更新派生自上一个值(或你在同一个事件里多次调用 setter)时,请改用 UseReducer——它的更新器拿到的实时的上一个值。

UseReducer(函数式)

当你的新状态依赖于旧状态时,UseReducerUseState 更干净。它的更新器接收一个 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}"))
        );
    }
}

UseReducer demo

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")
            )
        );
    }
}

Redux-style reducer

(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))
            )
        );
    }
}

UseEffect timer

关键细节:

  • 副作用在渲染完成之后运行,而不是在渲染期间。
  • 返回一个清理函数以释放资源。Reactor 会在重新运行该副作用之前、以及组件卸载时调用它。
  • 空依赖项 UseEffect(() => { ... }) — 只在挂载时运行一次。
  • 带依赖项 UseEffect(() => { ... }, running) — 当 running 变化时运行。
  • 按元数定类型的重载 — 对于一个、两个或三个依赖项,UseEffect(body, a, b) 以位置参数而非 params object[] 传递依赖项。在依赖项未变的路径上,它不分配数组,也避免了对值类型依赖项的装箱;其余行为完全一致。UseMemoUseCallback 也有同样的重载。

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 demo

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 demo

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 demo

如果没有 UseCallback,那个 lambda () => updateCount(c => c + 1) 每次渲染都会是一个新对象。只要依赖项没变,UseCallback 就会返回同一个委托实例。在把回调传给经过记忆化的子组件时,这一点很重要。

UseEffectUseMemo 一样,按元数定类型的重载 UseCallback(callback, a, b)(1–3 个依赖项)也能跳过未变路径上 params object[] 的分配与值类型装箱。

外部存储

有些状态位于 Reactor 之外,但依然具备干净的订阅形态:订阅通知,然后索取最新快照。对于这类存储,UseExternalStore 能替你省掉通常那套 UseEffectUseReducer 的样板代码:

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,就可能陷入自旋——重新渲染会重跑副作用,紧随其后的复检看到"变化",于是又迫使下一次渲染。请把快照记忆化,或在什么都没变时返回一个会被比较器判为相等的值。

从后台工作更新状态

宿主启动完成之后,UseStateUseReducer 的 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 内部

UseEffect(() => { if (a > 0) { /* ... */ } }, a);

注意:ifforwhileswitchtry 中调用 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 上下文,因此你可以把 UseStateUseEffect 等组合成一个具名的、可复用的包裹,同时又不丢掉那些规则。

// 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,并把条件放到它内部

UseEffect(() => { if (!open) return () => { }; return Subscribe(); }, open);

闭包过期

// 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 或局部函数中、于下一次渲染之前被执行的读取。这适用于 UseStateUsePersistedUseReducer。真正被推迟的回调(你只是交出去、并未在此刻调用的处理器)不受此限,因为它们会在之后的渲染中运行,看到的是新值。

注意:对 UseState<T>/UsePersisted<T> 而言,setter 是 Action<T>,因此当 T 本身是委托时,lambda 参数是新值,而不是函数式更新器。函数式更新器 API 是 UseReducer。因此,传一个 lambda 给 setter 并不能豁免其后的过期读取。

小贴士

派生式更新请用 UseReducer 当一次更新依赖于上一个值,或同一个事件里要跑好几次更新时,updateCount(c => c + 1)setCount(count + 1) 更安全——函数式更新器读取的是实时值,而 UseStateAction<T> setter 只会存下你传给它的东西。

凡是创建了资源的副作用,都要返回清理逻辑。 定时器、订阅与事件处理器都必须被释放。清理函数是你唯一的机会。

不要过度使用 UseMemo。$"{first} {last}" 这样简单的表达式开销很低。只在计算确实昂贵、或结果要作为依赖项传给别处时才做记忆化。

UseRef 不是用来放 UI 值的。 如果某个值变化后应当刷新屏幕,就用 UseStateUseRef 用于那些不影响渲染的记账工作。

让副作用保持聚焦。 一个副作用只管一件事。不要把定时器和 API 调用塞进同一个 UseEffect——把它们拆成各自带依赖数组的独立 Hook。更高级的模式见副作用与生命周期

下一步

  • 布局 — 下一篇:用 VStack、HStack、Grid 与响应式模式排布 UI
  • 组件 — 上一篇:组件类、props 与组合
  • 副作用与生命周期 — UseEffect 的高级模式、清理与异步工作
  • 上下文(Context) — 在组件树中共享状态,无需逐层传递 props
  • Hook 内部机制 — 表层的槽位表到底是怎么工作的
  • 持久化 — 用 UsePersisted 让状态在进程内的卸载/重新挂载之间存活
  • Reactor 准则 — 一处汇总 Hook 规则、惯用法与反面模式