Skip to content

New Context Provider may block Old context propagation if children are constant #12551

Closed
@Jessidhia

Description

@Jessidhia

Do you want to request a feature or report a bug?

Bug

What is the current behavior?

It seems that, if the children of a new-style React.createContext() context Provider are constant, the Provider can block updates from old-style this.context context providers from propagating to this.context consumers.

This sandbox demonstrates the issue. Clicking the button with a number will correctly increment the Root's state and context, but the update is only propagated to the Child3's context (and its button) when the "Colors!" button is clicked, as it causes an update to the value of the new-style Provider:

https://codesandbox.io/s/ol4lpokpjy

Copy of the source code in the sandbox
import PropTypes from "prop-types";
import React from "react";
import ReactDOM from "react-dom";

class Root extends React.Component {
  constructor(props: {}) {
    super(props);
    this.state = {
      count: 0
    };
    this.countUp = this.countUp.bind(this);
  }

  getChildContext() {
    return {
      ...this.context,
      count: this.state.count,
      countUp: this.countUp
    };
  }

  render() {
    return this.props.children;
  }

  countUp() {
    this.setState(({ count }) => ({ count: count + 1 }));
  }
}

Root.childContextTypes = {
  count: PropTypes.number.isRequired,
  countUp: PropTypes.func.isRequired
};

const ctx = React.createContext();

class Child1 extends React.Component {
  constructor(props: { onClick(): void }) {
    super(props);
    this.state = {
      color: randomHexColor(),
      newColor: this.newColor.bind(this)
    };
  }

  render() {
    return (
      <ctx.Provider value={this.state}>{this.props.children}</ctx.Provider>
    );
  }

  newColor() {
    const color = randomHexColor();
    this.setState(() => ({ color }));
  }
}

function randomHexColor() {
  const colorStr = Math.floor(Math.random() * (Math.pow(2, 24) - 1)).toString(
    16
  );
  return "#000000".slice(0, -colorStr.length) + colorStr;
}

class Child2 extends React.Component {
  render() {
    return (
      <ctx.Consumer>
        {ctx => (
          <React.Fragment>
            <Child3 color={ctx.color} />
            <button onClick={ctx.newColor}>Colors!</button>
          </React.Fragment>
        )}
      </ctx.Consumer>
    );
  }
}

class Child3 extends React.Component {
  render() {
    return (
      <button
        style={{ color: this.props.color }}
        onClick={this.context.countUp}
      >
        {this.context.count}
      </button>
    );
  }
}

Child3.contextTypes = {
  count: PropTypes.number.isRequired,
  countUp: PropTypes.func.isRequired
};

ReactDOM.render(
  <Root>
    <Child1>
      <Child2 />
    </Child1>
  </Root>,
  document.getElementById("root")
);

What is the expected behavior?

Both old-style and new-style context updates should coexist.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

16.3.1; also broken in 16.3.0.


This seems to only happen if the children of the Provider are constant, which is what happens when the children are provided on the first and only ReactDOM.render call. If Child1 is updated to directly use <Child2/> instead of {this.props.children}, the problem does not happen.

This can also be a problem when using a production optimization that hoists constant elements outside the Component if the specified children are constant, which would even defeat the fix/workaround for the example above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions