Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How should enzyme support the createContext() API #1973

Closed
minznerjosh opened this issue Jan 8, 2019 · 11 comments
Closed

How should enzyme support the createContext() API #1973

minznerjosh opened this issue Jan 8, 2019 · 11 comments
Projects

Comments

@minznerjosh
Copy link
Contributor

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!

@ryanirilli
Copy link

I am experiencing an issue with this as well as it relates to snapshot testing with Redux connected children. The below mock works well

const mockContext = jest.fn(/* mock values returned here */);
jest.mock('some-module', () => ({
    FooConsumer: ({ children }) => children(mockContext())
}));

and snapshots look great for general React components

shallow(<Test />).dive();

However when the child is a Redux connected component, I get this for a snapshot

<Connect(FooComponent)
  /* props */
>
  <FooConsumer>
    <Component />
  </FooConsumer>
</Connect(FooComponent)>

switching to using mount in this case and creating a mock store causes jest or enzyme to hang indefinitely.

@buddyp450
Copy link

Just as an aside, react-redux has already made their change to the new context api as of v6.0.0 which released just last month so consider me as the first of a flood of people that will be running into this issue shortly. :)

Downgrading to react-redux v5.1.1 in the meantime.

@ljharb
Copy link
Member

ljharb commented Jan 23, 2019

Understood; there's already a bit of discussion on reduxjs/react-redux#1161 about it.

@ljharb ljharb added this to Needs Triage in React 16 via automation Jan 23, 2019
@ljharb ljharb moved this from Needs Triage to Context in React 16 Jan 23, 2019
@csvan
Copy link

csvan commented Feb 12, 2019

It's perhaps worth noting that version 4 of styled-components is notoriously difficult to test using Enzyme due to the Context API. See e.g.

styled-components/jest-styled-components#191

@ljharb
Copy link
Member

ljharb commented Apr 5, 2019

Closed by #1960 and #1966.

@ljharb ljharb closed this as completed Apr 5, 2019
@vKongv

This comment has been minimized.

@ljharb

This comment has been minimized.

@ryanirilli

This comment has been minimized.

@ljharb

This comment has been minimized.

@heath-freenome
Copy link

Is there documentation on how to use the features provided by #1960 and #1966?

@ljharb
Copy link
Member

ljharb commented Jun 21, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
React 16
  
Context
Development

No branches or pull requests

7 participants