41 |
42 | `
43 | }
44 |
45 | var PORT = process.env.PORT || 8080
46 | app.listen(PORT, function() {
47 | console.log('Production Express server running at localhost:' + PORT)
48 | })
49 |
--------------------------------------------------------------------------------
/lessons/07-more-nesting/README.md:
--------------------------------------------------------------------------------
1 | # More Nesting
2 |
3 | Notice how the list of links to different repositories goes away when we
4 | navigate to a repository? What if we want the list to persist, just like
5 | the global navigation persists?
6 |
7 | Try to figure that out before reading on.
8 |
9 | ...
10 |
11 | First, nest the `Repo` route under the `Repos` route. Then go render
12 | `this.props.children` in `Repos`.
13 |
14 | ```js
15 | // index.js
16 | // ...
17 |
18 |
19 |
20 | ```
21 |
22 | ```js
23 | // Repos.js
24 | // ...
25 |
26 |
Repos
27 |
28 |
React Router
29 |
React
30 |
31 | {/* will render `Repo.js` when at /repos/:userName/:repoName */}
32 | {this.props.children}
33 |
34 | ```
35 |
36 | ## Active Links
37 |
38 | Let's bring in our `NavLink` from before so we can add the `active`
39 | class name to these links:
40 |
41 | ```js
42 | // modules/Repos.js
43 | // import it
44 | import NavLink from './NavLink'
45 |
46 | // ...
47 |
React Router
48 |
React
49 | // ...
50 | ```
51 |
52 | Notice how both the `/repos` link up top and the individual repo links are
53 | both active? When child routes are active, so are the parents.
54 |
55 | ---
56 |
57 | [Next: Index Routes](../08-index-routes/)
58 |
--------------------------------------------------------------------------------
/lessons/09-index-links/README.md:
--------------------------------------------------------------------------------
1 | # Index Links
2 |
3 | Have you noticed in our app that we don't have any navigation to get
4 | back to rendering the `Home` component?
5 |
6 | Lets add a link to `/` and see what happens:
7 |
8 | ```js
9 | // in App.js
10 | // ...
11 |
Home
12 | // ...
13 | ```
14 |
15 | Now navigate around. Notice anything weird? The link to `Home` is always
16 | active! As we learned earlier, parent routes are active when child routes
17 | are active. Unfortunately, `/` is the parent of everything.
18 |
19 | For this link, we want it to only be active when the index route is
20 | active. There are two ways to let the router know you're linking to the
21 | "index route" so it only adds the active class (or styles) when the
22 | index route is rendered.
23 |
24 | ## IndexLink
25 |
26 | First let's use the `IndexLink` instead of `NavLink`
27 |
28 | ```js
29 | // App.js
30 | import { IndexLink } from 'react-router'
31 |
32 | // ...
33 |
Home
34 | ```
35 |
36 | Fixed! Now this link is only "active" when we're at the index route. Go
37 | ahead and click around to see.
38 |
39 | ## `onlyActiveOnIndex` Property
40 |
41 | We can use `Link` as well by passing it the `onlyActiveOnIndex` prop
42 | (`IndexLink` just wraps `Link` with this property for convenience).
43 |
44 | ```js
45 |
Home
46 | ```
47 |
48 | That's fine, but we already abstracted away having to know what the
49 | `activeClassName` is with `Nav`.
50 |
51 | Remember, in `NavLink` we're passing along all of our props to `Link` with
52 | the `{...spread}` syntax, so we can actually add the prop when we render
53 | a `NavLink` and it will make its way down to the `Link`:
54 |
55 | ```js
56 |
Home
57 | ```
58 |
59 | ---
60 |
61 | [Next: Clean URLs](../10-clean-urls/)
62 |
--------------------------------------------------------------------------------
/lessons/10-clean-urls/README.md:
--------------------------------------------------------------------------------
1 | # Clean URLs with Browser History
2 |
3 | The URLs in our app right now are built on a hack: the hash. It's the
4 | default because it will always work, but there's a better way.
5 |
6 | Modern browsers let JavaScript manipulate the URL without making an http
7 | request, so we don't need to rely on the hash (`#`) portion of the url
8 | to do routing, but there's a catch (we'll get to it later).
9 |
10 | ## Configuring Browser History
11 |
12 | Open up `index.js` and import `browserHistory` instead of `hashHistory`.
13 |
14 | ```js
15 | // index.js
16 | // ...
17 | // bring in `browserHistory` instead of `hashHistory`
18 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'
19 |
20 | render((
21 |
22 | {/* ... */}
23 |
24 | ), document.getElementById('app'))
25 | ```
26 |
27 | Now go click around and admire your clean URLs.
28 |
29 | Oh yeah, the catch. Click on a link and then refresh your browser. What
30 | happens?
31 |
32 | ```
33 | Cannot GET /repos
34 | ```
35 |
36 | ## Configuring Your Server
37 |
38 | Your server needs to deliver your app no matter what URL comes in,
39 | because your app, in the browser, is manipulating the URL. Our current
40 | server doesn't know how to handle the URL.
41 |
42 | The Webpack Dev Server has an option to enable this. Open up
43 | `package.json` and add `--history-api-fallback`.
44 |
45 | ```json
46 | "start": "webpack-dev-server --inline --content-base . --history-api-fallback"
47 | ```
48 |
49 | We also need to change our relative paths to absolute paths in
50 | `index.html` since the URLs will be at deep paths and the app, if it
51 | starts at a deep path, won't be able to find the files.
52 |
53 | ```html
54 |
55 |
56 |
57 |
58 |
59 |
60 | ```
61 |
62 | Stop your server if it's running, then `npm start` again. Look at those
63 | clean URLs :)
64 |
65 | ---
66 |
67 | [Next: Production-ish Server](../11-productionish-server/)
68 |
--------------------------------------------------------------------------------
/lessons/06-params/README.md:
--------------------------------------------------------------------------------
1 | # URL Params
2 |
3 | Consider the following URLs:
4 |
5 | ```
6 | /repos/reactjs/react-router
7 | /repos/facebook/react
8 | ```
9 |
10 | These URLs would match a route path like this:
11 |
12 | ```
13 | /repos/:userName/:repoName
14 | ```
15 |
16 | The parts that start with `:` are URL parameters whose values will be
17 | parsed out and made available to route components on
18 | `this.props.params[name]`.
19 |
20 | ## Adding a Route with Parameters
21 |
22 | Lets teach our app how to render screens at `/repos/:userName/:repoName`.
23 |
24 | First we need a component to render at the route, make a new file at
25 | `modules/Repo.js` that looks something like this:
26 |
27 | ```js
28 | // modules/Repo.js
29 | import React from 'react'
30 |
31 | export default React.createClass({
32 | render() {
33 | return (
34 |
35 |
{this.props.params.repoName}
36 |
37 | )
38 | }
39 | })
40 | ```
41 |
42 | Now open up `index.js` and add the new route.
43 |
44 | ```js
45 | // ...
46 | // import Repo
47 | import Repo from './modules/Repo'
48 |
49 | render((
50 |
51 |
52 |
53 | {/* add the new route */}
54 |
55 |
56 |
57 |
58 | ), document.getElementById('app'))
59 | ```
60 |
61 | Now we can add some links to this new route in `Repos.js`.
62 |
63 | ```js
64 | // Repos.js
65 | import { Link } from 'react-router'
66 | // ...
67 | export default React.createClass({
68 | render() {
69 | return (
70 |
71 |
Repos
72 |
73 | {/* add some links */}
74 |
75 |
React Router
76 |
React
77 |
78 |
79 |
80 | )
81 | }
82 | })
83 | ```
84 |
85 | Now go test your links out. Note that the parameter name in the route
86 | `path` becomes the property name in the component. Both `repoName` and
87 | `userName` are available on `this.props.params` of your component. You
88 | should probably add some prop types to help others and yourself out
89 | later.
90 |
91 | ---
92 |
93 | [Next: More Nesting](../07-more-nesting/)
94 |
--------------------------------------------------------------------------------
/lessons/02-rendering-a-route/README.md:
--------------------------------------------------------------------------------
1 | # Rendering a Route
2 |
3 | At its heart, React Router is a component.
4 |
5 | ```js
6 | render(, document.getElementById('app'))
7 | ```
8 |
9 | That's not going to display anything until we configure a route.
10 |
11 | Open up `index.js` and
12 |
13 | 1. import `Router`, `Route`, and `hashHistory`
14 | 2. render a `Router` instead of `App`
15 |
16 | ```js
17 | // ...
18 | import { Router, Route, hashHistory } from 'react-router'
19 |
20 | render((
21 |
22 |
23 |
24 | ), document.getElementById('app'))
25 | ```
26 |
27 | Make sure your server is running with `npm start` and then visit
28 | [http://localhost:8080](http://localhost:8080)
29 |
30 | You should get the same screen as before, but this time with some junk
31 | in the URL. We're using `hashHistory`--it manages the routing history
32 | with the hash portion of the url. It's got that extra junk to shim some
33 | behavior the browser has natively when using real urls. We'll change
34 | this to use real urls later and lose the junk, but for now, this works
35 | great because it doesn't require any server-side configuration.
36 |
37 | ## Adding More Screens
38 |
39 | Create two new components at:
40 |
41 | - `modules/About.js`
42 | - `modules/Repos.js`
43 |
44 | ```js
45 | // modules/About.js
46 | import React from 'react'
47 |
48 | export default React.createClass({
49 | render() {
50 | return
62 | }
63 | })
64 | ```
65 |
66 | Now we can couple them to the app at their respective paths.
67 |
68 | ```js
69 | // insert into index.js
70 | import About from './modules/About'
71 | import Repos from './modules/Repos'
72 |
73 | render((
74 |
75 |
76 | {/* add the routes here */}
77 |
78 |
79 |
80 | ), document.getElementById('app'))
81 | ```
82 |
83 | Now visit [http://localhost:8080/#/about](http://localhost:8080/#/about) and
84 | [http://localhost:8080/#/repos](http://localhost:8080/#/repos)
85 |
86 | ---
87 |
88 | [Next: Navigating With Link](../03-navigating-with-link/)
89 |
--------------------------------------------------------------------------------
/lessons/05-active-links/README.md:
--------------------------------------------------------------------------------
1 | # Active Links
2 |
3 | One way that `Link` is different from `a` is that it knows if the path
4 | it links to is active so you can style it differently.
5 |
6 | ## Active Styles
7 |
8 | Let's see how it looks with inline styles, add `activeStyle` to your
9 | `Link`s.
10 |
11 | ```js
12 | // modules/App.js
13 |
About
14 |
Repos
15 | ```
16 |
17 | Now as you navigate, the active link is red.
18 |
19 | ## Active Class Name
20 |
21 | You can also use an active class name instead of inline-styles.
22 |
23 | ```js
24 | // modules/App.js
25 |
About
26 |
Repos
27 | ```
28 |
29 | We don't have a stylesheet on the page yet though. Lets add one-extra
30 | point if you can add a `link` tag from memory.
31 |
32 | ```html
33 | // index.html
34 |
35 | ```
36 |
37 | And the CSS file:
38 |
39 | ```css
40 | .active {
41 | color: green;
42 | }
43 | ```
44 |
45 | You'll need to manually refresh the browser since Webpack isn't building
46 | our `index.html`.
47 |
48 | ## Nav Link Wrappers
49 |
50 | Most links in your site don't need to know they are active, usually just
51 | primary navigation links need to know. It's useful to wrap those so you
52 | don't have to remember what your `activeClassName` or `activeStyle` is
53 | everywhere.
54 |
55 | We will use a spread operator here, the three dots. It clones our props
56 | and in this use case it clones `activeClassName` to our desired component for
57 | us to benefit from.
58 |
59 | Create a new file at `modules/NavLink.js` that looks like this:
60 |
61 | ```js
62 | // modules/NavLink.js
63 | import React from 'react'
64 | import { Link } from 'react-router'
65 |
66 | export default React.createClass({
67 | render() {
68 | return
69 | }
70 | })
71 | ```
72 |
73 | Now you can go change your links to `NavLink`s.
74 |
75 | ```js
76 | // modules/App.js
77 | import NavLink from './NavLink'
78 |
79 | // ...
80 |
81 |
About
82 |
Repos
83 | ```
84 |
85 | Oh, how beautiful upon the renders is the composability of components.
86 |
87 | ---
88 |
89 | [Next: Params](../06-params/)
90 |
--------------------------------------------------------------------------------
/lessons/08-index-routes/README.md:
--------------------------------------------------------------------------------
1 | # Index Routes
2 |
3 | When we visit `/` in this app it's just our navigation and a blank page.
4 | We'd like to render a `Home` component there. Lets create a `Home`
5 | component and then talk about how to render it at `/`.
6 |
7 | ```js
8 | // modules/Home.js
9 | import React from 'react'
10 |
11 | export default React.createClass({
12 | render() {
13 | return
Home
14 | }
15 | })
16 | ```
17 |
18 | One option is to see if we have any children in `App`, and if not,
19 | render `Home`:
20 |
21 | ```js
22 | // modules/App.js
23 | import Home from './Home'
24 |
25 | // ...
26 |
30 | //...
31 | ```
32 |
33 | This would work fine, but its likely we'll want `Home` to be attached to
34 | a route like `About` and `Repos` in the future. A few reasons include:
35 |
36 | 1. Participating in a data fetching abstraction that relies on matched
37 | routes and their components.
38 | 2. Participating in `onEnter` hooks
39 | 3. Participating in code-splitting
40 |
41 | Also, it just feels good to keep `App` decoupled from `Home` and let the
42 | route config decide what to render as the children. Remember, we want to
43 | build small apps inside small apps, not big ones!
44 |
45 | Let's add a new route to `index.js`.
46 |
47 | ```js
48 | // index.js
49 | // new imports:
50 | // add `IndexRoute` to 'react-router' imports
51 | import { Router, Route, hashHistory, IndexRoute } from 'react-router'
52 | // and the Home component
53 | import Home from './modules/Home'
54 |
55 | // ...
56 |
57 | render((
58 |
59 |
60 |
61 | {/* add it here, as a child of `/` */}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ), document.getElementById('app'))
71 | ```
72 |
73 | Now open [http://localhost:8080](http://localhost:8080) and you'll see the new component is
74 | rendered.
75 |
76 | Notice how the `IndexRoute` has no path. It becomes
77 | `this.props.children` of the parent when no other child of the parent
78 | matches, or in other words, when the parent's route matches exactly.
79 |
80 | Index routes can twist people's brains up sometimes. Hopefully it will
81 | sink in with a bit more time. Just think about a web server that looks
82 | for `index.html` when you're at `/`. Same idea, React Router looks for
83 | an index route if a route's path matches exactly.
84 |
85 | ---
86 |
87 | [Next: Index Links](../09-index-links/)
88 |
--------------------------------------------------------------------------------
/lessons/12-navigating/README.md:
--------------------------------------------------------------------------------
1 | # Navigating Programatically
2 |
3 | While most navigation happens with `Link`, you can programmatically
4 | navigate around an application in response to form submissions, button
5 | clicks, etc.
6 |
7 | Let's make a little form in `Repos` that programmatically navigates.
8 |
9 | ```js
10 | // modules/Repos.js
11 | import React from 'react'
12 | import NavLink from './NavLink'
13 |
14 | export default React.createClass({
15 |
16 | // add this method
17 | handleSubmit(event) {
18 | event.preventDefault()
19 | const userName = event.target.elements[0].value
20 | const repo = event.target.elements[1].value
21 | const path = `/repos/${userName}/${repo}`
22 | console.log(path)
23 | },
24 |
25 | render() {
26 | return (
27 |
28 |
Repos
29 |
30 |
React Router
31 |
React
32 | {/* add this form */}
33 |
34 |
39 |
40 |
41 | {this.props.children}
42 |
43 | )
44 | }
45 | })
46 | ```
47 |
48 | There are two ways you can do this, the first is simpler than the
49 | second.
50 |
51 | First we can use the `browserHistory` singleton that we passed into
52 | `Router` in `index.js` and push a new URL into the history.
53 |
54 | ```js
55 | // modules/Repos.js
56 | import { browserHistory } from 'react-router'
57 |
58 | // ...
59 | handleSubmit(event) {
60 | // ...
61 | const path = `/repos/${userName}/${repo}`
62 | browserHistory.push(path)
63 | },
64 | // ...
65 | ```
66 |
67 | There's a potential problem with this though. If you pass a different
68 | history to `Router` than you use here, it won't work. It's not very
69 | common to use anything other than `browserHistory`, so this is
70 | acceptable practice. If you're concerned about it, you can make a module
71 | that exports the history you want to use across the app, or...
72 |
73 | You can also use the `router` that `Router` provides on "context".
74 | First, you ask for context in the component, and then you can use it:
75 |
76 | ```js
77 | export default React.createClass({
78 |
79 | // ask for `router` from context
80 | contextTypes: {
81 | router: React.PropTypes.object
82 | },
83 |
84 | // ...
85 |
86 | handleSubmit(event) {
87 | // ...
88 | this.context.router.push(path)
89 | },
90 |
91 | // ..
92 | })
93 | ```
94 |
95 | This way you'll be sure to be pushing to whatever history gets passed to
96 | `Router`. It also makes testing a bit easier since you can more easily
97 | stub context than singletons.
98 |
99 | ---
100 |
101 | [Next: Server Rendering](../13-server-rendering/)
102 |
--------------------------------------------------------------------------------
/lessons/04-nested-routes/README.md:
--------------------------------------------------------------------------------
1 | # Nested Routes
2 |
3 | The navigation we added to `App` should probably be present on every
4 | screen. Without React Router, we could wrap that `ul` into a
5 | component, say `Nav`, and render a `Nav` on every one of our screens.
6 |
7 | This approach isn't as clean as the application grows. React Router
8 | provides another way to share UI like this with nested routes, a trick
9 | it learned from [Ember](http://emberjs.com) (/me tips hat).
10 |
11 | ## Nested UI and Nested URLs
12 |
13 | Have you ever noticed your app is just a series of boxes inside boxes
14 | inside boxes? Have you also noticed your URLs tend to be coupled to that
15 | nesting? For example given this url, `/repos/123`, our
16 | components would probably look like this:
17 |
18 | ```js
19 | {/* / */}
20 | {/* /repos */}
21 | {/* /repos/123 */}
22 |
23 |
24 | ```
25 |
26 | And our UI something like:
27 |
28 | ```
29 | +-------------------------------------+
30 | | Home Repos About | <- App
31 | +------+------------------------------+
32 | | | |
33 | Repos -> | repo | Repo 1 |
34 | | | |
35 | | repo | Boxes inside boxes |
36 | | | inside boxes ... | <- Repo
37 | | repo | |
38 | | | |
39 | | repo | |
40 | | | |
41 | +------+------------------------------+
42 | ```
43 |
44 | React Router embraces this by letting you nest your routes, which
45 | automatically becomes nested UI.
46 |
47 | ## Sharing Our Navigation
48 |
49 | Let's nest our `About` and `Repos` components inside of `App` so that we
50 | can share the navigation with all screens in the app. We do it in two
51 | steps:
52 |
53 | First, let the `App` `Route` have children, and move the other routes
54 | underneath it.
55 |
56 | ```js
57 | // index.js
58 | // ...
59 | render((
60 |
61 |
62 | {/* make them children of `App` */}
63 |
64 |
65 |
66 |
67 | ), document.getElementById('app'))
68 | ```
69 |
70 | Next, render children inside of `App`.
71 |
72 | ```js
73 | // modules/App.js
74 | // ...
75 | render() {
76 | return (
77 |
88 | )
89 | }
90 | // ...
91 | ```
92 |
93 | Alright, now go click the links and notice that the `App` component
94 | continues to render while the child route's component gets swapped
95 | around as `this.props.children` :)
96 |
97 | React Router is constructing your UI like this:
98 |
99 | ```js
100 | // at /about
101 |
102 |
103 |
104 |
105 | // at /repos
106 |
107 |
108 |
109 | ```
110 |
111 | ## By Small and Simple Things are Great Things Brought to Pass
112 |
113 | The best way to build large things is to stitch small things together.
114 |
115 | This is the real power of React Router, every route can be developed
116 | (even rendered!) as an independent application. Your route configuration
117 | stitches all these apps together however you'd like. Applications
118 | inside of Applications, boxes inside of boxes.
119 |
120 | What happens if you move the `About` route outside of `App`?
121 |
122 | Okay, now put it back.
123 |
124 | ---
125 |
126 | [Next: Active Links](../05-active-links/)
127 |
--------------------------------------------------------------------------------
/lessons/11-productionish-server/README.md:
--------------------------------------------------------------------------------
1 | # Production-ish Server
2 |
3 | None of this has anything to do with React Router, but since we're
4 | talking about web servers, we might as well take it one step closer to
5 | the real-world. We'll also need it for server rendering in the next
6 | section.
7 |
8 | Webpack dev server is not a production server. Let's make a production
9 | server and a little environment-aware script to boot up the right server
10 | depending on the environment.
11 |
12 | Let's install a couple modules:
13 |
14 | ```
15 | npm install express if-env compression --save
16 | ```
17 |
18 | First, we'll use the handy `if-env` in `package.json`. Update your
19 | scripts entry in package.json to look like this:
20 |
21 | ```json
22 | // package.json
23 | "scripts": {
24 | "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
25 | "start:dev": "webpack-dev-server --inline --content-base . --history-api-fallback",
26 | "start:prod": "webpack && node server.js"
27 | },
28 | ```
29 |
30 | When you run `npm start` it checks if the value of our `NODE_ENV` environment variable is
31 | `production`. If yes, it runs `npm run start:prod`, if not, it runs
32 | `npm run start:dev`.
33 |
34 | Now we're ready to create a production server with Express and add a new file at root dir. Here's a
35 | first attempt:
36 |
37 | ```js
38 | // server.js
39 | var express = require('express')
40 | var path = require('path')
41 | var compression = require('compression')
42 |
43 | var app = express()
44 |
45 | // serve our static stuff like index.css
46 | app.use(express.static(__dirname))
47 |
48 | // send all requests to index.html so browserHistory in React Router works
49 | app.get('*', function (req, res) {
50 | res.sendFile(path.join(__dirname, 'index.html'))
51 | })
52 |
53 | var PORT = process.env.PORT || 8080
54 | app.listen(PORT, function() {
55 | console.log('Production Express server running at localhost:' + PORT)
56 | })
57 | ```
58 |
59 | Now run:
60 |
61 | ```sh
62 | NODE_ENV=production npm start
63 | # For Windows users:
64 | # SET "NODE_ENV=production" && npm start
65 | ```
66 |
67 | Congratulations! You now have a production server for this app. After
68 | clicking around, try navigating to [http://localhost:8080/package.json](http://localhost:8080/package.json).
69 | Whoops. Let's fix that. We're going to shuffle around a couple files and
70 | update some paths scattered across the app.
71 |
72 | 1. make a `public` directory.
73 | 2. Move `index.html` and `index.css` into it.
74 |
75 | Now let's update `server.js` to point to the right directory for static
76 | assets:
77 |
78 | ```js
79 | // server.js
80 | // ...
81 | // add path.join here
82 | app.use(express.static(path.join(__dirname, 'public')))
83 |
84 | // ...
85 | app.get('*', function (req, res) {
86 | // and drop 'public' in the middle of here
87 | res.sendFile(path.join(__dirname, 'public', 'index.html'))
88 | })
89 | ```
90 |
91 | We also need to tell webpack to build to this new directory:
92 |
93 | ```js
94 | // webpack.config.js
95 | // ...
96 | output: {
97 | path: 'public',
98 | // ...
99 | }
100 | ```
101 |
102 | And finally (!) add it to the `--content-base` argument to `npm run start:dev` script:
103 |
104 | ```json
105 | "start:dev": "webpack-dev-server --inline --content-base public --history-api-fallback",
106 | ```
107 |
108 | If we had the time in this tutorial, we could use the `WebpackDevServer`
109 | API in a JavaScript file instead of the CLI in an npm script and then
110 | turn this path into config shared across all of these files. But, we're
111 | already on a tangent, so that will have to wait for another time.
112 |
113 | Okay, now that we aren't serving up the root of our project as public
114 | files, let's add some code minification to Webpack and gzipping to
115 | express.
116 |
117 | ```js
118 | // webpack.config.js
119 |
120 | // make sure to import this
121 | var webpack = require('webpack')
122 |
123 | module.exports = {
124 | // ...
125 |
126 | // add this handful of plugins that optimize the build
127 | // when we're in production
128 | plugins: process.env.NODE_ENV === 'production' ? [
129 | new webpack.optimize.DedupePlugin(),
130 | new webpack.optimize.OccurrenceOrderPlugin(),
131 | new webpack.optimize.UglifyJsPlugin()
132 | ] : [],
133 |
134 | // ...
135 | }
136 | ```
137 |
138 | And compression in express:
139 |
140 | ```js
141 | // server.js
142 | // ...
143 | var compression = require('compression')
144 |
145 | var app = express()
146 | // must be first!
147 | app.use(compression())
148 | ```
149 |
150 | Now go start your server in production mode:
151 |
152 | ```
153 | NODE_ENV=production npm start
154 | ```
155 |
156 | You'll see some UglifyJS logging and then in the browser, you can see
157 | the assets are being served with gzip compression.
158 |
159 | ---
160 |
161 | [Next: Navigating](../12-navigating/)
162 |
--------------------------------------------------------------------------------
/lessons/13-server-rendering/README.md:
--------------------------------------------------------------------------------
1 | # Server Rendering
2 |
3 | Alright, first things first. Server rendering, at its core is a simple
4 | concept in React.
5 |
6 | ```js
7 | render(, domNode)
8 | // can be rendered on the server as
9 | const markup = renderToString()
10 | ```
11 |
12 | It's not rocket science, but it also isn't trivial. First I'm going to
13 | just throw a bunch of webpack shenanigans at you with little
14 | explanation, then we'll talk about the Router.
15 |
16 | Since node doesn't (and shouldn't) understand JSX, we need to compile
17 | the code somehow. Using something like `babel/register` is not fit for
18 | production use, so we'll use webpack to build a server bundle, just like
19 | we use it to build a client bundle.
20 |
21 | Make a new file called `webpack.server.config.js` and put this stuff in
22 | there:
23 |
24 | ```js
25 | var fs = require('fs')
26 | var path = require('path')
27 |
28 | module.exports = {
29 |
30 | entry: path.resolve(__dirname, 'server.js'),
31 |
32 | output: {
33 | filename: 'server.bundle.js'
34 | },
35 |
36 | target: 'node',
37 |
38 | // keep node_module paths out of the bundle
39 | externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([
40 | 'react-dom/server', 'react/addons',
41 | ]).reduce(function (ext, mod) {
42 | ext[mod] = 'commonjs ' + mod
43 | return ext
44 | }, {}),
45 |
46 | node: {
47 | __filename: true,
48 | __dirname: true
49 | },
50 |
51 | module: {
52 | loaders: [
53 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' }
54 | ]
55 | }
56 |
57 | }
58 | ```
59 |
60 | Hopefully some of that makes sense, we aren't going to cover what all of
61 | that stuff does, it's sufficient to say that now we can run our
62 | `server.js` file through webpack and then run it.
63 |
64 | Now we need to make some scripts to build server bundle before we try to
65 | run our app. Update your `package.json` script config to look like
66 | this:
67 |
68 | ```
69 | "scripts": {
70 | "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
71 | "start:dev": "webpack-dev-server --inline --content-base public/ --history-api-fallback",
72 | "start:prod": "npm run build && node server.bundle.js",
73 | "build:client": "webpack",
74 | "build:server": "webpack --config webpack.server.config.js",
75 | "build": "npm run build:client && npm run build:server"
76 | },
77 | ```
78 |
79 | Now when we run `NODE_ENV=production npm start` both the client and
80 | server bundles get created by Webpack.
81 |
82 | Okay, let's talk about the Router. We're going to need our routes split
83 | out into a module so that both the client and server entries can require
84 | it. Make a file at `modules/routes` and move your routes and components
85 | into it.
86 |
87 | ```js
88 | // modules/routes.js
89 | import React from 'react'
90 | import { Route, IndexRoute } from 'react-router'
91 | import App from './App'
92 | import About from './About'
93 | import Repos from './Repos'
94 | import Repo from './Repo'
95 | import Home from './Home'
96 |
97 | module.exports = (
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | )
106 | ```
107 |
108 | ```js
109 | // index.js
110 | import React from 'react'
111 | import { render } from 'react-dom'
112 | import { Router, browserHistory } from 'react-router'
113 | // import routes and pass them into
114 | import routes from './modules/routes'
115 |
116 | render(
117 | ,
118 | document.getElementById('app')
119 | )
120 | ```
121 |
122 | Now open up `server.js`. We're going to bring in two modules from React
123 | Router to help us render on the server.
124 |
125 | If we tried to render a `` on the server like we do in the
126 | client, we'd get an empty screen since server rendering is synchronous
127 | and route matching is asynchronous.
128 |
129 | Also, most apps will want to use the router to help them load data, so
130 | asynchronous routes or not, you'll want to know what screens are going
131 | to render before you actually render so you can use that information to
132 | load asynchronous data before rendering. We don't have any data loading
133 | in this app, but you'll see where it could happen.
134 |
135 | First we import `match` and `RouterContext` from react router, then
136 | we'll match the routes to the url, and finally render.
137 |
138 | ```js
139 | // ...
140 | // import some new stuff
141 | import React from 'react'
142 | // we'll use this to render our app to an html string
143 | import { renderToString } from 'react-dom/server'
144 | // and these to match the url to routes and then render
145 | import { match, RouterContext } from 'react-router'
146 | import routes from './modules/routes'
147 |
148 | // ...
149 |
150 | // send all requests to index.html so browserHistory works
151 |
152 | app.get('*', (req, res) => {
153 | // match the routes to the url
154 | match({ routes: routes, location: req.url }, (err, redirect, props) => {
155 | // `RouterContext` is the what `Router` renders. `Router` keeps these
156 | // `props` in its state as it listens to `browserHistory`. But on the
157 | // server our app is stateless, so we need to use `match` to
158 | // get these props before rendering.
159 | const appHtml = renderToString()
160 |
161 | // dump the HTML into a template, lots of ways to do this, but none are
162 | // really influenced by React Router, so we're just using a little
163 | // function, `renderPage`
164 | res.send(renderPage(appHtml))
165 | })
166 | })
167 |
168 | function renderPage(appHtml) {
169 | return `
170 |
171 |
172 |
173 | My First React Router App
174 |
175 |
${appHtml}
176 |
177 | `
178 | }
179 |
180 | var PORT = process.env.PORT || 8080
181 | app.listen(PORT, function() {
182 | console.log('Production Express server running at localhost:' + PORT)
183 | })
184 | ```
185 |
186 | And that's it. Now if you run `NODE_ENV=production npm start` and visit
187 | the app, you can view source and see that the server is sending down our
188 | app to the browser. As you click around, you'll notice the client app
189 | has taken over and doesn't make requests to the server for UI. Pretty
190 | cool yeah?!
191 |
192 |
193 | Our callback to match is a little naive, here's what a production
194 | version would look like:
195 |
196 | ```js
197 | app.get('*', (req, res) => {
198 | match({ routes: routes, location: req.url }, (err, redirect, props) => {
199 | // in here we can make some decisions all at once
200 | if (err) {
201 | // there was an error somewhere during route matching
202 | res.status(500).send(err.message)
203 | } else if (redirect) {
204 | // we haven't talked about `onEnter` hooks on routes, but before a
205 | // route is entered, it can redirect. Here we handle on the server.
206 | res.redirect(redirect.pathname + redirect.search)
207 | } else if (props) {
208 | // if we got props then we matched a route and can render
209 | const appHtml = renderToString()
210 | res.send(renderPage(appHtml))
211 | } else {
212 | // no errors, no redirect, we just didn't match anything
213 | res.status(404).send('Not Found')
214 | }
215 | })
216 | })
217 | ```
218 |
219 | Server rendering is really new. There aren't really "best practices"
220 | yet, especially when it comes to data loading, so this tutorial is done,
221 | dropping you off at the bleeding edge.
222 |
223 | ---
224 |
225 | [Next: What's Next?](../14-whats-next/)
226 |
--------------------------------------------------------------------------------