When you have two unrelated kinds of data such as a current user and a currently-selected email, it’s common for some parts of the app to need the user and some other parts of the app to need the email. One way to do this is to create multiple Contexts, each with its own Provider and Consumer, and use each one to distribute one kind of data to the slices of the app that need it. In this lesson we’ll pass user data and email data to separate subtrees using two separate Contexts.
Instructor: [00:00] Let's make this email app actually display some emails. To do that, we're going to make a new context, in a file called email-context.js. We'll import react and the fetchEmails function from our API. Then we'll create a new context with react.createContext. Then we'll destructure that into provider and consumer.
[00:20] Then we'll create a class called email provider to hold the state and distribute it with the provider. We'll initialize the state, total emails. which will be an empty array, current email, which will start off as null, error, which also starts as null, and a loading flag, which starts out false.
[00:39] Then we'll implement the componentDidMount life cycle method to fetch the emails. In here, we'll this.setState. The loading true, and reset any error that we might have had. Then we'll call fetchEmails. Then with the emails, I'll call setState, set loading to false, and save the emails into state.
[01:00] If there's an error, we'll catch it, and call setState with loading false, and the error. We'll add a method called handleSelectEmail that'll take an email and call setState to update the current email. This will give consumers a function to call when they want to change the current email.
[01:22] For render, we'll return the provider, which will render out this.props.children. The value of this provider is going to be an object that's a copy of state, plus another property, called onSelectEmail. We'll pass in our handleSelectEmail function. Finally, we can export this email provider class and consumer as emailConsumer.
[01:48] Now that we have a provider, we can go back to index.js and at the top, we'll import emailProvider from our new email context file. At the bottom, we can wrap route in email provider. Now, the whole app will have access to emails and the current user. Now, let's open up message list, and we'll import our email consumer from emailContext.
[02:14] Since this component is already consuming one context, we can just add our emailContext inside. We could also put this outside of userConsumer. It doesn't really matter here. Inside the emailConsumer, we'll write the function that's going to pull out loading, emails, and onSelectEmail from context.
[02:34] Now we have all this data. We just need to render it out. If we're loading, we'll render a loading message, otherwise if email's length is zero, we'll render out the mailbox is empty message that we already have. Otherwise, if we have messages, it'll render out an unordered list. Inside that list, we can map over the emails, with emails.map.
[02:57] For each email, render out an email component, which we'll create in a second, passing the required key prop, with the email's unique ID. Down below, we'll create this email component. It's going to need the email to display, and an onClick prop. It'll render out a list item, passing along the onClick prop to make it clickable.
[03:20] Inside, we'll have a div for the subject, that displays email subject, and a div for the message preview. Now, we can pass these props along to the email, passing in the email itself, and an onClick prop, which will be a function that calls onSelectEmail with the email. Now, it's all wired up, and we can see the emails displayed, but clicking on them doesn't do anything yet.
[03:45] Let's open up the main page file, and we can see right now this component is only ever rendering a list. We need it to render either a list or the currently selected email. To do that, we'll pull in emailConsumer from emailContext, and wrap the content with this consumer. Inside here, we'll pull out the current email property from context.
[04:07] If we have a current email, then we'll render a message viewer. Otherwise, we'll render the message list. We just need to import messageViewer and now, when we click on an email, we're taken to the message display. Let's work on this display now. We'll open up messageViewer and import the emailConsumer.
[04:30] Then we can wrap this content in an emailConsumer, and pull out the current email and onSelectEmail from context. Inside here, we'll replace this with an h2 to display current email subject, and a div, to display current email body.
[04:49] At the top, we'll add a button for navigating back, and give an onClick prop that'll call onSelectEmail with null to reset the selected email, and bring us back to the list. Now, we can click an email, read the message, click back, and it's all working.
api.js
is included in the example code for the lesson. It's not a component, just a set of named exports, one for each endpoint. Like export function login(username, password) { ... }
. The file for this example has some fake data setup and stuff, but in a real app, it'd have the global fetch or axios config like you mention.
I forked your sandbox and now I seem to be getting: Error A cross-origin error was thrown. React doesn't have access to the actual error object in development. See https://fb.me/react-crossorigin-error for more information. ▶ 17 stack frames were collapsed. evaluate /src/index.js:17:9 14 | ); 15 | } 16 |
17 | ReactDOM.render( | ^ 18 | <UserProvider> 19 | <EmailProvider> 20 | <Root />
Can you help? I'm stuck now.
Nevermind! I found the typo!
FYI: The 'before' code for this lesson is here if you need it.
I have an AuthContext which sets the autentication token on componentDidMount(). Also a nested component which fetches data once the api authentication is done. but right now the fetchData() method is getting executed before getting the authToken. This cause the fetchData() request call to fail. Is there a way to make sure my AuthContext runs before the rest of the nested context getting triggered.
@shiraz React renders the children before the parents, because a component isn't considered "mounted" until all of its nested children have been rendered. I'd suggest checking that the authToken is present before making the call in fetchData
, and rely on the fact that the nested component will re-render after the AuthContext
has updated its state with the token.
Thanks @Dave
Hi, could you provide some example code for api.js? Would this be a class component with global fetch/axios config and then named exports for each API endpoint? Thanks