Microsoft.UI.Reactor(以下简称 Reactor)中的 Context<T>,是带响应式能力、作用域限定在元素树上的命名空间式依赖注入。把一个 context 定义为一个静态字段,用 .Provide(ctx, value) 修饰符向任意元素的子树发布一个值,任意后代组件都能用 UseContext(ctx) 读取当前值。组件不需要知道是哪位祖先提供了它;中间组件也不需要把这个值作为 prop 转发出去;而当所提供的值变化时,每一个消费者都会重新渲染。这与 React 的 createContext /
<Provider value> / useContext 三件套是同一个形态,也与 Compose 的
CompositionLocal 同形。这套系统最重要的性质是值身份敏感性:当所提供的值与上一个值不相等时,消费者就会重新渲染,因此每次渲染都传一个全新的对象字面量,会让整棵子树里的每个消费者都跟着抖动。当你手上有许多相距很远的组件都需要的数据(主题、当前用户、区域设置、功能开关),而逐层传 prop 会迫使每个中间组件都声明一个自己从不使用的 prop 时,就来读这一页。
上下文(Context)¶
Context 让你把数据穿过组件树传递,而无需在每一层 props 上打洞。定义一个 Context<T>,在任意层级提供一个值,任意后代都能用 UseContext 读取它。
速查¶
| API | 形态 | 用途 |
|---|---|---|
Context<T> |
new Context<T>(T defaultValue) |
句柄。定义为静态字段、只定义一次;当作用域内没有 provider 时返回 defaultValue。 |
.Provide(ctx, value) |
任意 Element 上的修饰符 |
向该元素的后代提供 value。多次 .Provide(...) 会链接成一个合并后的字典。 |
UseContext(ctx) |
返回 T 的 Hook |
从最近的祖先 provider 读取当前值;若没有则返回 ctx.DefaultValue。 |
Context<T> 是响应式的:改变所提供的 value(通过以不同的引用重新渲染 provider),会让子树中每一个对该 context 调用过 UseContext 的组件重新渲染。身份很重要——参见稳定所提供的值。
创建一个 Context¶
一个 context 就是一个带默认值的静态字段:
static class Contexts
{
public static Context<string> ThemeMode = new("light");
public static Context<string> UserName = new("Guest");
public static Context<int> FontScale = new(16);
}
请选择一个合理的默认值——它能让组件在开发期间独立工作,在测试夹具中无需额外接线就能工作,也能在 Storybook 风格的预览中工作。默认值取 null 会迫使每个消费者都去处理"未提供"的情况;而一个真实值的默认值几乎总是更友好。
提供与消费¶
在任意元素上使用 .Provide(),为它的子树提供一个值;在任意后代中使用 UseContext() 读取它:
class ProvideConsumeExample : Component
{
public override Element Render()
{
return VStack(12,
TextBlock("Outside: no provider"),
VStack(12,
Component<Greeting>()
).Provide(Contexts.UserName, "Alice")
).Padding(24);
}
}
class Greeting : Component
{
public override Element Render()
{
var name = UseContext(Contexts.UserName);
return TextBlock($"Hello, {name}!").FontSize(20).Bold();
}
}

.Provide() 是一个修饰符,就像 .Padding() 或 .Background() 一样(参见
样式)——它作用于任意元素,并且这个值会传播到每一个后代,无论有多深。provider 与消费者之间的那些组件,根本不需要知道这个值的存在。
用 Context 做主题切换¶
一个常见的使用场景是影响整棵子树的主题开关。下面的根组件提供一个主题值,子组件消费它:
class ThemeSwitchExample : Component
{
public override Element Render()
{
var (isDark, setIsDark) = UseState(false);
var mode = isDark ? "dark" : "light";
return VStack(16,
ToggleSwitch(isDark, setIsDark, onContent: "Dark", offContent: "Light"),
VStack(12, Component<ThemePanel>()).Provide(Contexts.ThemeMode, mode)
).Padding(24);
}
}
class ThemePanel : Component
{
public override Element Render()
{
var theme = UseContext(Contexts.ThemeMode);
var elTheme = theme == "dark" ? ElementTheme.Dark : ElementTheme.Light;
return Border(
VStack(8,
TextBlock($"Current theme: {theme}").Bold(),
TextBlock("Panel adapts to context.").Foreground(Theme.SecondaryText)
).Padding(16)
).Background(Theme.CardBackground)
.CornerRadius(8)
.RequestedTheme(elTheme);
}
}

ThemePanel 组件读取该 context,并据此应用 ElementTheme。当开关改变了所提供的值时,所有消费者都会以新主题重新渲染。这与 Reactor 的样式与主题令牌内部所用的形态是同一个。
嵌套 Context 覆盖¶
一个子 provider 会为自己的子树覆盖父级的值。兄弟子树则保留原值:
class NestedOverrideExample : Component
{
public override Element Render()
{
return HStack(16,
VStack(8,
Caption("Parent value"),
Component<NameDisplay>()
).Provide(Contexts.UserName, "Alice"),
VStack(8,
Caption("Overridden child"),
VStack(4, Component<NameDisplay>())
.Provide(Contexts.UserName, "Bob")
).Provide(Contexts.UserName, "Alice")
).Padding(24);
}
}
class NameDisplay : Component
{
public override Element Render()
{
var name = UseContext(Contexts.UserName);
return TextBlock(name).FontSize(18).SemiBold().Foreground(Theme.Accent);
}
}

内层的 .Provide() 只影响该元素的后代。兄弟子树看到的仍是父级的值。这让你能够创建局部覆盖——一个始终以深色渲染的预览磁贴、一个强调色演示、一棵重新限定当前用户 context 的"权限不足"子树——而不会影响到树的其余部分。
多个 Context¶
组件可以同时提供和消费多个 context。每个 context 彼此独立:
class MultipleContextsExample : Component
{
public override Element Render()
{
return VStack(8,
Component<ProfileCard>()
).Provide(Contexts.UserName, "Charlie")
.Provide(Contexts.FontScale, 22)
.Padding(24);
}
}
class ProfileCard : Component
{
public override Element Render()
{
var name = UseContext(Contexts.UserName);
var fontSize = UseContext(Contexts.FontScale);
return Border(
VStack(8,
TextBlock(name).FontSize(fontSize).Bold(),
TextBlock($"Font scale from context: {fontSize}px")
.Foreground(Theme.SecondaryText)
).Padding(16)
).Background(Theme.CardBackground).CornerRadius(8);
}
}

每一个 Context<T> 都是一个独立的通道。提供其中一个不会影响另一个。一个组件可以按需要多次调用 UseContext;每一次调用都会让该组件订阅其特定 context 的变化。
当前用户模式¶
在应用根节点放一个强类型 record,只提供一次,每个页面都读取它——这就是"无需逐层传 prop 地共享状态"的规范形态:
// A typed user-context record at the app root — every page reads the
// current user via UseContext rather than threading a User prop through
// every component along the way.
record CurrentUser(string Id, string DisplayName, bool IsAdmin);
static class AppContexts
{
public static Context<CurrentUser> User = new(
new CurrentUser("guest", "Guest", IsAdmin: false));
}
class UserContextExample : Component
{
private static readonly CurrentUser InitialUser = new("u1", "Alice", IsAdmin: true);
public override Element Render()
{
var (user, setUser) = UseState(InitialUser);
return VStack(12,
HStack(8,
Button("Sign in as Alice (admin)",
() => setUser(new CurrentUser("u1", "Alice", IsAdmin: true))),
Button("Sign in as Bob (user)",
() => setUser(new CurrentUser("u2", "Bob", IsAdmin: false)))
),
VStack(8,
Component<AccountMenu>(),
Component<AdminPanel>()
).Provide(AppContexts.User, user)
).Padding(24);
}
}
class AccountMenu : Component
{
public override Element Render()
{
var user = UseContext(AppContexts.User);
return TextBlock($"Signed in as: {user.DisplayName}").SemiBold();
}
}
class AdminPanel : Component
{
public override Element Render()
{
var user = UseContext(AppContexts.User);
return user.IsAdmin
? Border(TextBlock("Admin tools available").Padding(8))
.Background(Theme.CardBackground).CornerRadius(4)
: TextBlock("(no admin tools)").Foreground(Theme.SecondaryText);
}
}

已登录用户存放在应用根节点的 UseState 里。
.Provide(AppContexts.User, user) 修饰符把它发布出去;AccountMenu 与
AdminPanel 组件可以位于树中的任意位置,并通过 UseContext 读取它。换一个用户登录——两个面板都会重新渲染,因为它们订阅的是同一个 context。
稳定所提供的值¶
UseContext 的消费者所比较的,正是所提供值的身份。每次渲染都构造一个全新的对象字面量——即使底层数据没变——也会让每一个消费者失效:
// The value identity matters. Wrapping in UseMemo with explicit deps
// stops every consumer from re-rendering on every provider render.
// ThemeConfig is a *class*, so it compares by reference — context
// invalidation uses Equals, and a record with unchanged fields would
// compare equal and re-render nothing, hiding the very cost this
// snippet is about.
sealed class ThemeConfig(string mode, int fontScale, string accent)
{
public string Mode { get; } = mode;
public int FontScale { get; } = fontScale;
public string Accent { get; } = accent;
}
static class ThemeContexts
{
public static Context<ThemeConfig> Theme = new(new ThemeConfig("light", 14, "#0078D4"));
}
class MemoizeContextValueExample : Component
{
public override Element Render()
{
var (mode, setMode) = UseState("light");
var (scale, setScale) = UseState(14);
// GOOD — identity stable while inputs unchanged. Consumers only
// re-render when mode or scale actually change.
var theme = UseMemo(() => new ThemeConfig(mode, scale, "#0078D4"), mode, scale);
// BAD — a fresh reference every render, so every consumer re-renders
// even when mode and scale are unchanged.
// var theme = new ThemeConfig(mode, scale, "#0078D4");
return VStack(12,
HStack(8,
Button("Toggle mode", () => setMode(mode == "light" ? "dark" : "light")),
Button("Bump scale", () => setScale(scale + 2))
),
VStack(8, Component<ThemedHeading>())
.Provide(ThemeContexts.Theme, theme)
).Padding(24);
}
}
class ThemedHeading : Component
{
public override Element Render()
{
var t = UseContext(ThemeContexts.Theme);
return TextBlock($"Headline @ {t.FontScale}px ({t.Mode})")
.FontSize(t.FontScale).Foreground(t.Accent);
}
}

那次 UseMemo 调用把 ThemeConfig 的身份钉在了它的依赖项上。消费者只在 mode 或 scale 真正变化时才重新渲染。这与 React 中为 context 值使用 useMemo 的形态相同。
注意: context 值的身份驱动着消费者的重新渲染。每次渲染都传一个全新的对象字面量——在
Render()里写.Provide(ctx, new Config { ... })——会导致子树中每一个调用过UseContext(ctx)的组件每一帧都重新渲染,即使底层数据并没有变化。经典的故障形态是:应用根节点的主题 provider 内联传入new Theme(...);于是父树中任何状态变化,都会让每一个页面、每一个表单字段、每一个文本块重新渲染。修复有两条:(1) 用带显式依赖项的UseMemo把值包起来,使身份在更新之间保持稳定;或 (2) 把构造过程移到Render()之外,让它成为一个单例。性能覆盖层(mur docs perf-overlay)能把这一点显现出来——在开发工具视图里留意偏高的"context 驱动的重新渲染"计数。
模式¶
把状态提升到最近的共同祖先¶
React 中被引用最多的一条建议——"把状态提升到需要知道它的最低层祖先"——在 Reactor 中可以逐字照搬。Context 就是你把这些已提升的状态再传下去、又不必经过那些无聊的中间层的方式。一个带共享工具栏、需要知道当前选中项的双窗格编辑器:把选中项提升到同时拥有两个窗格的父组件,作为 context 提供出去,两个窗格各自消费。应用根节点上的键盘快捷键处理器读取的是同一个 context——同一个模式,不需要逐层传 prop。
用模拟 Provider 做测试¶
生产代码用真实的用户获取 provider 包裹组件树。测试则渲染同一棵组件树,外面包一层 .Provide(ctx,
testStub)——消费方代码一行不改,而测试可以通过这个桩控制每一个值:
// Production root provides the real value; tests render the same
// component tree with a Provide(...) wrapper supplying a stub. The
// consumer code is unchanged — context lets you swap dependencies
// without re-plumbing props.
class CartConsumer : Component
{
public override Element Render()
{
var user = UseContext(AppContexts.User);
return TextBlock($"Cart for {user.DisplayName} ({user.Id})");
}
}
class MockProviderExample : Component
{
public override Element Render()
{
// Two trees: one with the "real" production default, one with a
// test stub supplied via .Provide(). Same CartConsumer in both.
return HStack(24,
VStack(8,
Caption("Production default"),
Component<CartConsumer>()
),
VStack(8,
Caption("Test stub"),
VStack(0, Component<CartConsumer>())
.Provide(AppContexts.User, new CurrentUser("test", "Test User", IsAdmin: false))
)
).Padding(24);
}
}

对于跨越多个组件的单元测试,这是推荐的模式——渲染器夹具的细节与快照测试的接口见测试。
有作用域的功能开关¶
从最近的祖先 Context<FeatureFlags> 读取一个功能开关,为某一棵子树覆盖它以演示一个 beta 界面,而应用的其余部分仍保持稳定。上面嵌套 Context 覆盖一节就是那个形态;开关 record 则是载荷。
常见错误¶
把内联字面量当作所提供的值¶
class InlineLiteralProvideDont : Component
{
public override Element Render()
{
var (mode, setMode) = UseState("light");
var (scale, setScale) = UseState(14);
// Don't — a fresh ThemeConfig every render re-renders every consumer
// in the subtree even when mode and scale are unchanged.
return VStack(8, Component<ThemedHeading>())
.Provide(ThemeContexts.Theme, new ThemeConfig(mode, scale, "#0078D4"));
}
}
每次渲染都会构造一个全新的 ThemeConfig,它与上一次渲染的值比较不相等,于是子树中每一个
UseContext(ctx) 的消费者都会重新渲染。参见上面的稳定值的注意事项,改用
UseMemo,或把构造过程提升出去。
在任何 provider 之外读取 context¶
static class NullableUserContexts
{
// Static field default of `null`.
public static readonly Context<CurrentUser?> User = new(null);
}
class ReadsContextWithoutProvider : Component
{
public override Element Render()
{
// In a component that's somehow rendered before the root provides,
// UseContext returns the context's DefaultValue — here, null.
var user = UseContext(NullableUserContexts.User);
// So branch on the sentinel instead of dereferencing blind:
return TextBlock(user?.DisplayName ?? "(not signed in)");
}
}
当没有任何祖先调用过 .Provide 时,UseContext 会静默返回
Context<T>.DefaultValue——没有警告,也没有异常。组件会基于默认值运行,而这很少是开发者期望的结果。要么选一个可以安全渲染的非空默认值,要么把这个默认值当作"未初始化"的哨兵并显式分支(如上)。
init 模式用的是后者——匿名用户就是哨兵,认证流程会在 provider 处把它换掉。
把本该是 props 的状态放进 context¶
static class SelectionContexts
{
// Don't — a single-source-single-sink value belongs in a prop.
public static readonly Context<int> SelectedIndex = new(0);
}
class ContextForPropsDont : Component
{
public override Element Render()
{
var index = UseContext(SelectionContexts.SelectedIndex);
return TextBlock($"Selected: {index}");
}
}
如果这个值是从一个父组件流向一个子组件,那它就是 prop。Context 适用于横切性的数据——主题、区域设置、当前用户、功能开关——即那些处于不同深度、相距很远的众多组件都需要的数据。硬把一种"单来源单去处"的关系塞进 context,会掩盖数据流,并让该组件无法带着不同的选中项被复用。参见 React 关于 context 替代方案的指导。
小贴士¶
把 context 用于横切关注点。 主题、区域设置、当前用户、功能开关、调度器句柄——任何被处于不同深度的众多组件所需要的东西。组件特有的数据是 props 的职责。
始终设置有意义的默认值。 读取 context 的组件即便没有 provider 也应该能工作——默认值让独立测试、Storybook 式预览,以及针对孤立组件的顺手开发成为可能。
把所提供的值记忆化。 用 UseMemo 并带上显式依赖项,把这个值包起来。每次渲染都内联字面量,会让子树里的每个消费者都跟着抖动。
在有帮助的地方做局部覆盖。 在单个测试面板上加一个仅用于调试的 .Provide(),就能预览另一种主题、另一种区域设置或一个被模拟的用户,而无需重新接线应用的其余部分。参见嵌套 Context 覆盖一节。
把 context 与 UseState 组合起来表达动态值。 在 provider 组件里把值存进状态,需要时把 setter 通过一个独立的命令 context 传下去,那么 setter 被调用时消费者就会自动更新。