Избегайте ненужных повторных рендеров с помощью React hook useContext - PullRequest
0 голосов
/ 14 апреля 2020

Я хочу найти способ избежать ненужных повторных рендеров при использовании useContext. Я создал игрушечную песочницу, которую вы можете найти здесь: https://codesandbox.io/s/eager-mclean-oupkx, чтобы продемонстрировать проблему. Если вы откроете консоль разработчика, вы увидите, что console.log происходит каждую секунду.

В этом примере у меня есть контекст, который просто увеличивает счетчик каждую 1 секунду. Затем у меня есть компонент, который потребляет его. Я не хочу, чтобы каждый тик счетчика вызывал повторную визуализацию компонента. Например, я могу просто хотеть обновлять свой компонент каждые 10 тиков, или я могу хотеть некоторый другой компонент, который обновляется с каждым тиком. Я попытался создать хук для переноса useContext, а затем вернуть значение stati c, надеясь, что это предотвратит повторные рендеры, но безрезультатно.

Лучший способ преодолеть это проблема заключается в том, чтобы сделать что-то вроде обернуть мой Component в HO C, который потребляет count через useContext, а затем передает значение в Component в качестве реквизита. Это выглядит как окольный способ сделать что-то простое.

Есть ли лучший способ сделать это?

1 Ответ

0 голосов
/ 14 апреля 2020

Компоненты, которые прямо или косвенно вызывают useContext через пользовательский хук, будут рендериться каждый раз, когда изменяется значение провайдера.

Вы можете вызвать useContext из контейнера и позволить контейнеру отобразить чистый компонент или Пусть контейнер вызывает функциональный компонент, который запоминается с помощью useMemo.

В приведенном ниже примере значение счетчика меняется каждые 100 миллисекунд, но, поскольку useMyContext возвращает Math.floor(count / 10), компоненты будут отображаться только один раз в секунду. Контейнеры будут «рендериться» каждые 100 миллисекунд, но они будут возвращать один и тот же jsx 9 из 10 раз.

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, возвращает что-то отличное от того, которое было в прошлый раз run и все функции, переданные useSelector, будут работать при изменении избыточного хранилища. Так что здесь вам не нужно разделять контейнер и представление для предотвращения повторного рендеринга.

Компоненты также будут повторно рендериться, если их родительский объект рендерится и передает им другие реквизиты или если родительский рендеринг воспроизводится, а компонент является функциональным компонентом (функциональные компоненты всегда перерисовываются). Но так как в примере кода родительский элемент (App) никогда не рендерится, мы можем определить контейнеры как функциональные компоненты (не нужно переносить в React.memo).

Ниже приведен пример того, как вы можете создать подключенный компонент аналогичен реагирующему соединению с редуксом, но вместо этого он подключается к контексту:

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>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...