Skip to content

How should enzyme support the createContext() API #1973

@minznerjosh

Description

@minznerjosh

It's been quite some time since the createContext() API shipped in react, and I'd love to help out in implementing "full support" for that API in enzyme ASAP. (Before the libraries many of us depend on [react-router, react-redux, etc] switch from using the deprecated, legacy context API to the new, fully-supported API.)

However, based on my reading of many createContext()-related issues in this repo, I don't have a clear idea of what "full support" for createContext() in enzyme looks like. So I've created this issue so we can hammer it out.

Assumptions

I'm walking into this discussion with the following assumptions. Please challenge them if they don't make sense!

  1. The API should work the same in both shallow() and mount().
  2. The API should let you provide the context of multiple providers, because a single component could read from multiple consumers. (This is how enzyme's legacy context API works.)
  3. The API should be able to support createContext() and the legacy context API simultaneously, just like react.

Enzyme's Current (Legacy) Context APIs

I've seen some issues (#1913, #1840) that imply that options.context should somehow work with the createContext() API in the future. However, as per my assumptions, I don't understand how this API could support createContext(). For example, how would this work?

const ContextA = createContext();
const ContextB = createContext();
function MyComponent() {
  return (
    <ContextA.Consumer>
      {a => (
        <ContextB.Consumer>
          {b => (
            <div>
              A: {a}, B: {b}
            </div>
          )}
        </ContextB.Consumer>
      )}
    </ContextA.Consumer>
  );
}

const wrapper = mount(<MyComponent />, {
  context: "hello!" // which context are we providing here?
});
wrapper.setContext("foo"); // same here!

The only API I could imagine still making sense for createContext() is wrapper.context(). But even then, it would only be for class components that are using the contextType feature added in react@16.6, as that's the only part of the API that sets this.context in a component.

An Idea for Moving Forward

The current context APIs

I think we should move forward with the idea that the existing context APIs (options.context, .setContext(), perhaps .context()) are only for legacy context, and will never have anything to do with the createContext() API. We should update the docs to reflect this immediately.

The createContext() API

I'm of the opinion that enzyme shouldn't actually add any new APIs for createContext(). createContext() is all about just rendering components! It doesn't involve component methods like getChildContext() or static properties like contextTypes and childContextTypes. If enzyme can handle rendering createContext()'s <Consumer /> and <Provider />, it doesn't need to do anything else!

For clarification, here's how one would translate the use of the legacy context APIs using createContext()!

  • options.context
    • Legacy Context
      class ConsumerA extends React.Component {
        render() {
          return <div>A is: {this.context.foo}</div>;
        }
      }
      ConsumerA.childContextTypes = {
        foo: PropTypes.string,
      };
      class ConsumerB extends React.Component {
        render() {
          return <div>B is: {this.context.bar}</div>;
        }
      }
      ConsumerB.childContextTypes = {
        bar: PropTypes.string,
      };
      class MyComponent extends Component {
        render() {
          return (
            <div>
              <ConsumerA />
              <ConsumerB />
            </div>
          );
        }
      }
      
      shallow(<MyComponent />, {
        context: { foo: 'hello', bar: 'world' },
      });
      mount(<MyComponent />, {
        context: { foo: 'hello', bar: 'world' },
        childContextTypes: { foo: PropTypes.string, bar: PropTypes.string },
      });
    • createContext()
      const A = createContext();
      const B = createContext();
      class MyComponent extends Component {
        render() {
          return (
            <div>
              <A.Consumer>
                {value => <div>A is {value}</div>}
              </A.Consumer>
              <B.Consumer>
                {value => <div>B is {value}</div>}
              </B.Consumer>
            </div>
          );
        }
      }
      
      shallow(
        <A.Provider value="hello">
          <B.Provider value="world">
            <MyComponent />
          </B.Provider>
        </A.Provider>
      ).dive().dive(); // dive() through to <MyComponent />
      mount(
        <A.Provider value="hello">
          <B.Provider value="world">
            <MyComponent />
          </B.Provider>
        </A.Provider>
      )
  • setContext()
    • Legacy Context

      class MyComponent extends React.Component {
        render() {
          return <div>Context is: {this.context.someContext}</div>;
        }
      }
      MyComponent.childContextTypes = { someContext: PropTypes.string };
      
      const sWrapper = shallow(<MyComponent />, { context: { someContext: 'foo' } });
      sWrapper.setContext({ someContext: 'bar' });
      
      const mWrapper = mount(<MyComponent />, { context: { someContext: 'foo' } });
      mWrapper.setContext({ someContext: 'bar' });
    • createContext()

      const Context = createContext();
      class MyComponent extends React.Component {
        render() {
          return (
            <Context.Consumer>
              {value => <div>Context is: {value}</div>}
            </Context.Consumer>
          )
        }
      }
      
      const sProvider = shallow(
        <Context.Provider value="foo">
          <MyComponent />
        </Context.Provider>
      );
      let sWrapper = sProvider.dive();
      sWrapper = sProvider.setProps({ value: 'bar' }).dive();
      
      const mWrapper = mount(
        <Context.Provider value="foo">
          <MyComponent />
        </Context.Provider>
      );
      mWrapper.setProps({ value: 'bar' });

The only problem I see with this approach is that, if you must wrap your component in <Provider />s to get your context, your component will never be the root, and it is therefore not possible to call .setProps() on it. To address this, I'm working on #1960.

Thanks for reading! Looking forward to discussing!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions