重学React——memo能防止Context的额外渲染吗

Context在项目中应用的最大问题是全局更新------在顶层修改Context的value,会导致整个组件树都会重新渲染。

使用memo防止额外渲染

如下代码中,Doo组件并没有依赖任何context:

typescript 复制代码
const context = createContext({ count1: 0, count2: 0 });
function Foo() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const add1 = useCallback(() => {
    setCount1((value) => ++value);
  }, []);
  const add2 = useCallback(() => {
    setCount2((value) => ++value);
  }, []);

  return (
    <context.Provider value={{ count1: count1, count2: count2 }}>
      <div>
        <button
          className="border rounded-full px-2 bg-blue-400 text-white"
          onClick={add1}
        >
          add count1
        </button>
        <button
          className="border rounded-full px-2 bg-blue-400 text-white"
          onClick={add2}
        >
          add count2
        </button>
        <Boo></Boo>
        <Coo></Coo>
        <Doo></Doo>
      </div>
    </context.Provider>
  );
}
function Boo() {
  const { count1 } = useContext(context);
  console.log("render boo");
  return <div>boo:使用context.count1:{count1}</div>;
}
function Doo() {
  console.log("render doo");
  return <div>doo:未使用context</div>;
}
function Coo() {
  const { count2 } = useContext(context);
  console.log("render coo");
  return <div>coo:使用context.count2:{count2}</div>;
}

点击按钮后Doo仍然会重新渲染:

这一点很好理解------父级的setState会触发发子组件的render。因此只要对子组件增加memo就可以阻止该次render。

diff 复制代码
+const MemoDoo = memo(Doo);
function Foo() {
  ...
  return (
    <context.Provider value={{ count1: count1, count2: count2 }}>
      <div>
       ...

-       <Doo></Doo>
+       <MemoDoo></MemoDoo>
      </div>
    </context.Provider>
  );
}

这样Doo组件确实没有额外更新了,但是Boo和Coo,两个组件分别依赖context中某个值,该值即使没有变也会更新。这种情况即便添加了memo,也无法阻止额外更新。

这一点也不难理解,useContext并不会关心你到底用到了context中哪个值,只要context更新,那么组件就会更新。

因此想要解决这个问题,就得确保context不变。这一点似乎又和全局状态共享冲突:如何一方面保持context不变,另一方面又能接收到最新的状态值呢?

useSelector和useSyncExternalStore

上文提到需要保持context不变,又要响应状态更新,因此状态要脱离react的管理,context中存放状态存取的方法,再用useSyncExternalStore去响应外部的状态更新,就可以完美实现以上诉求。

jsx 复制代码
// ============= 创建 Store =============
function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();

  return {
    getState: () => state,
    setState: (newState) => {
      state = typeof newState === "function" ? newState(state) : newState;
      listeners.forEach((l) => l());
    },
    subscribe: (listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

// ============= Context Provider =============
const StoreContext = createContext(null);

export function StoreProvider({ children, initialState }) {
  const store = useMemo(() => createStore(initialState), []);
  return (
    <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
  );
}

// ============= useSelector 实现 =============
export function useSelector(selector) {
  const store = useContext(StoreContext);
  if (!store) throw new Error("useSelector must be used within StoreProvider");

  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
    () => selector(store.getState())
  );
}

// ============= 派发方法 =============
export function useStore() {
  const store = useContext(StoreContext);
  if (!store) throw new Error("useStore must be used within StoreProvider");
  return store.setState;
}
function UserInfo() {
  const user = useSelector((s) => s.user);
  console.log("UserInfo render");
  return <div>👤 {user.name}</div>;
}

function ThemeInfo() {
  const theme = useSelector((s) => s.theme);
  console.log("ThemeInfo render");
  return <div>🎨 {theme}</div>;
}

function Test2() {
  const initialState = { user: { name: "Alice" }, theme: "light" };

  return (
    <StoreProvider initialState={initialState}>
      <Toolbar />
    </StoreProvider>
  );
}

function Toolbar() {
  const setState = useStore();

  const toggleTheme = () =>
    setState((s) => ({ ...s, theme: s.theme === "light" ? "dark" : "light" }));

  const renameUser = () =>
    setState((s) => ({ ...s, user: { name: s.user.name + "!" } }));

  return (
    <>
      <UserInfo />
      <ThemeInfo />
         <button
        className="border rounded-full px-2 bg-blue-400 text-white"
        onClick={toggleTheme}
      >
        Toggle Theme
      </button>
      <button
        className="border rounded-full px-2 bg-blue-400 text-white"
        onClick={renameUser}
      >
        Rename User
      </button>
    </>
  );
}

效果如下------状态变更后,两个组件分别更新:

其他思路

如果是react 16,无法使用useSyncExternalStore,还有其他办法吗?有的。

参考Redux提供connect函数,该函数接收一个组件和一个selector,内部再对该组件包裹一个context.Provider,value是selector后的值,就可以实现按需更新。

总结

  1. 组件使用useContext后,memo无法阻止不必要的更新
  2. 不建议使用Context作状态管理,但可以作全局静态数据的共享
  3. 简单项目可以使用Context + useSyncExternalStore自行封装状态库,不需要依赖第三方库
  4. 如果自行维护状态管理太麻烦,建议使用redux或zustand等状态库
相关推荐
FuckPatience5 小时前
Vue 与.Net Core WebApi交互时路由初探
前端·javascript·vue.js
小小前端_我自坚强5 小时前
前端踩坑指南 - 避免这些常见陷阱
前端·程序员·代码规范
lichenyang4535 小时前
从零实现JSON与图片文件上传功能
前端
WebGirl5 小时前
动态生成多层表头表格算法
前端·javascript
hywel6 小时前
一开始只是想整理下书签,结果做成了一个 AI 插件 😂
前端
傅里叶6 小时前
SchedulerBinding 的三个Frame回调
前端·flutter
小小前端_我自坚强6 小时前
React Hooks 使用详解
前端·react.js·redux
java水泥工6 小时前
基于Echarts+HTML5可视化数据大屏展示-车辆综合管控平台
前端·echarts·html5·大屏模版
aklry6 小时前
elpis之学习总结
前端·vue.js