【问题标题】:React: When Does Context Provider Update its Consumers?React:上下文提供者何时更新其消费者?
【发布时间】:2019-03-28 16:54:21
【问题描述】:

据我了解,React 的 Context Provider 会在 context 值 更改时更新其 Consumers

From the Docs:

作为 Provider 后代的所有消费者都将重新渲染 每当 Provider 的 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:通过查看Object.is 控制台日志,您可以看到单击Change OtherState上下文值 没有改变。

问题

为什么ThemedButton 重新渲染,而上下文值 没有改变?

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    useState 每次调用都会返回一个新数组。因此,每次渲染都会将一个新数组传递给上下文。 useMemo 解决问题。

    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"/>

    【讨论】:

    • 谢谢,非常有帮助。这是确保消费者仅在更改的上下文上更新的最常见/典型的方式(假设我们希望让消费者能够自己更新上下文)?
    • 快速跟进:useMemo(第一个参数)中的函数是否仅在依赖关系发生变化时运行?我对文档感到困惑,其中说:“记住传递给 useMemo 的函数在渲染期间运行” - 来源:reactjs.org/docs/hooks-reference.html#usememo
    • 这取决于传递给Context.Provider 的值。对于themesetTheme 有两个单独的上下文,您不需要使用useMemo,因为setTheme 保证每次渲染都具有相同的功能。传递给 useMemo 的回调不会运行每个渲染。但是当它发生时,它会在渲染过程中发生。
    • 谢谢,这澄清了。顺便说一句,我想我想出了一个更好的方法来做到这一点。基本上,我只是将状态更新功能放入状态本身。因为,状态对象在重新渲染时不会改变。这样,我就不需要 useMemo。在此处查看我的示例代码:codesandbox.io/s/7jv99l7okj
    猜你喜欢
    • 1970-01-01
    • 2022-08-08
    • 1970-01-01
    • 2018-11-03
    • 1970-01-01
    • 2021-03-17
    • 2020-03-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多