Chapter 06

Context 与依赖注入

用 createContext + Provider 优雅地跨层级共享状态,避免 props drilling

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)简化使用。