-
-
Notifications
You must be signed in to change notification settings - Fork 2k
How should enzyme support the createContext() API #1973
Description
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!
- The API should work the same in both
shallow()andmount(). - 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.)
- 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> )
- Legacy Context
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!