React: Предотвращение повторных рендеров с помощью React.memo и хука useContext.

Созданный на 19 мар. 2019  ·  37Комментарии  ·  Источник: facebook/react

Вы хотите запросить функцию или сообщить об ошибке ?

ошибка

Каково текущее поведение?

Я не могу полагаться на данные из контекстного API, используя (useContext hook) для предотвращения ненужных повторных рендеров с помощью React.memo

Если текущее поведение является ошибкой, пожалуйста, предоставьте шаги для воспроизведения и, если возможно, минимальную демонстрацию проблемы. Вставьте ссылку в свой пример JSFiddle (https://jsfiddle.net/Luktwrdm/) или CodeSandbox (https://codesandbox.io/s/new) ниже:

React.memo(() => {
const [globalState] = useContext(SomeContext);

render ...

}, (prevProps, nextProps) => {

// How to rely on context in here?
// I need to rerender component only if globalState contains nextProps.value

});

Какое ожидаемое поведение?

У меня должен быть какой-то доступ к контексту в обратном вызове второго аргумента React.memo, чтобы предотвратить рендеринг
Или у меня должна быть возможность вернуть старый экземпляр компонента реакции в теле функции.

Какие версии React и какой браузер / ОС подвержены этой проблеме?
16.8.4

Самый полезный комментарий

Это работает как задумано. Если вам интересно, это более подробно обсуждается в https://github.com/facebook/react/issues/14110 .

Предположим, по какой-то причине у вас есть AppContext , значение которого имеет свойство theme , и вы хотите повторно визуализировать только некоторые ExpensiveTree при изменениях appContextValue.theme .

TL; DR - это то, что на данный момент у вас есть три варианта:

Вариант 1 (предпочтительно): разделить контексты, которые не меняются вместе

Если нам просто нужно appContextValue.theme во многих компонентах, но сам appContextValue меняется слишком часто, мы можем отделить ThemeContext от AppContext .

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Теперь любое изменение AppContext не будет повторно отображать потребителей ThemeContext .

Это предпочтительное исправление. Тогда вам не нужна особая помощь.

Вариант 2: разделите компонент на две части, поместив между ними memo

Если по какой-то причине вы не можете разделить контексты, вы все равно можете оптимизировать рендеринг, разделив компонент на два и передав более конкретные свойства внутреннему. Вы все равно визуализируете внешний, но он должен быть дешевым, поскольку ничего не делает.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Вариант 3: один компонент с useMemo внутри

Наконец, мы могли бы сделать наш код более подробным, но сохранить его в одном компоненте, заключив возвращаемое значение в useMemo и указав его зависимости. Наш компонент все равно будет выполняться повторно, но React не будет повторно отображать дочернее дерево, если все входные данные useMemo одинаковы.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

В будущем может быть больше решений, но это то, что у нас есть сейчас.

Тем не менее, обратите внимание, что вариант 1 предпочтительнее - если какой-то контекст меняется слишком часто, подумайте о его разделении .

Все 37 Комментарий

Это работает как задумано. Если вам интересно, это более подробно обсуждается в https://github.com/facebook/react/issues/14110 .

Предположим, по какой-то причине у вас есть AppContext , значение которого имеет свойство theme , и вы хотите повторно визуализировать только некоторые ExpensiveTree при изменениях appContextValue.theme .

TL; DR - это то, что на данный момент у вас есть три варианта:

Вариант 1 (предпочтительно): разделить контексты, которые не меняются вместе

Если нам просто нужно appContextValue.theme во многих компонентах, но сам appContextValue меняется слишком часто, мы можем отделить ThemeContext от AppContext .

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Теперь любое изменение AppContext не будет повторно отображать потребителей ThemeContext .

Это предпочтительное исправление. Тогда вам не нужна особая помощь.

Вариант 2: разделите компонент на две части, поместив между ними memo

Если по какой-то причине вы не можете разделить контексты, вы все равно можете оптимизировать рендеринг, разделив компонент на два и передав более конкретные свойства внутреннему. Вы все равно визуализируете внешний, но он должен быть дешевым, поскольку ничего не делает.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Вариант 3: один компонент с useMemo внутри

Наконец, мы могли бы сделать наш код более подробным, но сохранить его в одном компоненте, заключив возвращаемое значение в useMemo и указав его зависимости. Наш компонент все равно будет выполняться повторно, но React не будет повторно отображать дочернее дерево, если все входные данные useMemo одинаковы.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

В будущем может быть больше решений, но это то, что у нас есть сейчас.

Тем не менее, обратите внимание, что вариант 1 предпочтительнее - если какой-то контекст меняется слишком часто, подумайте о его разделении .

Оба этих параметра не будут использовать рендеринг дочерних элементов, если тема не изменилась.

@gaearon Являются ли кнопки элементы ? Мне не хватает контекста, как они используются.

Использование опции 2 unstable_Profiler по-прежнему вызовет обратные вызовы onRender но не вызовет фактическую логику рендеринга. Может я что не так делаю? ~ https://codesandbox.io/s/kxz4o2oyoo~ https://codesandbox.io/s/00yn9yqzjw

Я обновил пример, чтобы было понятнее.

Использование опции 2 unstable_Profiler по-прежнему будет запускать обратные вызовы onRender, но не вызывает фактическую логику рендеринга. Может я что не так делаю? https://codesandbox.io/s/kxz4o2oyoo

Именно в этом суть этого варианта. :-)

Возможно, хорошим решением для этого было бы иметь возможность «взять» контекст и повторно визуализировать компонент только в том случае, если данный обратный вызов возвращает true, например:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

Основная проблема с хуками, с которыми я столкнулся, заключается в том, что мы не можем управлять изнутри крючка тем, что возвращается компонентом - для предотвращения рендеринга, возврата мемоизированного значения и т. Д.

Если бы мы могли, это не было бы составным.

https://overrected.io/why-isnt-xa-hook/#not -a-hook-usebailout

Вариант 4. Не использовать контекст для распространения данных, а подписку на данные. Используйте useSubscription (потому что сложно написать, чтобы охватить все случаи).

Есть еще один способ избежать повторного рендеринга.
«Вам нужно переместить JSX на уровень выше из компонента повторного рендеринга, тогда он не будет создаваться повторно каждый раз»

Больше информации здесь

Возможно, хорошим решением для этого было бы иметь возможность «взять» контекст и повторно визуализировать компонент только в том случае, если данный обратный вызов возвращает true, например:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

Основная проблема с хуками, с которыми я столкнулся, заключается в том, что мы не можем управлять изнутри крючка тем, что возвращается компонентом - для предотвращения рендеринга, возврата мемоизированного значения и т. Д.

Вместо истинного / ложного здесь ... можем ли мы предоставить функцию на основе идентификации, которая позволила бы нам подмножество данных из контекста?

const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])

где useContext не появлялся бы в этом компоненте, если бы результат селектора fn не отличался по идентичности от предыдущего результата той же функции.

обнаружил, что эта библиотека может быть решением для интеграции Facebook с хуками https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/

Есть еще один способ избежать повторного рендеринга.
«Вам нужно переместить JSX на уровень выше из компонента повторного рендеринга, тогда он не будет создаваться повторно каждый раз»

Больше информации здесь

Проблема в том, что реструктуризация дерева компонентов может оказаться дорогостоящей только для предотвращения повторного рендеринга сверху вниз.

@fuleinist В конечном счете, он не сильно отличается от MobX, хотя и сильно упрощен для конкретного случая использования. MobX уже работает таким образом (также с использованием прокси), состояние изменяется, и компоненты, которые используют определенные биты состояния, повторно визуализируются, ничего больше.

@gaearon Я не знаю, упустил ли я что-то, но я попробовал ваш второй и третий варианты, и они работают некорректно. Не уверен, что это только ошибка расширения Chrome или есть другой улов. Вот мой простой пример формы, где я вижу повторную визуализацию обоих входных данных. В консоли я вижу, что мемо выполняет свою работу, но DOM все время перерисовывается. Я пробовал 1000 элементов, и событие onChange очень медленное, поэтому я думаю, что memo () неправильно работает с контекстом. Спасибо за любой совет:

Вот демонстрация с 1000 элементами / текстовыми полями. Но в этой демонстрации инструменты разработчика не отображают повторный рендеринг. Вы должны загрузить исходники на локальном компьютере, чтобы протестировать его: https://codesandbox.io/embed/zen-firefly-d5bxk

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

const FormContext = createContext();

const FormProvider = ({ initialValues, children }) => {
  const [values, setValues] = useState(initialValues);

  const value = {
    values,
    setValues
  };

  return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};

const TextField = memo(
  ({ name, value, setValues }) => {
    console.log(name);
    return (
      <input
        type="text"
        value={value}
        onChange={e => {
          e.persist();
          setValues(prev => ({
            ...prev,
            [name]: e.target.value
          }));
        }}
      />
    );
  },
  (prev, next) => prev.value === next.value
);

const Field = ({ name }) => {
  const { values, setValues } = useContext(FormContext);

  const value = values[name];

  return <TextField name={name} value={value} setValues={setValues} />;
};

const App = () => (
  <FormProvider initialValues={{ firstName: "Marr", lastName: "Keri" }}>
    First name: <Field name="firstName" />
    <br />
    Last name: <Field name="lastName" />
  </FormProvider>
);

export default App;

image

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

import React, { useState, memo } from "react";
import ReactDOM from "react-dom";

const arr = [...Array(1000).keys()];

const TextField = memo(
  ({ index, value, onChange }) => (
    <input
      type="text"
      value={value}
      onChange={e => {
        console.log(index);
        onChange(index, e.target.value);
      }}
    />
  ),
  (prev, next) => prev.value === next.value
);

const App = () => {
  const [state, setState] = useState(arr.map(x => ({ name: x })));

  const onChange = (index, value) =>
    setState(prev => {
      return prev.map((item, i) => {
        if (i === index) return { name: value };

        return item;
      });
    });

  return state.map((item, i) => (
    <div key={i}>
      <TextField index={i} value={item.name} onChange={onChange} />
    </div>
  ));
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

image

@marrkeri Я не вижу ничего плохого в первом фрагменте кода. В инструментах разработки выделяется компонент Field который использует контекст, а не TextField который является компонентом памятки и реализует функцию areEqual.

Я думаю, что проблема производительности в примере codeandbox исходит из 1000 компонентов, которые используют контекст. Выполните рефакторинг до одного компонента, который использует контекст, скажем, Fields , и верните из этого компонента (с картой) TextField для каждого значения.

@marrkeri Я не вижу ничего плохого в первом фрагменте кода. В инструментах разработки выделяется компонент Field который использует контекст, а не TextField который является компонентом памятки и реализует функцию areEqual.

Я думаю, что проблема производительности в примере codeandbox исходит из 1000 компонентов, которые используют контекст. Выполните рефакторинг до одного компонента, который использует контекст, скажем, Fields , и верните из этого компонента (с картой) TextField для каждого значения.

Как вы сказали, я думал, что следует перерисовывать каждый раз, но ( ) только при изменении значения. Но, вероятно, это просто проблемы с инструментами разработчика, потому что я добавил отступы вокруг и вместо этого перерисовывается. Проверить это изображение
image

image

Я не уловил вашего второго замечания по поводу рефакторинга одного компонента . Не могли бы вы сделать снимок, пожалуйста? А что вы думаете о максимальном отображаемом количестве какие нормально без лагов? 1000 - это много?

@marrkeri Я предлагал что-то вроде этого: https://codesandbox.io/s/little-night-p985y.

Является ли это причиной, по которой react-redux пришлось прекратить использовать преимущества стабильного API контекста и передавать текущее состояние в контекст при переходе на хуки?

Похоже, что есть

Вариант 4: передать функцию подписки в качестве значения контекста, как в эпоху устаревшего контекста

Это может быть единственный вариант, если вы не контролируете использование (необходимо для вариантов 2-3) и не можете перечислить все возможные селекторы (необходимые для варианта 1), но все же хотите предоставить API хуков.

const MyContext = createContext()
export const Provider = ({children}) => (
  <MyContext.provider value={{subscribe: listener => ..., getValue: () => ...}}>
    {children}
  </MyContext.provider>
)

export const useSelector = (selector, equalityFunction = (a, b) => a === b) => {
  const {subscribe, getValue} = useContext(MyContext)
  const [value, setValue] = useState(getValue())
  useEffect(() => subscribe(state => {
      const newValue = selector(state)
      if (!equalityFunction(newValue, value) {
        setValue(newValue)
      }
  }), [selector, equalityFunction])
}

@Hypnosphi : мы перестали передавать состояние хранилища в контексте (реализация v6) и переключились обратно на прямые подписки на хранилище (реализация v7) из-за сочетания проблем с производительностью и невозможности отказаться от обновлений, вызванных контекстом (что сделало его невозможно создать API хуков React-Redux на основе подхода v6).

Подробнее читайте в моем посте История и реализация React-Redux .

Я прочитал ветку, но мне все еще интересно - доступны ли сегодня единственные варианты условного повторного рендеринга при изменении контекста, перечисленные выше «Варианты 1 2 3 4»? Есть ли что-то еще в разработке, чтобы официально решить эту проблему, или "4 решения" считаются достаточно приемлемыми?

Я писал в другой ветке, но на всякий случай. Вот _неофициальный_ обходной путь.
Предложение useContextSelector и библиотека use-context-selector в пользовательском пространстве.

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

Вариант 3 не работает. Я что-то не так делаю? https://stackblitz.com/edit/react-w8gr8z

@ Мартин , я с этим не согласен.

Шаблон хуков можно прочитать с помощью хорошо организованной документации и кода.
структура, классы React и жизненный цикл заменяются функциональными
эквивалентные.

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

В сб, 11 января 2020 г., в 9:44 Мартин Женев [email protected]
написал:

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

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/facebook/react/issues/15156?email_source=notifications&email_token=AAI4DWUJ7WUXMHAR6F2KVXTQ5D25TA5CNFSM4G7UEEO2YY3PNVWWK3TUL52HS4DFMNVREXWG43TUL52HS4DFMVREXWWK3TUL52HS4DFVREXWWPNVWWK3TUL52HS4DFVREXWW6
или отказаться от подписки
https://github.com/notifications/unsubscribe-auth/AAI4DWUCO7ORHV5OSDCE35TQ5D25TANCNFSM4G7UEEOQ
.

@mgenev Вариант 3 предотвращает повторную визуализацию дочернего элемента ( <ExpensiveTree /> , имя говорит само за себя)

@Hypnosphi, спасибо, это было правильно.
https://stackblitz.com/edit/react-ycfyye

Я переписал, и теперь фактические компоненты визуализации (отображения) не перерисовываются, но все контейнеры (связанные с контекстом) отрисовываются при любом изменении опоры в контексте, независимо от того, используется ли она в них. Теперь единственный вариант, который я вижу, - это начать разбивать контексты, но некоторые вещи действительно глобальны и обертываются на самом высоком уровне, и изменение любого свойства в них приведет к срабатыванию всех контейнеров во всем приложении, я не Я понимаю, как это может быть полезно для производительности ...

Есть ли где-нибудь хороший пример того, как это сделать эффективно? Официальные документы действительно ограничены

Вы можете попробовать мой вариант 4, который в основном является тем, что делает react-redux внутри
https://github.com/facebook/react/issues/15156#issuecomment -546703046

Чтобы реализовать функцию subscribe , вы можете использовать что-то вроде Observables или EventEmitter, или просто написать базовую логику подписки самостоятельно:

function StateProvider({children}) {
  const [state, dispatch] = useReducer(reducer, initialState)
  const listeners = useRef([])
  const subscribe = listener => {
    listeners.current.push(listener)
  }
  useEffect(() => {
    listeners.current.forEach(listener => listener(state)
  }, [state])

  return (
    <DispatchContext.Provider value={dispatch}>
      <SubscribeContext.Provider value={{subscribe, getValue: () => state}}>
          {children}      
      </SubscribeContext.Provider>
    </DispatchContext.Provider>
  );
}

Для тех, кому может быть интересно, вот сравнение различных библиотек, имеющих отношение к этой теме.
https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode

Моя последняя попытка - проверить поддержку ветвления по состоянию с помощью useTransition что будет важно в предстоящем параллельном режиме.
В принципе, если мы используем состояние и контекст React как обычно, все в порядке. (в противном случае нам понадобится трюк, например этот .)

Спасибо @ dai-shi. Мне очень нравятся ваши пакеты, и я собираюсь их принять.

@ dai-shi привет, я только что нашел вашу react-tracked lib, и она выглядит очень хорошо, если решает проблемы с производительностью с контекстами, как обещает. Актуально еще или лучше использовать что-нибудь другое? Здесь я вижу хороший пример его использования, а также демонстрирует, как создать уровень промежуточного программного обеспечения с помощью use-reducer-async https://github.com/dai-shi/react-tracked/blob/master/examples/12_async/src/store .ts Спасибо за это. В настоящее время я сделал нечто подобное, используя useReducer , обернув dispatch своим собственным для асинхронного промежуточного программного обеспечения и используя Context но беспокоясь о будущих проблемах с производительностью рендеринга из-за упаковки контекстов.

Я бы сказал, что @bobrosoft react-tracked довольно стабильна (как продукт для разработчиков). Отзывы очень приветствуются, и именно так можно улучшить библиотеку. В настоящее время он внутренне использует недокументированную функцию React, и я надеюсь, что в будущем мы сможем заменить ее более совершенным примитивом. use-reducer-async почти как простой синтаксический сахар, который никогда не ошибется.

Этот HoC работал у меня:
`` ''
импортировать React, {useMemo, ReactElement, FC} из response;
сокращение импорта из lodash / reduce;

type Selector = (context: any) => any;

interface SelectorObject {
}

const withContext = (
Компонент: FC,
Контекст: любой,
селекторы: SelectorObject,
): FC => {
return (реквизит: любой): ReactElement => {
const Consumer = ({context}: любой): ReactElement => {
const contextProps = уменьшить (
селекторы,
(acc: any, selector: Selector, key: string): any => {
значение константы = селектор (контекст);
acc [ключ] = значение;
возврат в соотв.
},
{},
);
вернуть useMemo (
(): ReactElement => ,
[... Object.values ​​(реквизиты), ... Object.values ​​(contextProps)],
);
};
возвращение (

{(контекст: любой): ReactElement => }

);
};
};

экспорт по умолчанию withContext;
`` ''

пример использования:

export default withContext(Component, Context, { value: (context): any => context.inputs.foo.value, status: (context): any => context.inputs.foo.status, });

это можно рассматривать как контекстный эквивалент redux mapStateToProps

Я сделал hoc почти очень похожим на connect () в redux

const withContext = (
  context = createContext(),
  mapState,
  mapDispatchers
) => WrapperComponent => {
  function EnhancedComponent(props) {
    const targetContext = useContext(context);
    const { ...statePointers } = mapState(targetContext);
    const { ...dispatchPointers } = mapDispatchers(targetContext);
    return useMemo(
      () => (
        <WrapperComponent {...props} {...statePointers} {...dispatchPointers} />
      ),
      [
        ...Object.values(statePointers),
        ...Object.values(props),
        ...Object.values(dispatchPointers)
      ]
    );
  }
  return EnhancedComponent;
};

Выполнение :

const mapActions = state => {
  return {};
};

const mapState = state => {
  return {
    theme: (state && state.theme) || ""
  };
};
export default connectContext(ThemeContext, mapState, mapActions)(Button);


Обновление: в конечном итоге я переключился на EventEmitter для быстрого изменения детализированных данных с динамическими слушателями (при движении мыши). Я понял, что это лучший инструмент для работы. Контекст отлично подходит для общего обмена данными, но не с высокой частотой обновления.

Разве это не сводится к декларативной подписке или не подписке на контекст? Условная упаковка компонента в другой, использующий useContext ().

Основное требование - внутренний компонент не может иметь состояния, поскольку он фактически будет другим экземпляром из-за ветвления. Или, может быть, вы можете предварительно отрендерить компонент, а затем использовать cloneElement для обновления свойств.

Некоторые компоненты не имеют ничего общего с контекстом в рендере, а должны «просто читать значение» из него. Что мне не хватает?
context

Безопасно ли

Я пытаюсь подписать компоненты на контексты, рендеринг которых дешевле, насколько это возможно. Но затем я обнаружил, что передаю реквизиты, как по-старому, с кодом sphagetti или с помощью memo, чтобы не подписываться на обновления, когда с обновлениями нет ничего логичного. Решения Memo хороши, когда рендеринг вашего компонента зависит от контекстов, но в остальном ...

@ vamshi9666 вы

Я нашел https://recoiljs.org/ хорошим решением этой проблемы. Я думаю, было бы здорово, если бы вы интегрировали его в React.

@ vamshi9666 вы

Я использовал его только в одном месте, и моя первоначальная цель - изолировать управление состоянием от jsx, но также не создавать не шаблонный. поэтому, когда я расширил функциональность мутаций, это выглядело очень похоже на шаблон редуктора и создателя действий redux. Что даже лучше. но я не вижу смысла изобретать что-то заново, когда оно уже есть.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги