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.
[00:00] 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.
[00:10] 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.
[00:20] 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.
[00:29] Let's start by adding a file called router.js to our router directory. I want to import React and component from React. We'll export class, called router, which 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.
[01:08] 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.
[01:26] 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.
[01:39] 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.
[01:52] To keep our router simple, we're just going to return the last segment of the path name. I'm going to return a call to path.substring, and we're going to start that substring at path.lastIndexOf/.
[02:13] 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.
[02:23] I'm going to drop down under state, and I'm going to define a new method, I'll call it handle line click. 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.
[02:44] 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.
[03:04] With this defined, let's save the file, and in index.js, under the router directory, let's add an export for the router component. 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.
[03:23] What I want to do is I want to add an import here for the router component, and we're going to grab that from components/router, and then we can wrap this app tag in our router component tag.
[03:45] When the browser reloads, we can open up the dev tools. 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.
[04:02] 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 handling click method down via props.
[04:16] 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.
[04:26] 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.
[04:40] 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, call it child context types. That's going to be equal to an object.
[04:54] We're going to expose these like we do with prop types. 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 link handler, we'll call link handler, and that's going to be a function, so react.PropTypes.func.
[05:21] 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 link handler will be this.handle and click.
[05:50] 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.
[06:07] We're going to call this context types, 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 sites from routers. I can actually come to router, and we're going to borrow these, because they're going to be the same exact values.
[06:24] 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 link handler in the router component, I'm going to take that out of there, and instead, I want to call that link handler function.
[06:40] To do that, we're going to reference that through this.context.linkHandler. I want to use the pass that our route, so we'll do that through 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.
[06:58] I'll declare a constant 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. I'm just going to drop down and I'm going to give my anchor tag here a class name attribute, and we'll set that to equal active class.
[07:30] 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 active class for links inside the footer, and we'll just make it 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.
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.
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.
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.
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!
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>
}
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.
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.
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!
Like the previous lesson this also fails to compile due to the use of history.pushState
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.
Andrew
I don't what I'm doing wrong, when the app finish loading the All(link) becomes bold but when I click on other links nothing happens. I watched the video three time and also read transcript but no luck.
Code: https://github.com/BuzweMfaca/todo_react
Andrew
I'm not sure what I'm going but my links are not working, when I click url just append # code: https://github.com/BuzweMfaca/todo_react
Andrew
I'm not sure what I'm going but my links are not working, when I click url just append # code: https://github.com/BuzweMfaca/todo_react
Andrew I'm not sure what I'm going but my links are not working, when I click url just append # code: https://github.com/BuzweMfaca/todo_react
Andrew I'm not sure what I'm going but my links are not working, when I click url just append # code: https://github.com/BuzweMfaca/todo_react referring to https://egghead.io/lessons/react-use-react-context-to-manage-application-state-through-routes
Hi Andrew, does React context work like the provider in react-redux?
Hello, what should I do with this https://prnt.sc/iw75ee
In Link.js I've changed contextTypes to childContextTypes.
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?