React: когда поставщик контекста обновляет своих потребителей?

#javascript #reactjs #react-hooks

#javascript #reactjs #реагирующие хуки

Вопрос:

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

Из документов:

Все потребители, которые являются потомками поставщика, будут повторно отображаться при каждом изменении value реквизита поставщика. Распространение от поставщика к его потребителям-потомкам не зависит от shouldComponentUpdate метода, поэтому потребитель обновляется, даже когда компонент-предок завершает обновление.

Изменения определяются путем сравнения нового и старого значений с использованием того же алгоритма, что и Object.is.

Однако следующий код, похоже, указывает на обратное:

 var themes = {
  light: {
    name: "Light",
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    name: "Dark",
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext({
  theme: themes.light,
  updateTheme: () => {}
});

let prevTheme = undefined;

function App() {

  console.log("RE-RENDERING App...");
  const stateArray = React.useState(themes.light);

  const [theme, setTheme] = stateArray;

  const [otherState, setOtherState] = React.useState(true);

  function handleSetOtherState() {
    console.log("SETTING OTHER STATE.....");
    setOtherState(prevState => !prevState);
  }

  console.log("theme:", theme);
  console.log("prevTheme:", prevTheme);
  console.log(`Object.is(prevTheme, theme): ${Object.is(prevTheme, theme)}`);
  prevTheme = theme;
  return (
    <ThemeContext.Provider value={stateArray}>
      <Toolbar />
      <button onClick={handleSetOtherState}>Change OtherState</button>
    </ThemeContext.Provider>
  );
}

class Toolbar extends React.PureComponent {
  render() {
    console.log("RE-RENDERING Toolbar (DOES NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
    return (
      <div>
        <ThemedButton />
      </div>
    );
  }
}

function ThemedButton() {

  console.log("RE-RENDERING ThemedButton (SHOULD NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
  const themeContext = React.useContext(ThemeContext);
  const [theme, setTheme] = themeContext;
  console.log("themeContext:", themeContext);
  console.log("theme.name:", theme.name);
  console.log("setTheme:", setTheme);

  function handleToggleTheme() {
    console.log("SETTING THEME STATE.....");
    setTheme(
      prevState =>
        themes.dark
    );
  }

  return <button onClick={handleToggleTheme}>Click me: {theme.name}</button>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);  
 <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"/>  

Как видно, при нажатии Change OtherState :

  1. Родительский компонент, заключающий в себе контекст, Provider выполнит повторный рендеринг, тем самым позволяя Provider увидеть, что в значении контекста действительно не было никаких изменений
  2. Дочерние элементы будут повторно отображаться, потому что это сделал их родитель, но этот процесс останавливается на полпути, Toolbox будучи PureComponent
  3. Итак, вся идея контекста Provider заключается в том, что он должен обновлять его только Consumers в том случае, если изменилось значение в контекстном
  4. Проверка изменений выполняется с помощью Object.is , как указано в документах (см. Выше)
  5. Независимо от этого факта, Consumer ( ThemedButton ) все еще обновляется при OtherState изменениях
  6. Этого не должно произойти, потому что значение контекста фактически не изменилось, и повторный рендеринг дочернего элемента останавливается посередине с помощью PureComponent
  7. Обновление должно происходить только при изменении значения контекста Consumers , даже если повторный рендеринг остановлен в промежуточных компонентах с PureComponent

PS: Вы можете видеть, что значение контекста не изменяется при Change OtherState нажатии, просмотрев Object.is журнал консоли.

Вопрос

Почему выполняется ThemedButton повторный рендеринг, когда значение контекста не изменилось?

Ответ №1:

useState возвращает новый массив при каждом вызове. Таким образом, вы передаете новый массив в контекст при каждом рендеринге. Используйте MEMO для устранения проблемы.

 var themes = {
  light: {
    name: "Light",
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    name: "Dark",
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext({
  theme: themes.light,
  updateTheme: () => {}
});

let prevTheme = undefined;
let prevStateArray = undefined;

function App() {

  console.log("RE-RENDERING App...");
  const stateArray = React.useState(themes.light);
  console.log('stateArray', prevStateArray, stateArray, Object.is(prevStateArray, stateArray));
  prevStateArray = stateArray;

  const [theme, setTheme] = stateArray;
  const memoState = React.useMemo(() => [theme, setTheme], [theme, setTheme]);

  const [otherState, setOtherState] = React.useState(true);

  function handleSetOtherState() {
    console.log("SETTING OTHER STATE.....");
    setOtherState(prevState => !prevState);
  }

  console.log("theme:", theme);
  console.log("prevTheme:", prevTheme);
  console.log(`Object.is(prevTheme, theme): ${Object.is(prevTheme, theme)}`);
  prevTheme = theme;
  return (
    <ThemeContext.Provider value={memoState}>
      <Toolbar />
      <button onClick={handleSetOtherState}>Change OtherState</button>
    </ThemeContext.Provider>
  );
}

class Toolbar extends React.PureComponent {
  render() {
    console.log("RE-RENDERING Toolbar (DOES NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
    return (
      <div>
        <ThemedButton />
      </div>
    );
  }
}

function ThemedButton() {

  console.log("RE-RENDERING ThemedButton (SHOULD NOT HAPPEN WHEN CHANGING OTHERSTATE)...");
  const themeContext = React.useContext(ThemeContext);
  const [theme, setTheme] = themeContext;
  console.log("themeContext:", themeContext);
  console.log("theme.name:", theme.name);
  console.log("setTheme:", setTheme);

  function handleToggleTheme() {
    console.log("SETTING THEME STATE.....");
    setTheme(
      prevState =>
        themes.dark
    );
  }

  return <button onClick={handleToggleTheme}>Click me: {theme.name}</button>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);  
 <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"/>  

Комментарии:

1. Спасибо, очень полезно. Является ли это наиболее распространенным / типичным способом гарантировать, что потребители обновляют только измененный контекст (предполагая, что мы хотим предоставить потребителям возможность самим обновлять контекст)?

2. Краткое продолжение: выполняется ли функция внутри useMemo (первый аргумент) только при изменении зависимостей? Меня смутили документы, в которых говорится: «Помните, что функция, переданная useMemo, выполняется во время рендеринга» — Источник: reactjs.org/docs/hooks-reference.html#usememo .

3. Это зависит от значения, переданного Context.Provider . Вам не нужно использовать два отдельных контекста для theme и setTheme , useMemo потому что setTheme гарантируется, что при каждом рендеринге будет одна и та же функция. Обратный вызов, переданный useMemo , не запускает каждый рендеринг. Но когда это происходит, это происходит во время рендеринга.

4. Спасибо, это проясняет ситуацию. Кстати, я думаю, что придумал лучший способ сделать это. По сути, я просто помещаю свою функцию обновления состояния внутри самого состояния. Потому что объект состояния не изменится при повторном рендеринге. Таким образом, мне не нужен useMemo. Смотрите мой пример кода здесь: codesandbox.io/s/7jv99l7okj