使用React钩子useContext避免不必要的重新渲染

时间:2020-04-14 03:04:10

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

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

在此示例中,我有一个上下文,该上下文仅每1秒增加一个计数器。然后,我有一个消耗它的组件。我不希望计数器的每个刻度都导致组件上的重新渲染。例如,我可能只想每隔10个滴答声更新一次组件,或者我想要一些其他随着滴答声更新的组件。我尝试创建一个用于包装useContext的钩子,然后返回一个静态值,希望这样做不会阻止重新渲染。

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

有更好的方法吗?

1 个答案:

答案 0 :(得分:0)

通过自定义钩子直接或间接调用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的所有函数将在redux存储更改时运行,才会重现调用useSelector的组件。因此,在这里您不必拆分容器和演示文稿即可防止重新渲染。

如果组件的父级重新渲染并通过不同的prop传递给它们,或者如果父级重新渲染并且组件是功能性组件(功能性组件始终会重新渲染),则组件也会重新渲染。但是,由于在代码示例中,父代(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>