重学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等状态库
相关推荐
listhi5201 小时前
利用React Hooks简化状态管理
前端·javascript·react.js
一点一木1 小时前
🚀 2025 年 10 月 GitHub 十大热门项目排行榜 🔥
前端·人工智能·github
华仔啊1 小时前
这个Vue3旋转菜单组件让项目颜值提升200%!支持多种主题,拿来即用
前端·javascript·css
非凡ghost2 小时前
Adobe Lightroom安卓版(手机调色软件)绿色版
前端·windows·adobe·智能手机·软件需求
BestAns2 小时前
Postman 平替?这款轻量接口测试工具,本地运行 + 批量回归超实用!
前端
专注前端30年3 小时前
Webpack进阶玩法全解析(性能优化+高级配置)
前端·webpack·性能优化
烛阴3 小时前
Lua世界的基石:变量、作用域与七大数据类型
前端·lua
张拭心3 小时前
“不卷 AI、不碰币、下班不收消息”——Android 知名技术大牛 Jake Wharton 的求职价值观
android·前端·aigc
SoaringHeart3 小时前
Flutter疑难解决:单独让某个页面的电池栏标签颜色改变
前端·flutter
Yeats_Liao3 小时前
Go Web 编程快速入门 13 - 部署与运维:Docker容器化、Kubernetes编排与CI/CD
运维·前端·后端·golang