新的React Context API会触发重新渲染吗?

102

我一直在努力理解新的React Context API并进行尝试。我只是想检查一个简单的情况 - 当提供程序中的数据更新时,哪些内容会重新渲染。

请查看此处的Codesandbox上面的小例子

所以,在我的例子中,我有一个App组件 - 它的状态类似于这样 -

this.state = {
  number - A random number
  text - A static text
} 

我从这里创建了一个新的 React Context,其中包含来自状态的 numbertext,并将值传递给两个 Consumers NumberText

因此,我的假设是,如果随机数更新,它将更改上下文,并且两个组件都应该触发重新渲染。

但实际情况是,值正在更新,但没有重新渲染。

所以,我的问题是 -

  1. 上下文的更新是否不通过通常的重新渲染进行传播?因为当上下文更改时我无法看到我的日志/颜色更改。

  2. 所有订阅该 Provider 的 Consumers 是否都已更新?

2个回答

82
更新上下文值不会触发提供程序(provider)的所有子组件重新渲染,只有从Consumer内部呈现的组件才会重新渲染。因此,在您的情况下,尽管数字(number)组件包含了Consumer,但Number组件不会重新呈现,而只是在Consumer中的渲染函数中进行更改,因此当上下文更新时值就会更改。这种方式非常高效,因为它不会触发其所有子元素的重新渲染。

所有使用该提供程序(provider)的消费者将经历一个更新周期,但它们是否重新渲染由React虚拟DOM比较决定。可以在此sandbox中的控制台中查看此演示。

编辑

需要确保组件被呈现为ContextProvider组件的子元素,并向其传递处理程序,而不是内联呈现并更新ContextProvider的状态,因为这将触发ContextProvider内所有组件的重新呈现。

高性能用法

App.js

  constructor() {
    super();
    this.state = {
      number: Math.random() * 100,
      text: "testing context api"
      updateNumber: this.updateNumber,
    };
  }
  render() {
    return (
      <AppContext.Provider
        value={this.state}
      >
        {this.props.children}
      </AppContext.Provider>
    );
  }

index.js

class Data extends React.Component {
  render() {
    return (
      <div>
        <h1>Welcome to React</h1>
        <Number />
        <Text />
        <TestComp />
        <AppContext.Consumer>
          {({ updateNumber }) => (
            <button onClick={updateNumber}>Change Number </button>
          )}
        </AppContext.Consumer>
      </div>
    );
  }
}

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

性能较差的使用方式

App.js

class App extends Component {
  constructor() {
    super();
    this.state = {
      number: Math.random() * 100,
      text: "testing context api"
    };
  }

  updateNumber = () => {
    const randomNumber = Math.random() * 100;
    this.setState({ number: randomNumber });
  };

  render() {
    return (
      <AppContext.Provider value={this.state}>
        <div>
          <h1>Welcome to React</h1>
          <Number />
          <Text />
          <TestComp />
          <button onClick={this.updateNumber}>Change Number </button>
        </div>
      </AppContext.Provider>
    );
  }
}

2
为什么在内联更新状态会触发重新渲染所有子组件,而分离则不会? - TomSawyer
2
@TomSawyer,这是因为当您将它们呈现为内联时,它们位于您的Provider组件的层次结构中,并且会在提供程序状态更改时重新呈现。 - Shubham Khatri
6
useContext是什么? - Reza Sam
@ShubhamKhatri:尽管Provider的所有子组件在value更改时不会全部重新渲染,但我相信消费者组件的所有子组件仍将重新渲染? - darKnight
所有消费者都将通过更新周期与该提供程序进行通信,但是否重新渲染由React虚拟DOM比较决定。这是错误的。在React术语中,此更新周期称为“渲染”。 - Ben Wainwright
显示剩余2条评论

59
这是关于 useContext Hook 的更新:

const value = useContext(MyContext)

当组件上方最近的 <MyContext.Provider> 更新时,此 Hook 将使用传递给该 MyContext 提供程序的最新上下文 value 触发重新渲染。即使祖先使用了 React.memoshouldComponentUpdate,重新渲染也将从使用 useContext 的组件本身开始。

调用 useContext组件将在 上下文值更改始终重新渲染。如果重新渲染组件代价很高,可以通过使用记忆化进行优化。

因此,在以下代码示例中,组件 NumberText 将在每次上下文值更改时重新渲染,因为两者都直接包含 useContext(AppContext)

const AppContext = React.createContext();

const Number = React.memo(props => {
  const renderCount = useRenderCount();
  const contextNo = React.useContext(AppContext);
  return (
    <div style={{ backgroundColor: `${randomColor()}` }}>
      Number: rendered {renderCount.current} times.
    </div>
  );
});

const Text = React.memo(() => {
  const renderCount = useRenderCount();
  const context = React.useContext(AppContext);
  return (
    <div style={{ backgroundColor: `${randomColor()}` }}>
      Text: rendered {renderCount.current} times. I rerender with context value
      changes!
    </div>
  );
});

const App = () => {
  const [ctxVal, setCtxVal] = React.useState(0);
  const [prop, setProp] = React.useState(0);
  return (
    <AppContext.Provider value={ctxVal}>
      <Number prop={prop} />
      <Text />
      <button onClick={() => setCtxVal(ctxVal + 1)}>
        Change context value
      </button>
      <button onClick={() => setProp(prop + 1)}>
        Only change prop in Number
      </button>
    </AppContext.Provider>
  );
};

function useRenderCount() {
  const renderCount = React.useRef(1);
  React.useEffect(() => {
    renderCount.current += 1;
  }); 
  return renderCount;
}

function randomColor() {
  const letters = "0123456789ABCDEF"; let color = "#";
  for (let i = 0; i < 6; i++) color += letters[Math.floor(Math.random() * 16)];
  return color;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>


1
在 @ford04 的示例基础上,使用对象作为 ctxVal 的扩展在这里: codesandbox。感谢作者!我需要快速展示对象变更检测的示例,并且能够快速使用您的示例作为基础。 - John Lee
你提到它不在意父组件是否具有should component或React.memo,但在我的情况下,我使用useContext记忆了父组件,并且当contextapi中的值更改时,它不会重新渲染.. - Bryan Lumbantobing
3
@BryanLumbantobing 当值发生变化时,使用useContext的组件会触发重新渲染。如果您的子组件被React.memo包装,并且其props没有改变,则该子组件不会重新渲染,尽管父级上下文已更改。 - ford04
如果context const只被声明而没有在返回方法中使用,那么Text组件如何基于context更新其状态?在组件内部声明上下文是否足以触发重新渲染?难道不应该在返回方法中使用context.someVariable,以便在状态更新后将其更改传播到这些Text组件中吗? - Fed
1
@FedericoCapaldo 是的,只要一个组件包含 useContext 就足以在上下文值更改时重新渲染。 - ford04

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接