reactjs - 使用 React hook useContext 避免不必要的重新渲染

标签 reactjs react-redux react-hooks react-context

我想找到一种方法来避免使用useContext时不必要的重新渲染。 。我创建了一个玩具沙箱,您可以在这里找到它:https://codesandbox.io/s/eager-mclean-oupkx来演示问题。如果打开开发控制台,您将看到 console.log每秒都在发生。

在此示例中,我的上下文只是每 1 秒递增一个计数器。然后我有一个消耗它的组件。我不希望计数器的每一次滴答都会导致组件重新渲染。例如,我可能只想每 10 个刻度更新一次我的组件,或者我可能想要每个刻度更新一次的其他组件。我尝试创建一个钩子(Hook)来包裹 useContext ,然后返回一个静态值,希望能够阻止重新渲染,但没有成功。

我能想到的解决这个问题的最好方法是做一些像包裹我的 Component 的事情。在 HOC 中,消耗 count通过useContext ,然后将该值传递到 Component作为 Prop 。这似乎是完成本应简单的事情的一种迂回方式。

有更好的方法吗?

最佳答案

每次提供者的值发生变化时,直接或间接通过自定义 Hook 调用 useContext 的组件都会重新渲染。

您可以从容器调用 useContext 并让容器渲染纯组件,或者让容器调用使用 useMemo 内存的功能组件。

在下面的示例中,计数每 100 毫秒更改一次,但由于 useMyContext 返回 Math.floor(count/10) 组件每秒只会渲染一次。容器将每 100 毫秒“渲染”一次,但它们将在 10 次中返回相同的 jsx 9 次。

const {
  useEffect,
  useMemo,
  useState,
  useContext,
  memo,
} = React;
const MyContext = React.createContext({ count: 0 });
function useMyContext() {
  const { count } = useContext(MyContext);

  return Math.floor(count / 10);
}

// simple context that increments a timer
const Context = ({ children }) => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const i = setInterval(() => {
      setCount((c) => c + 1);
    }, [100]);
    return () => clearInterval(i);
  }, []);

  const value = useMemo(() => ({ count }), [count]);
  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
};
//pure component
const MemoComponent = memo(function Component({ count }) {
  console.log('in memo', count);
  return <div>MemoComponent {count}</div>;
});
//functional component
function FunctionComponent({ count }) {
  console.log('in function', count);
  return <div>Function Component {count}</div>;
}

// container that will run every time context changes
const ComponentContainer1 = () => {
  const count = useMyContext();
  return <MemoComponent count={count} />;
};
// second container that will run every time context changes
const ComponentContainer2 = () => {
  const count = useMyContext();
  //using useMemo to not re render functional component
  return useMemo(() => FunctionComponent({ count }), [
    count,
  ]);
};

function App() {
  console.log('App rendered only once');
  return (
    <Context>
      <ComponentContainer1 />
      <ComponentContainer2 />
    </Context>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>

使用 React-redux useSelector,只有当传递给 useSelector 的函数返回与上次运行时不同的内容时,调用 useSelector 的组件才会重新渲染,并且传递给 useSelector 的所有函数都会在以下情况下运行: redux 存储发生变化。因此,在这里您不必拆分容器和演示文稿来防止重新渲染。

如果父组件重新渲染并向它们传递不同的 props,或者父组件重新渲染并且组件是功能组件(功能组件始终重新渲染),组件也会重新渲染。但由于在代码示例中,父级(App)永远不会重新渲染,我们可以将容器定义为功能组件(无需包装在 React.memo 中)。

以下是如何创建类似于 React Redux Connect 的连接组件的示例,但它将连接到上下文:

const {
  useMemo,
  useState,
  useContext,
  useRef,
  useCallback,
} = React;
const { createSelector } = Reselect;
const MyContext = React.createContext({ count: 0 });
//react-redux connect like function to connect component to context
const connect = (context) => (mapContextToProps) => {
  const select = (selector, state, props) => {
    if (selector.current !== mapContextToProps) {
      return selector.current(state, props);
    }
    const result = mapContextToProps(state, props);
    if (typeof result === 'function') {
      selector.current = result;
      return select(selector, state, props);
    }
    return result;
  };
  return (Component) => (props) => {
    const selector = useRef(mapContextToProps);
    const contextValue = useContext(context);
    const contextProps = select(
      selector,
      contextValue,
      props
    );
    return useMemo(() => {
      const combinedProps = {
        ...props,
        ...contextProps,
      };
      return (
        <React.Fragment>
          <Component {...combinedProps} />
        </React.Fragment>
      );
    }, [contextProps, props]);
  };
};
//connect function that connects to MyContext
const connectMyContext = connect(MyContext);
// simple context that increments a timer
const Context = ({ children }) => {
  const [count, setCount] = useState(0);
  const increment = useCallback(
    () => setCount((c) => c + 1),
    []
  );
  return (
    <MyContext.Provider value={{ count, increment }}>
      {children}
    </MyContext.Provider>
  );
};
//functional component
function FunctionComponent({ count }) {
  console.log('in function', count);
  return <div>Function Component {count}</div>;
}
//selectors
const selectCount = (state) => state.count;
const selectIncrement = (state) => state.increment;
const selectCountDiveded = createSelector(
  selectCount,
  (_, divisor) => divisor,
  (count, { divisor }) => Math.floor(count / divisor)
);
const createSelectConnectedContext = () =>
  createSelector(selectCountDiveded, (count) => ({
    count,
  }));
//connected component
const ConnectedComponent = connectMyContext(
  createSelectConnectedContext
)(FunctionComponent);
//app is also connected but won't re render when count changes
//  it only gets increment and that never changes
const App = connectMyContext(
  createSelector(selectIncrement, (increment) => ({
    increment,
  }))
)(function App({ increment }) {
  const [divisor, setDivisor] = useState(0.5);
  return (
    <div>
      <button onClick={increment}>increment</button>
      <button onClick={() => setDivisor((d) => d * 2)}>
        double divisor
      </button>
      <ConnectedComponent divisor={divisor} />
      <ConnectedComponent divisor={divisor * 2} />
      <ConnectedComponent divisor={divisor * 4} />
    </div>
  );
});

ReactDOM.render(
  <Context>
    <App />
  </Context>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>

关于reactjs - 使用 React hook useContext 避免不必要的重新渲染,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61200063/

相关文章:

javascript - 使用 React,如何将单击事件附加到类或 ID?

node.js - create-react-app my-app redux 与 new react 18.0.0 的错误

reactjs - React 和 Redux 工具包 - promise 后拒绝

javascript - 更新状态时如何防止模态重新渲染?

javascript - 如何在js中从字符串构建变量?

javascript - 如何在另一个自定义 Hook 中使用返回值的自定义 Hook ?

reactjs - 如何使用 webpack 注册 Service Worker?

javascript - react + Redux : Changing input focus programatically

javascript - 如何在 promise 中设置 react 钩子(Hook)的值(value)?

javascript - 当我单击 native react 按钮时如何显示或切换文本?