useContext定义全局状态

useContext定义全局状态

Content #

React 组件之间的状态传递只有一种方式,那就是通过 props。这就意味着这种传递关系只能在父子组件之间进行。

如果要跨层次,或者同层的组件之间要进行数据的共享,那应该如何去实现呢?这其实就涉及到一个新的命题:全局状态管理。

为此,React 提供了 Context 这样一个机制,能够让所有在某个组件开始的组件树上创建一个 Context。这样这个组件树上的所有组件,就都能访问和修改这个 Context 了。那么在函数组件里,我们就可以使用 useContext 这样一个 Hook 来管理 Context。

useContext 的 API 签名如下:

const value = useContext(MyContext);

正如刚才提到的,一个 Context 是从某个组件为根组件的组件树上可用的,所以我们需要有 API 能够创建一个 Context,这就是 React.createContext API,如下:

const MyContext = React.createContext(initialValue);

这里的 MyContext 具有一个 Provider 的属性,一般是作为组件树的根组件。这里我仍然以 React 官方文档的例子来讲解,即:一个主题的切换机制。代码如下:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
// 创建一个 Theme 的 Context
const ThemeContext = React.createContext(themes.light);
function App() {
  // 整个应用使用 ThemeContext.Provider 作为根组件
  return (
    // 使用 themes.dark 作为当前 Context
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{
      background: theme.background,
      color: theme.foreground
    }}>
      I am styled by theme context!
    </button>
  );
}

Context 看上去就是一个全局的数据,为什么要设计这样一个复杂的机制,而不是直接用一个全局的变量去保存数据呢?

答案其实很简单,就是为了能够进行数据的绑定。当这个 Context 的数据发生变化时,使用这个数据的组件就能够自动刷新。但如果没有 Context,而是使用一个简单的全局变量,就很难去实现了。

不过刚才我们看到的其实是一个静态的使用 Context 的例子,直接用了 thems.dark 作为 Context 的值。那么如何让它变得动态呢?

比如说常见的切换黑暗或者明亮模式的按钮,用来切换整个页面的主题。事实上,动态 Context 并不需要我们学习任何新的 API,而是利用 React 本身的机制,通过这么一行代码就可以实现:

<ThemeContext.Provider value={themes.dark}>

可以看到,themes.dark 是作为一个属性值传给 Provider 这个组件的,如果要让它变得动态,其实只要用一个 state 来保存,通过修改 state,就能实现动态的切换 Context 的值了。而且这么做,所有用到这个 Context 的地方都会自动刷新。比如这样的代码:

// ...
function App() {
  // 使用 state 来保存 theme 从而可以动态修改
  const [theme, setTheme] = useState("light");

  // 切换 theme 的回调函数
  const toggleTheme = useCallback(() => {
    setTheme((theme) => (theme === "light" ? "dark" : "light"));
  }, []);

  return (
    // 使用 theme state 作为当前 Context
    <ThemeContext.Provider value={themes[theme]}>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

在这段代码中,我们使用 state 来保存 theme,从而达到可以动态调整的目的。

可以看到,Context 提供了一个方便在多个组件之间共享数据的机制。不过需要注意的是,它的灵活性也是一柄双刃剑。你或许已经发现,Context 相当于提供了一个定义 React 世界中全局变量的机制,而全局变量则意味着两点:

  1. 会让调试变得困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
  2. 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。

所以在 React 的开发中,除了像 Theme、Language 等一目了然的需要全局设置的变量外,我们很少会使用 Context 来做太多数据的共享。需要再三强调的是, Context 更多的是提供了一个强大的机制,让 React 应用具备定义全局的响应式数据的能力。

Viewpoints #

From #

04|内置 Hooks(2):为什么要避免重复定义回调函数?