1. Props Drilling 问题
当需要将数据从顶层组件传递到深层嵌套的子组件时,中间层组件需要接收并转发这些 props,即使它们自身并不使用这些数据。这就是 props drilling(属性穿透)问题:
// 问题:theme 需要穿越 Layout → Sidebar → NavItem,但 Layout 和 Sidebar 不用它
<App theme="dark">
<Layout theme="dark"> {/* Layout 只是转发 theme */}
<Sidebar theme="dark"> {/* Sidebar 只是转发 theme */}
<NavItem theme="dark"/> {/* NavItem 真正使用 theme */}
</Sidebar>
</Layout>
</App>
2. createContext + useContext
createContext 创建一个 Context 对象,useContext 在任意子组件中读取最近的 Provider 提供的值:
import { createContext, useContext, createSignal } from "solid-js";
import type { JSX } from "solid-js";
// 1. 定义 Context 类型
type ThemeContextType = {
theme: () => "light" | "dark";
toggle: () => void;
};
// 2. 创建 Context(传入默认值,当没有 Provider 时使用)
const ThemeContext = createContext<ThemeContextType>({
theme: () => "light",
toggle: () => {},
});
// 3. 创建 Provider 组件(封装状态)
function ThemeProvider(props: { children: JSX.Element }) {
const [theme, setTheme] = createSignal<"light" | "dark">("dark");
const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{props.children}
</ThemeContext.Provider>
);
}
// 4. 在任意子组件中消费
function NavItem() {
const { theme, toggle } = useContext(ThemeContext);
return (
<button
class={`nav-item theme-${theme()}`}
onClick={toggle}
>
当前主题:{theme()}
</button>
);
}
SolidJS Context 的独特之处:Context 值可以直接包含 Signal getter(函数),子组件读取时只需调用函数即可获得响应式值。这与 React Context 不同——React 需要把 setState 也一起传递,而 SolidJS 直接传 Signal 的读/写函数。
3. 实战:主题系统
// theme-context.tsx — 完整主题系统
import { createContext, useContext, createSignal, createEffect } from "solid-js";
type Theme = "light" | "dark" | "system";
interface ThemeState {
theme: () => Theme;
resolved: () => "light" | "dark"; // "system" 解析后的实际值
setTheme: (t: Theme) => void;
}
const ThemeCtx = createContext<ThemeState>();
export function ThemeProvider(props: { children: JSX.Element }) {
const saved = localStorage.getItem("theme") as Theme || "system";
const [theme, setThemeSignal] = createSignal<Theme>(saved);
const systemDark = window.matchMedia("(prefers-color-scheme: dark)");
const [systemTheme, setSystemTheme] = createSignal<"light"|"dark">(
systemDark.matches ? "dark" : "light"
);
systemDark.addEventListener("change", (e) =>
setSystemTheme(e.matches ? "dark" : "light")
);
const resolved = createMemo(() =>
theme() === "system" ? systemTheme() : theme() as "light"|"dark"
);
// 将主题应用到 document
createEffect(() => {
document.documentElement.dataset.theme = resolved();
localStorage.setItem("theme", theme());
});
const setTheme = (t: Theme) => setThemeSignal(t);
return (
<ThemeCtx.Provider value={{ theme, resolved, setTheme }}>
{props.children}
</ThemeCtx.Provider>
);
}
export const useTheme = () => {
const ctx = useContext(ThemeCtx);
if (!ctx) throw new Error("useTheme 必须在 ThemeProvider 内使用");
return ctx;
};
4. 实战:全局用户状态
// auth-context.tsx
interface User { id: string; name: string; role: "admin" | "user"; }
interface AuthState {
user: () => User | null;
login: (user: User) => void;
logout: () => void;
isAdmin: () => boolean;
}
const AuthCtx = createContext<AuthState>();
export function AuthProvider(props: { children: JSX.Element }) {
const [user, setUser] = createSignal<User | null>(null);
const login = (u: User) => setUser(u);
const logout = () => setUser(null);
const isAdmin = () => user()?.role === "admin";
return (
<AuthCtx.Provider value={{ user, login, logout, isAdmin }}>
{props.children}
</AuthCtx.Provider>
);
}
export const useAuth = () => useContext(AuthCtx)!;
// 在任意组件中使用
function AdminPanel() {
const { user, isAdmin, logout } = useAuth();
return (
<Show when={isAdmin()} fallback={<p>无权限</p>}>
<div>
<p>管理员:{user()?.name}</p>
<button onClick={logout}>退出</button>
</div>
</Show>
);
}
5. Context vs Store:如何选择
| 维度 | Context | 全局 Store(模块级) |
|---|---|---|
| 作用域 | Provider 树内 | 全局(整个应用) |
| 多实例 | 支持(多个 Provider) | 通常单例 |
| 服务端渲染 | 天然隔离(每次请求) | 需要手动重置 |
| 测试隔离 | 容易(包裹 Provider) | 需要 mock 模块 |
| 推荐场景 | 主题、认证、国际化 | 全应用共享的缓存数据 |
本章小结:createContext + Provider 是 SolidJS 跨层级状态共享的官方方案。将 Signal/Store 的读写函数放入 Context value,子组件直接 useContext 即可获得响应式数据。建议将 Context 和 Provider 封装成独立模块,并暴露自定义 hook(如 useTheme、useAuth)简化使用。