如何避免 React Context 的重复渲染?

代码如下,请问在只更新 Box1 的 count 的值时,如何避免 Box2,Box3,Box4 的重复渲染?

import React, { memo, useCallback, useContext, useRef } from "react";

const Context = React.createContext<any>({});

export default function App() {
  const [count, setCount] = React.useState(0);
  const [flag, setFlag] = React.useState(false);
  const add = useCallback(() => setCount((v) => v + 1), []);
  const toggle = useCallback(() => setFlag((v) => !v), []);
  const contextValue = { count, add, flag, toggle };
  return (
    <Context.Provider value={contextValue}>
      <Box1 />
      <Box2 />
      <Box3 />
      <Box4 />
    </Context.Provider>
  );
}

const Box1 = memo(() => {
  const { count } = useContext(Context);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box1 count:{count}
    </div>
  );
});

const Box2 = memo(() => {
  const { flag } = useContext(Context);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box2 flag:{flag}
    </div>
  );
});

const Box3 = memo(() => {
  const { add } = useContext(Context);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" onClick={add} style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box3 add
    </div>
  );
});

const Box4 = memo(() => {
  const { toggle } = useContext(Context);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" onClick={toggle} style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box4 toggle
    </div>
  );
});
阅读 1.7k
avatarAI BotBETA

要避免 React Context 的重复渲染,你可以使用 React.memouseCallback 钩子来优化你的组件。React.memo 可以防止组件因 props 的浅比较而进行不必要的渲染,而 useCallback 可以防止不必要的上下文值的更新。

然而,当你在 Context 提供者的子组件中使用 useContext 时,即使 Context 的值没有改变,子组件也会由于父组件的重新渲染而重新渲染。这是 React 的正常行为,无法通过 React.memouseCallback 来避免。

要让 Box2,Box3,Box4 在只更新 Box1 的 count 的值时不重复渲染,你可能需要改变你的代码结构,以减少不必要的 Context 更新。你可以创建一个专门用于传递 count 值的 Context,并仅在 count 发生变化时更新它。这样,只有 count 相关的组件(如 Box1 和 Box3)才会因 count 的改变而重新渲染。

如下面的示例:

import React, { createContext, useState, useCallback, useContext, memo } from "react";

const CountContext = createContext<number>(0);

export function App() {
  const [count, setCount] = useState(0);
  const add = useCallback(() => setCount((v) => v + 1), []);

  return (
    <CountContext.Provider value={{ count, add }}>
      <Box1 />
      <Box2 />
      <Box3 />
      <Box4 />
    </CountContext.Provider>
  );
}

const Box1 = memo(() => {
  const { count } = useContext(CountContext);
  return <div className="box">Box1 count: {count}</div>;
});

const Box2 = memo(() => {
  const { count } = useContext(CountContext);
  return <div className="box">Box2 count: {count}</div>;
});

const Box3 = memo(() => {
  const { add } = useContext(CountContext);
  return (
    <div className="box" onClick={add}>
      Box3 add
    </div>
  );
});

const Box4 = memo(() => {
  // 这个组件并不关心 count 的值,所以它不需要依赖 CountContext。
  return <div className="box">Box4</div>;
});

这样,即使 Box1 的 count 值改变,Box2 和 Box3 也只会在其自己的 count 值改变时才会重新渲染。Box4 则完全不依赖于 count 值,因此它不会因为 count 的改变而重新渲染。

2 个回答

确实有一些问题,把state 和 actions 分开这个会好一点:

const StateContext = React.createContext<any>(null);
const ActionsContext = React.createContext<any>(null);

export default function App() {
  const [count, setCount] = React.useState(0);
  const [flag, setFlag] = React.useState(false);

  const state = { count, flag };
  
  const actions = useMemo(() => ({
    add: () => setCount(v => v + 1),
    toggle: () => setFlag(v => !v)
  }), []);

  return (
    <ActionsContext.Provider value={actions}>
      <StateContext.Provider value={state}>
        <Box1 />
        <Box2 />
        <Box3 />
        <Box4 />
      </StateContext.Provider>
    </ActionsContext.Provider>
  );
}

const Box1 = memo(() => {
  const { count } = useContext(StateContext);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box1 count:{count}
    </div>
  );
});

const Box2 = memo(() => {
  const { flag } = useContext(StateContext);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box2 flag:{flag}
    </div>
  );
});

const Box3 = memo(() => {
  const { add } = useContext(ActionsContext);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" onClick={add} style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box3 add
    </div>
  );
});

const Box4 = memo(() => {
  const { toggle } = useContext(ActionsContext);
  const ref = useRef(true);
  ref.current = !ref.current;
  return (
    <div className="box" onClick={toggle} style={{ backgroundColor: ref.current ? "white" : "gray" }}>
      Box4 toggle
    </div>
  );
});

用 useMemo:

export default function App() {
  const [count, setCount] = React.useState(0);
  const [flag, setFlag] = React.useState(false);
  const add = useCallback(() => setCount((v) => v + 1), []);
  const toggle = useCallback(() => setFlag((v) => !v), []);
  
  const contextValue = useMemo(() => {
    return { count, add, flag, toggle };
  }, [count, add, flag, toggle]);

  return (
    <Context.Provider value={contextValue}>
      <Box1 />
      <Box2 />
      <Box3 />
      <Box4 />
    </Context.Provider>
  );
}


拆分 Context:

const CountContext = React.createContext();
const FlagContext = React.createContext();

export default function App() {
  const [count, setCount] = React.useState(0);
  const [flag, setFlag] = React.useState(false);

  const add = useCallback(() => setCount((v) => v + 1), []);
  const toggle = useCallback(() => setFlag((v) => !v), []);

  return (
    <CountContext.Provider value={{ count, add }}>
      <FlagContext.Provider value={{ flag, toggle }}>
        <Box1 />
        <Box2 />
        <Box3 />
        <Box4 />
      </FlagContext.Provider>
    </CountContext.Provider>
  );
}

const Box1 = memo(() => {
  const { count } = useContext(CountContext);
  // ...
});

const Box2 = memo(() => {
  const { flag } = useContext(FlagContext);
  // ...
});
useMemo是无效的,即使组件已被记忆化,当其使用的 context 发生变化时,它仍将重新渲染。记忆化只与从父组件传递给组件的 props 有关。见官方文档
  1. 拆分Context就不说了。
  2. 拆分组件为两层

    const Box1 = memo(() => {
      const { count, add } = useContext(Context);
      const ref = useRef(true);
      ref.current = !ref.current;
      console.log("渲染Box1");
      return (
     <Box1Son count={count} />
      );
    });
    
    import { memo } from "react";
    
    function Box1Son({ count }) {
      console.log("渲染Box1Son");
      return (
     <div className="box">
       我是Box1 Box1 count:{count}
       {/* <Button onClick={add}>点击</Button> */}
     </div>
      );
    }
    
    export default memo(Box1Son);
    
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题