Reputation: 1655
All the examples I've seen of the new Context API in React are in a single file, e.g. https://github.com/wesbos/React-Context.
When I try to get it working across multiple files, I'm clearly missing something.
I'm hoping to make a GlobalConfiguration
component (the MyProvider
below) create and manage the values in the context, ready for any child component (MyConsumer
below) read from it.
render() {
return (
<MyProvider>
<MyConsumer />
</MyProvider>
);
}
import React, { Component } from 'react';
const MyContext = React.createContext('test');
export default class MyProvider extends Component {
render() {
return (
<MyContext.Provider
value={{ somevalue: 1 }}>
{this.props.children}
</MyContext.Provider >
);
}
}
import React, { Component } from 'react';
const MyContext = React.createContext('test');
export default class MyConsumer extends Component {
render() {
return (
<MyContext.Consumer>
{(context) => (
<div>{context.state.somevalue}</div>
)}
</MyContext.Consumer>
);
}
}
Unfortunately that fails with this in the console:
consumer.js:12 Uncaught TypeError: Cannot read property 'somevalue' of undefined
Have I completely missed the point? Is there documentation or an example of how this works across multiple files?
Upvotes: 46
Views: 27084
Reputation: 9708
I'm gonna throw my solution into the pot - it was inspired by @Striped and simply just renames the exports into something that makes sense in my head.
import React, { Component } from 'react'
import Blockchain from './cloudComputing/Blockchain'
const { Provider, Consumer: ContextConsumer } = React.createContext()
class ContextProvider extends Component {
constructor(props) {
super(props)
this.state = {
blockchain: new Blockchain(),
}
}
render() {
return (
<Provider value={this.state}>
{this.props.children}
</Provider>
)
}
}
module.exports = { ContextConsumer, ContextProvider }
Now it's easy to implement a ContextConsumer
into any component
...
import { ContextConsumer } from '../Context'
...
export default class MyComponent extends PureComponent {
...
render() {
return (
<ContextConsumer>
{context => {
return (
<ScrollView style={blockStyle.scrollView}>
{map(context.blockchain.chain, block => (
<BlockCard data={block} />
))}
</ScrollView>
)
}}
</ContextConsumer>
)
}
I'm SO done with redux!
Upvotes: 4
Reputation: 1173
I think the problem that you are running into is that you are creating two different contexts, and trying to use them as one. It is the Context
created by React.createContext
that links Provider
and Consumer
.
Make a single file (I'll call it configContext.js
)
configContext.js
import React, { Component, createContext } from "react";
// Provider and Consumer are connected through their "parent" context
const { Provider, Consumer } = createContext();
// Provider will be exported wrapped in ConfigProvider component.
class ConfigProvider extends Component {
state = {
userLoggedIn: false, // Mock login
profile: { // Mock user data
username: "Morgan",
image: "https://morganfillman.space/200/200",
bio: "I'm Mogran—so... yeah."
},
toggleLogin: () => {
const setTo = !this.state.userLoggedIn;
this.setState({ userLoggedIn: setTo });
}
};
render() {
return (
<Provider
value={{
userLoggedIn: this.state.userLoggedIn,
profile: this.state.profile,
toggleLogin: this.state.toggleLogin
}}
>
{this.props.children}
</Provider>
);
}
}
export { ConfigProvider };
// I make this default since it will probably be exported most often.
export default Consumer;
index.js
...
// We only import the ConfigProvider, not the Context, Provider, or Consumer.
import { ConfigProvider } from "./configContext";
import Header from "./Header";
import Profile from "./Profile";
import "./styles.css";
function App() {
return (
<div className="App">
<ConfigProvider>
<Header />
<main>
<Profile />
</main>
<footer>...</footer>
</ConfigProvider>
</div>
);
}
...
Header.js
import React from 'react'
import LoginBtn from './LoginBtn'
... // a couple of styles
const Header = props => {
return (
... // Opening tag, etc.
<LoginBtn /> // LoginBtn has access to Context data, see file.
... // etc.
export default Header
LoginBtn.js
import React from "react";
import Consumer from "./configContext";
const LoginBtn = props => {
return (
<Consumer>
{ctx => {
return (
<button className="login-btn" onClick={() => ctx.toggleLogin()}>
{ctx.userLoggedIn ? "Logout" : "Login"}
</button>
);
}}
</Consumer>
);
};
export default LoginBtn;
Profile.js
import React, { Fragment } from "react";
import Consumer from "./configContext"; // Always from that same file.
const UserProfile = props => {...}; // Dumb component
const Welcome = props => {...}; // Dumb component
const Profile = props => {
return (
<Consumer>
...
{ctx.userLoggedIn ? (
<UserProfile profile={ctx.profile} />
) : (<Welcome />)}
...
</Consumer>
...
Upvotes: 45
Reputation: 2223
My current method of solving the same problem is to use the Unstated library, which as a convenient wrapper around the React Context API. "Unstated" also provides dependency injection allow the creating of discrete instances of a container; which is handy for code reuse and testing.
The following skeleton API Service holds state properties such as loggedIn
, as well as two service methods: login()
and logout()
. These props and methods are now available throughout the app with a single import in each file that needs the context.
For example:
import React from "react";
// Import helpers from Unstated
import { Provider, Subscribe, Container } from "unstated";
// APIContainer holds shared/global state and methods
class APIContainer extends Container {
constructor() {
super();
// Shared props
this.state = {
loggedIn: false
};
}
// Shared login method
async login() {
console.log("Logging in");
this.setState({ loggedIn: true });
}
// Shared logout method
async logout() {
console.log("Logging out");
this.setState({ loggedIn: false });
}
}
// Instantiate the API Container
const instance = new APIContainer();
// Wrap the Provider
const ApiProvider = props => {
return <Provider inject={[instance]}>{props.children}</Provider>;
};
// Wrap the Subscriber
const ApiSubscribe = props => {
return <Subscribe to={[instance]}>{props.children}</Subscribe>;
};
// Export wrapped Provider and Subscriber
export default {
Provider: ApiProvider,
Subscribe: ApiSubscribe
}
Now the Api.js
module can be used as global provide in App.js
:
import React from "React";
import { render } from "react-dom";
import Routes from "./Routes";
import Api from "./Api";
class App extends React.Component {
render() {
return (
<div>
<Api.Provider>
<Routes />
</Api.Provider>
</div>
);
}
}
render(<App />, document.getElementById("root"));
Finally, Api.js
can subscribe to the state of the API from deep within the React tree.
import React from "react";
import Api from "../Api";
const Home = () => {
return (
<Api.Subscribe>
{api => (
<div>
<h1>🏠 Home</h1>
<pre>
api.state.loggedIn = {api.state.loggedIn ? "👍 true" : "👎 false"}
</pre>
<button onClick={() => api.login()}>Login</button>
<button onClick={() => api.logout()}>Logout</button>
</div>
)}
</Api.Subscribe>
);
};
export default Home;
Try the CodeSandbox demo here: https://codesandbox.io/s/wqpr1o6w15
Hope that helps!
PS: Someone bash me on the head quick if I'm doing this the wrong way. I'd love to learn different/better approaches. - Thanks!
Upvotes: 1
Reputation: 2647
As of right now, the two context you created in the files are not the same even thought the name is the same. You need to export the context that you created in one of the files, and use that through out.
so something like this, in your provider.js file:
import React, { Component } from 'react';
const MyContext = React.createContext();
export const MyContext;
export default class MyProvider extends Component {
render() {
return (
<MyContext.Provider
value={{ somevalue: 1 }}>
{this.props.children}
</MyContext.Provider >
);
}
}
then in your consumer.js file
import MyContext from 'provider.js';
import React, { Component } from 'react';
export default class MyConsumer extends Component {
render() {
return (
<MyContext.Consumer>
{(context) => (
<div>{context.somevalue}</div>
)}
</MyContext.Consumer>
);
}
}
Upvotes: 6
Reputation: 2547
Reading the source code of React-Context, they do
<MyContext.Provider value={{
state: this.state,
}}>
and
<MyContext.Consumer>
{(context) => <p>{context.state.age}</p>}
So if you do
<MyContext.Provider value={{ somevalue: 1 }}>
{this.props.children}
</MyContext.Provider>
You should get somevalue
like that
<MyContext.Consumer>
{(context) => <div>{context.somevalue}</div>}
</MyContext.Consumer>
EDIT
What if you create a file called myContext.js
with:
const MyContext = React.createContext('test');
export default MyContext;
and then import it like :
import MyContext form '<proper_path>/myContext';
Upvotes: 13