MattDuFeu
MattDuFeu

Reputation: 1655

React New Context API - Access Existing Context across Multiple Files

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.

App.js

render() {
    return (
        <MyProvider>
            <MyConsumer />
        </MyProvider>
    );
}

provider.js

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 >
        );
    }
}

consumer.js

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

Answers (5)

Norfeldt
Norfeldt

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

anthonynorton
anthonynorton

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

f1lt3r
f1lt3r

Reputation: 2223

TLDR; Demo on CodeSandbox

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.

How to Wrap a React/Unstated-Context as a Service

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:

Api.js

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
}

App.js

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"));

Pages/Home.js:

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

Wen W
Wen W

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

Striped
Striped

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

Related Questions