This lesson is for PRO members.

Unlock this lesson NOW!
Already subscribed? sign in

Use React Context to Manage Application State Through Routes

7:51 React lesson by

We’ll create a Router component that will wrap our application and manage all URL related state. We’ll see how we can use React’s built in context mechanism to pass data and functions between components without having to pass props all the way down through the component tree.

Get the Code Now
click to level up

egghead.io comment guidelines

Avatar
egghead.io

We’ll create a Router component that will wrap our application and manage all URL related state. We’ll see how we can use React’s built in context mechanism to pass data and functions between components without having to pass props all the way down through the component tree.

Avatar
Marko

Quote from the Facebook page: "If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React." (https://facebook.github.io/react/docs/context.html)

Should we be concerned?

Avatar
Andrew Van Slaars

I wouldn't be. This code could be updated to pass that same information down through the entire component tree via props. Also, if you're building anything with more routing functionality, you'll likely want to pull in something like react-router, which also uses context. I'm sure if the API breaks in the future, there will be some alternative, or it will be reasonable to update existing code.

In reply to Marko
Avatar
gitnicos

Quite honestly, I am a bit confused. On the one hand we are in for pure functions use as much as possible and by all cost have to avoid reliance on the global state, here though we are taking a context approach. I understand value of closures (function context) but static context could be single source of bugs. Also, is not it role of this.state to act as a reasonable single version of truth and state propagation with controlled boundaries?
I guess I am saying I question if this is a "best practice" to stick to.

In reply to Andrew Van Slaars
Avatar
Andrew Van Slaars

Context here is a convenience to avoid having to pass routing data down through the entire component tree as props, but if you prefer to avoid context, you can handle it via props from the top of the component tree.

In regards to having a single source of truth. The most important aspect to having a "single source of truth" is not that there is only one source for everything, but that each "fact" can only come from a single source. The trouble comes in when you have two sources for the same data. So if, for instance, I had exposed the route data via context and then assigned that value into some property on state in the top level App component, then there would be a risk in those getting out of sync. In this case, the state of the Router component is the source of truth for routing and is exposed via context just for convenience.

Hope this helps.

In reply to gitnicos
Avatar
gitnicos

Totally! It dawned on me that since this is a "special case" of using "context" in context of router, which by definition is supposed to be a "state" change coordinator, hence wrapping the the App component itself. Makes sense. Thanks again for eloquent explanation, Andrew!

In reply to Andrew Van Slaars
Avatar
Hector Pacheco

I have been using the es6 instead of es7, and I have gotten stuck on this lesson, I can't see to create the context to satisfied es6, I have followed a few suggestions found on the net. any suggestions here is my code
import React,{Component} from 'react'

const getCurrentPath = () => {
const path = document.location.pathname
return path.substring(path.lastIndexOf('/'))
}
export class Router extends Component{

constructor(){
    super()
    this.state = {
        route: getCurrentPath()
    }
    this.handleLinkClick = this.handleLinkClick.bind(this)
    this.getChildContext = this.getChildContext.bind(this)
    this.childContextTypes = {
        route: React.PropTypes.string,
        linkHandler: React.PropTypes.func
    }
}

handleLinkClick(route){
    this.state({route})
    history.pushState(null,'',route)
}



getChildContext(){
    return {
        route: this.state.route,
        linkHandler: this.handleLinkClick
    }
}


render(){
    return <div>{this.props.children}</div>
}
Avatar
Andrew Van Slaars

Hector,

It looks like the problem is that you're defining childContextTypes on the class instance by using this, but that needs to be static. You can use the static keyword with ES6 class like in the repo for the lesson:

https://github.com/avanslaars/egghead_react_todo_app_course/blob/lesson_17/src/components/router/Router.js#L18-L21

Or you can attach them to the class outside of the class definition like

Router.childContextTypes = {
        route: React.PropTypes.string,
        linkHandler: React.PropTypes.func
    }

Hope this gets everything working for you.

In reply to Hector Pacheco
Avatar
Caden Albaugh

This question should've been asked way earlier in the series and I'm sure has a very simple answer but here goes. How are you able to not use any semicolons in your code? It finally got to me enough to ask. Thanks.

Avatar
Andrew Van Slaars

Caden,

It's pretty simple. Semicolons are not required in JavaScript. There are a couple cases where you need them, but if you don't write code that runs into those edge cases, it's completely safe and valid.

You can read some more specifics here: regarding semicolons

There are even linting setups that specifically call for no semicolons, I like Standard

People tend to have very strong feelings about this, some love it, others hate it. I prefer to leave them out, but if I'm working on a team that prefers them, I will put them in.

Hope this helps!

In reply to Caden Albaugh
Avatar
Brian Jensen

Like the previous lesson this also fails to compile due to the use of history.pushState

Avatar
Andrew Van Slaars

Andrew Van Slaars 14 minutes ago
Brian,

Just like the previous video, this is a linting error caused by some updated lint config.

You should be able to get everything working again by referencing history directly from window with window.history. You could also disable linting for that line by adding // eslint-disable-line to the end of the offending line.

You could also use the same version of react scripts as the video if you would prefer to not apply these workarounds while following along with the videos.

Hope this helps.

In reply to Brian Jensen

As it stands, the Footer component is using this Link component to render() the hyperlinks in this app. It also responds to clicks by updating the URL through history.pushState.

Link.js

export class Link extends Component {
    handleClick = (evt) => {
        evt.preventDefault()
        history.pushState(null, '', this.props.to)
    }

This is fine for updating the URL and history, but without querying the document location, we have no way of knowing what route the app is currently on in our other components.

We should put this information in our application state, and we should do it higher up in the component tree, so it's more accessible to components that need it. For this, we'll create a streamlined Router component.

Let's start by adding a file called Router.js to our router directory. I want to import React, {Component} from 'react'. We'll also export class Router extends Component, and we'll give it a render() method. render() is going to return a <div> that contains children, so we'll get that through this.props.children.

Router.js

export class Router extends Component {
    render() {
        return <div>{this.props.children}</div>
    }
}

I also want to give the Router some state, so we're going to just use property initializer syntax, so I can just say state equals and assign an object at the class level. The Router is going to maintain a single state property that represents the current route, so we'll just define route on the state object.

Initially, we're going to have to calculate our route, so I'm going to do that in a function I'll call getCurrentPath, and we'll call that here. Then we're going to come up here, outside of our class, and define getCurrentPath.

export class Router extends Component { 
    state = {
        route: getCurrentPath()
    }
    render() {
        return <div>{this.props.children}</div>
    }
}

That's going to be const getCurrentPath, and that's going to equal a function. Inside the function, we'll start by defining a const, we'll call it path. That's going to be equal to document.location.pathname.

const getCurrentPath = () => {
    const path = document.location.pathname
}

To keep our Router simple, we're just going to return the last segment of the pathname. I'm going to return a call to path.substring(), and we're going to start that substring at path.lastIndexOf('/').

const getCurrentPath = () => {
    const path = document.location.pathname
    return path.substring(path.lastIndexOf('/'))
}

Now, this was at the state's route property when this component is loaded, but it won't be updated when we click on a link. Let's create a method that will update the route and handle the call to history.pushState in this component.

I'm going to drop down under state, and I'm going to define a new method, I'll call it handleLinkClick. This is going to accept a single argument, we'll call route. In here, I'm just going to call this.setState, passing it in an object that contains route.

export class Router extends Component { 
    state = {
        route: getCurrentPath()
    }

    handleLinkClick = (route) => {
        this.setState({route})
    }

    ...

Since my value name and my property name are the same, I can shorthand it to just route inside the curly braces. Then I'm going to call history.pushState to handle the update to our browser history, and pushState is going to take null, an empty string, and then our route as arguments.

handleLinkClick = (route) => {
    this.setState({route})
    history.pushState(null, '', route)
}

With this defined, let's save the file, and in index.js, under the router directory, let's add export {Router} from './Router'. Now we can pull the Router into our application. We're going to do that in our index.js file, where we're rendering out our application.

What I want to do is I want to add import {Router} from './components/router, and then we can wrap this <App /> tag in our <Router> component tag.

src/index.js

ReactDOM.render(
    <Router><App /></Router>
    document.getElementById('root')
);

When the browser reloads, we can open up the devtools. We'll see that our Router component is now at the top level, and then there's the <div> that we have in Router's render() method, followed by our App and everything else that falls inside of it.

Devtools

Now that we have Router wrapped around our App, we want to use it to updated state and call history.pushState when one of our Link components is clicked. The links are nested in the app inside the Footer component, so you might think we would pass the Router's handleLinkClick method down via props.

There are two problems with this. One, in a complex app, that could potentially mean passing the same item down many levels. This could mean a lot of maintenance if things need to change.

The second problem is that, in this setup, App is being placed inside the Router through a call to this.props.children. We can't just add props onto the App component in our render() function. The way we're going to handle this is through React's context mechanism.

The first thing we need to do to use context is to expose the types that we want available to our child components. Let's start by defining a static value on our component, called childContextTypes. That's going to be equal to an object.

Router.js

handleLinkClick = (route) => {
    this.setState({route})
    history.pushState(null, '', route)
}

static childContextTypes = {

}

We're going to expose these like we do with PropTypes. We're going to start with a key, and then we're going to assign that key a type using React.PropTypes. Our route's going to be a .string. We're also going to expose our linkHandler, we'll call linkHandler, and that's going to be a function, so React.PropTypes.func.

static childContextTypes = {
    route: React.PropTypes.string,
    linkHandler: React.PropTypes.func
}

Now that our types are defined, we need to define a method that'll actually get these values out of our component, and we do that with a method called getChildContext. getChildContext will return an object with our keys and their associated values, so in this case, it'll be this.state.route, and linkHandler will be this.handleLinkClick.

getChildContext() {
    return {
        route: this.state.route,
        linkHandler: this.handleLinkClick
    }
}

Now we've exposed our context, so let's save that file. Then we want to go into Link.js and we want to be able to consume the context in our Link component. In order to use context in this component, we're going to come up to the top of the class and we're going to define a static value.

We're going to call this contextTypes, and this is going to be an object. This'll define the keys and their data types, just like the way we expose child context types from Router. I can actually come to Router, and we're going to borrow these key and values, because they're going to be the same exact values.

Link.js

export class Link extends Component {
    static contextTypes = {
        route: this.state.route,
        linkHandler: this.handleLinkClick      
    }

We'll just paste them right in there. This is all we really have to do in order to consume context. I'm going to drop down here, and since history.pushState is already taken care of in our linkHandler in the Router component, I'm going to take pushState out of handleClick, and instead, I want to call that linkHandler function.

To do that, we're going to reference that through this.context.linkHandler. I want to pass that our route, so we'll do that through this.props.to.

handleClick = (evt) => {
    evt.preventDefault()
    this.context.linkHandler(this.props.to)
}

Since we also have access to route through context, let's drop down here and apply an active class to our Link if it matches the active route.

I'll declare a const called activeClass, and then we'll say if this.context.route is equal to this.props.to, then that value will be the string 'active' and otherwise we'll just use an empty string ''.

render() {
    const activeClass = this.context.route === this.props.to ? 'active' ''
    return <a href="#" onClick={this.handleClick}>{this.props.children}</a>
}

I'm just going to drop down and I'm going to give my anchor tag here a className attribute, and we'll set that to equal 'activeClass'.

I can save that and then I'm going to open up App.css, and down at the bottom, I'm just going to define that activeClass for links inside the Footer, and we'll just make it bold.

App.css

.Footer a.active {
    font-weight:bold;
}

After the browser reloads, we'll see that all is bold, and as I click through the links, my address is updated and my class is applied to the appropriate Link.

HEY, QUICK QUESTION!
Joel's Head
Why are we asking?