constate 原理解析

4,233 阅读4分钟

原文地址:github.com/whinc/blog/…

constate 介绍

constate 是一个基于 React Hooks 和 React Context 的轻量级状态管理库。

constate 的主要功能是将自定义 Hooks 的执行结果提升到 Context 中,利用 React Context 通信机制,把结果提供给子组件消费使用,从而实现跨组件的状态共享。

constate 使用示例

下面是 constate 提供的示例:

import React, { useState } from "react";
import createContextHook from "constate";
 
// 创建自定义 Hooks
function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(prevCount => prevCount + 1);
  return { count, increment };
}
 
// 以自定义 Hooks 作为参数调用 constate 提供的 createContextHook() 函数,
// 并返回一个新函数 useCounterContext
const useCounterContext = createContextHook(useCounter);
 
// 使用 <useCounterContext.Provider> 作为根组件,提供状态共享的容器
function App() {
  return (
    <useCounterContext.Provider>
      <Count />
      <Button />
    </useCounterContext.Provider>
  );
}
 
// 子组件<Button>
function Button() {
  // 调用 useCounterContext() 获取自定义 Hooks useCounter 的返回结果
  const { increment } = useCounterContext();
  // 点击按钮后调用 increment 更新自定义 hooks 中的状态 count,引起组件 <useCounterContext.Provider> 更新
  return <button onClick={increment}>+</button>;
}
 
// 子组件<Count>
function Count() {
  // 调用 useCounterContext() 获取自定义 Hooks useCounter 的返回结果
  const { count } = useCounterContext();
  // 当<useCounterContext.Provider> 更新时重新渲染
  return <span>{count}</span>;
}

在上面代码中,首先调用了 constate 提供的createContextHooks函数,并将自定义 Hooks useCounter作为参数传入,函数调用后返回一个新的 Hooks useCounterContext,在子组件中使用它获取useCounter的内部状态,即countincrement,这样就可以在子组件中使用自定义 Hooks 中的状态了。

constate 是基于 React Context 实现的,所以需要用类似 Context.Provider 这样的组件来包裹子组件,示例中对应的是userCounterContext.Provider(内部实际是包装了Context.Provider,下面会进一步介绍)。

你也可以在线修改这个示例并查看效果。

Edit constate-counter

constate 原理解析

下面是精简后的 constate 实现代码,使用 TypeScript 编写,为了突出重点,我已去掉了一些无关的代码。(constate 还在更新中,源码可能会有所不同)

function createContextHook<P, V>(useValue: (props: P) => V) {
  const Context = React.createContext({});
 
  const Provider: React.FunctionComponent<P> = props => {
    // 执行传入的 Hooks 获取结果,并将结果作为 Context 容器的值,这样子组件通过 <Context.Consumer> 或 useContext() 可以获取
    const value = useValue(props);
    return (
      <Context.Provider value={value}>
        {props.children}
      </Context.Provider>
    );
  };
 
  // 子组件中执行 useContext() 时,实际获取到的是内部 Context 容器中的值——传入的 Hooks 的执行结果
  const useContext = () => React.useContext(Context);
  useContext.Context = Context;
  useContext.Provider = Provider;
  return useContext;
}

从源码看 constate 的实现是比较简单的:constate 内部定义了一个Provider组件,并将传入的自定义 Hooks 的返回值作为 Context 的 value,constate 最终会返回一个 Hooks,该 Hooks 调用后实际执行useContext来获取到 Context 中的 value,即传入的自定义 Hooks 的返回值。

下面是我绘制的一个调用关系图,蓝色方块是外部用户代码,灰色方块是内部实现代码。

通过几个 Q&A,聚焦对几个重要点的理解。

  1. 子组件是如何获取到自定义 Hooks 的返回值的?

答:将自定义 Hooks 的返回值传递给<Context.Provider>组件,并用该组件作为子组件的祖先,利用 React Context 的跨组件通信机制,子组件从 Context 中获取值。

  1. 子组件是如何更新 Context 容器中的值的?

答:子组件更新时调用自定义 Hooks 返回更新函数(如示例中的increment),该更新方法最终会调用useState()返回的更新函数(如示例中的setCount),这会引起自定义 Hooks 所在组件(如示例中的useCounterContext.Provider)重新渲染,并更新值。

  1. 子组件 A 如何影响子组件 B 的显示?

答:根据第2点的回答,子组件 A 调用更新函数后,会引起根组件更新,根组件内部其实又是一个 React Context,所以会引起使用到该 Context 中值的 B 组件也随之更新,从而子组件 B 显示最新的值。

小结

hooks 导入后即可使用,不受组件层级关系限制。constate 利用这一点,对 React Context 和useContext进行封装,让我们可以在根组件定义 Provider 后,在任意子组件中使用自定义 Hooks 的返回结果。

带来的好处首先是基于 Context 实现了状态共享,其次是可伸缩的。可伸缩意味着刚开始状态可以放在自定义 Hooks 中,像组件内部状态一样使用,当发现组件内部状态需要被其他组件共享时,通过 constate 将自定义 Hooks 返回的状态提升到 Context 中进行状态共享,不需要修改自定义 Hooks 的代码,即可实现组件内部状态到共享状态的平滑过渡。

constate 本质上依然是将 Context 进行封装,建立父与子组件之间的通信来简化基于 Context 的状态共享,这种方式在 hooks 之前就已经存在,不过是使用类组件+高阶组件的方式实现。