├── .gitignore
├── Gemfile
├── _includes
├── header.html
├── icon-github.html
├── icon-twitter.html
├── icon-twitter.svg
├── head.html
├── icon-github.svg
└── footer.html
├── _layouts
├── page.html
├── default.html
└── post.html
├── _config.yml
├── about.md
├── index.html
├── README.md
├── _posts
├── 2016-01-13-hot-module-reload.markdown
├── 2016-01-02-setup.markdown
├── 2016-01-12-istanbul.markdown
├── 2016-01-01-intro.markdown
├── 2016-01-17-redux-devtools.markdown
├── 2016-01-15-react-tools.markdown
├── 2016-01-20-ajax-and-lifecycle.markdown
├── 2016-01-04-tooling.markdown
├── 2016-01-18-fixing-tests.markdown
├── 2016-01-08-react-router.markdown
├── 2016-01-07-react-2.markdown
├── 2016-01-06-jsx.markdown
├── 2016-01-21-webpack-chunking.markdown
├── 2016-01-05-tooling-2.markdown
├── 2016-01-11-react-testing.markdown
├── 2016-01-03-react.markdown
├── 2016-01-19-universal-rendering.markdown
├── 2016-01-16-redux.markdown
├── 2016-01-10-react-state.markdown
├── 2016-01-14-react-marshall-data.markdown
└── 2016-01-09-react-props.markdown
├── css
└── main.scss
├── feed.xml
├── _sass
├── _syntax-highlighting.scss
├── _base.scss
└── _layout.scss
└── Gemfile.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 | .jekyll-metadata
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gem 'github-pages'
--------------------------------------------------------------------------------
/_includes/header.html:
--------------------------------------------------------------------------------
1 | {{ page.title }}
8 |
npm install from the directory where you downloaded the repo. If you have node and npm installed, you should see a list of dependencies being installed.
18 |
19 | ## npm global installs
20 |
21 | Run the following global npm installs
22 |
23 | {% highlight bash %}
24 | npm install -g mocha
25 | npm install -g nodemon
26 | npm install -g webpack
27 | npm install -g standard
28 | {% endhighlight %}
29 |
30 | We'll be using webpack v1.12, Mocha v2.4 and standard v6.0.
--------------------------------------------------------------------------------
/feed.xml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: null
3 | ---
4 |
5 | npm run cover you should see 100% coverage on both Search and ShowCard. That means all the exported code is getting covered in a test! Yay! But where are the rest of our files? Well, we're not testing them at all yet so istanbul doesn't know about them. Once you start testing those files they'll show up.
14 |
15 | Now try adding an extra method to Search. It can be some dumb method that returns 5. Now run your npm run cover again. Your code will not be totally covered now. This is what istanbul gets you. If you open coverage/index.html in your browser, you'll actually be able to visualize what the code coverage looks like. Pretty sweet.
16 |
17 | [istanbul]: https://github.com/gotwarlost/istanbul
18 | [nyc]: https://github.com/bcoe/nyc
--------------------------------------------------------------------------------
/_posts/2016-01-01-intro.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Intro"
3 | ---
4 |
5 | ## **NOTE**: This is an older version of this workshop. Go to [The React Learning Path][react-path] to find the latest version of Complete Intro to React
6 |
7 | Welcome to _A Complete Intro to React_. The goal of this workshop is to get you full up to speed on modern development and give you an idea what it is like to develop an app in the React ecosystem.
8 |
9 | When talking about React, you cannot simply _just_ React. It is an incomplete picture; it is a cog in the machine. React does well to introduce some useful primitives into your toolbox and allows you to build your app without introducing too many opinions. As such, we need to learn some other tools to round the whole story of our app. However, keep in mind that these are modules we are choosing to plug together; you are free to go home and swap in your own parts to suit your own needs. While React fits many/most needs, this complete stack won't; it has opinions and you need to make sure each piece contributes to your story and try not to shoehorn any piece in that you don't need.
10 |
11 | In addition to React, we are going to be using node.js, express, redux, WebPack, Mocha, Enzyme, npm, and react-router. Don't worry if you're aren't familiar with any of these: that's the point of this workshop. Since this workshop is about React, we will not be deep diving into node.js or database schema. We will discuss these satellite concepts just as they pertain to React.
12 |
13 | Questions? Feel free to tweet at me. Corrections or concerns about this page? Please file an issue or make a pull request on the repo.
14 |
15 | [readme]: https://github.com/btholt/complete-intro-to-react-v1/blob/gh-pages/README.md
16 | [react-path]: https://frontendmasters.com/learn/react/
17 |
--------------------------------------------------------------------------------
/_posts/2016-01-17-redux-devtools.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "redux Devtools"
3 | ---
4 |
5 | One of the most compelling reason to use redux is its amazing debugging experience. redux has the _fantastic_ ability to do time-traveling debugging, meaning you can step forwards and backwards through actions you've dispatched. It's really powerful for debugging.
6 |
7 | There are several ways to get it working but I'm going to show you the bare minimum to get up and running. Unlike React, there is some code you have to put in to get it working. Luckily, it's like two or three lines.
8 |
9 | In Store.jsx put:
10 |
11 | {% highlight javascript %}
12 | // replace store declaration
13 | const store = redux.createStore(reducer, initialState, redux.compose(
14 | typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => f
15 | ))
16 | {% endhighlight %}
17 |
18 | That's it for code! It just adds a middleware to redux that hooks into the dev tools. It's also doing a window check to make sure if you're unit testing or running your components in node that the window reference doesn't blow up.
19 |
20 | Now go grab the [Chrome extension][chrome-extension]. The Firefox one is forthcoming as is the Safari one. Until then Chrome is it. Good news is that you can actually just build the debugger into the page so it works everywhere. Bad news is I'm going to show you how since I've never done it. In any case, feel free to explore it on your time.
21 |
22 | Okay, last bit: this doesn't work with the file:/// protocol. So we're going to use node's [http-server][http-server]. If you don't have it already just run npm install -g http-server. From there, navigate to your project's root directory and run http-server -p 5050 ./. Then open localhost:5050 in your browser.
23 |
24 | Now you should see three green circles with smaller orange circles circling the green ones on your Chrome tool bar. Click on that and that should show you the redux tools. This allows you to play with the redux tools. I'll let you toy with them but suffice to say they're pretty impressive.
25 |
26 | [chrome-extension]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en
27 | [http-server]: https://github.com/indexzero/http-server
--------------------------------------------------------------------------------
/_posts/2016-01-15-react-tools.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "React Tools"
3 | ---
4 |
5 | So far we've been using pretty old school debugging technology: console.logs and just dumping stuff out to the DOM. There is an easier way! [React Dev Tools][github]! Grab it here for [Chrome][chrome] and [Firefox][firefox]. In _theory_ you can get the Chrome version working on Microsoft Edge, but good luck. If you're not using Chrome or Firefox, you're out of luck for now. They're talking about doing a standalone app but we'll see when that finally surfaces. I'll be talking about the Chrome version because that's the one I know the best.
6 |
7 | [github]: https://github.com/facebook/react-devtools
8 | [chrome]: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
9 | [firefox]: https://addons.mozilla.org/en-US/firefox/addon/react-devtools/
10 |
11 | Dev Tools allow you to explore the virtual DOM of React as if it was just a normal DOM tree. You can see what props and states are in each component and even modify the state of components. The virtual DOM explorer is by-far the most useful part of it.
12 |
13 | Find the Dev Tools in Chrome by opening the Chrome dev tools and the last tab (along side Console, Sources, Network, etc.) should be React on your page. If you don't see it try restarting your browser. If you _still_ don't see it, the tab won't show up if the extension can't detect React on the page. You may have a dated version of the dev tools. After that I'm not sure; it can be fickle sometimes.
14 |
15 | Feel free to poke around a bit here to familiarize yourself with what the React Dev Tools can do. Here are a couple of tricks for you:
16 |
17 | - If you right-click -> Inspect Element on something and then click the React tab, it will take you straight to that element in the virtual DOM.
18 | - Select something in the virtual DOM in the React tab. Now go to the Console tab and type $r. It should be a reference to the element you have selected in the virtual DOM and you can manipulate it.
19 | - As a side note, you can do the above trick with the normal DOM explorer with $0, $1, $2, $3, and $4 instead where 0 is the last one you selected, 1 is the penultimate, etc.
20 | - iframes and Chrome extensions don't work with React Dev Tools as of writing.
--------------------------------------------------------------------------------
/_posts/2016-01-20-ajax-and-lifecycle.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "React Lifecycle Methods and AJAX with React"
3 | ---
4 |
5 | Due to the structuring of our app, we haven't had to use React lifecycle methods despite the fact they're fairly common to use and thus important to know. One of the most compelling reasons to use lifecycle methods is to do AJAX. Once a component gets mounted to the page then we want to be able request data from the server. First let's discuss the lifecycle of a React component.
6 |
7 | 1. __constructor/getInitialState__ - This is where set up your components initial state. The former is for ES6 classes (that we've been using) and the latter is for the React.createClass method.
8 | 1. __componentWillMount__ - This method runs right _before_ the component gets mounted. This one is not too common to use, but you will want to use it any time you want code to run both in node and in the browser.
9 | 1. __componentDidMount__ - This method runs right _after_ your component gets put into the DOM. This method _will not get run in node but will in the browser_. This makes it so your component can render first _then_ you can go get the data you need. In your component you can throw up a loader if you need to. Also if you need to interact with the DOM (like if you were wrapping D3 or a jQuery plugin) this would be the place to do it.
10 | 1. __componentWillUnmount__ - This method runs right before the component is taken off the DOM. Most common thing to do here is get rid of external event listeners or other things you need to clean up.
11 |
12 | Cool! So let's make our Details page check the IMDB rating! Open Details.jsx
13 |
14 | {% highlight javascript %}
15 | // require in axios
16 | const axios = require('axios')
17 |
18 | // add constructor and componentDidMount to Details
19 | constructor (props) {
20 | super(props)
21 |
22 | this.state = {
23 | omdbData: {}
24 | }
25 | }
26 | componentDidMount () {
27 | console.log('here', this.props.shows[this.props.params.id].imdbID)
28 | axios.get(`http://www.omdbapi.com/?i=${this.props.shows[this.props.params.id].imdbID}`)
29 | .then((response) => {
30 | this.setState({omdbData: response.data})
31 | })
32 | .catch((error) => {
33 | console.error('axios error', error)
34 | })
35 | }
36 |
37 | // add to render before return
38 | let rating
39 | if (this.state.omdbData.imdbRating) {
40 | rating = npm install -g standard is a wrapper around a pre-configured [eslint][eslint-docs]. If you are unfamiliar with linting tools (like jshint, jslint, jscs) the basic gist is that this tool programatically checks for certain violations of code style in your code. A common (and arguably the most useful) check that it does is make sure you have no unused variables and that you make use of no undeclared variables in your code. A common reason to trip either of those rules is that misspelled a variable somewhere. There are dozens of other checks standard does: check them out [here][standard-checks].
12 |
13 | A note on standard, since it inevitably causes controversy. standard is a set of eslint rules that is meant to not to be configurable to totally sidestep the conversation of what rules include and what not to. It's meant to be a step in the direction of a standard format in the JavaScript community, much akin to how the Go community has standardized on one. Perhaps the most controversial rule in standard is no semicolons. It's okay to not use semicolons in JS. Really. I promise. It's not slower and it won't introduce crazy bugs. It's okay to use them too! It doesn't particularly matter. However, in this workshop, you code won't pass lint if you use semicolons. If you _insist_ that you need your semicolons, you can use the package [semistandard][semistandard]. But give standard a shot. You may like it.
14 |
15 | That being said, we _will_ introducing new rules for our JSX since we want some rules added to enforce good "Reactful" code.
16 |
17 | So, head to your terminal and navigate to the directory of the project. Given that standard is preconfigured to recursively check all js and jsx files recursively in a project and leave out some sane defaults (it doesn't check node_modules/) you should be able to just run standard from your terminal and see if you have any issues. If you do, go fix them! If you feel so inclined, you can use [standard-format][standard-format] to have your code auto-fixed for you. I've found it sometimes has some derpy indentation but you may have better results.
18 |
19 | ## npm scripts
20 |
21 | One of the nice convenience that npm allows you is its scripts features. If you work on a team or an open source project, it's a great way to standardize how things are done in your project. Even if it's just you, it's an easy way to shorten commands. So go to your package.json and add:
22 |
23 | {% highlight json %}
24 | "scripts": {
25 | "test": "standard"
26 | },
27 | {% endhighlight %}
28 |
29 | Once in your package.json, now you can go to your terminal and run npm run test and it will run standard for you. This isn't a big right now but we'll start chaining our whole testing process together and we won't have to remember the complex stuff we're doing each time. Furthermore, npm run test is a standard in the node community and should work in just about any project. We'll have several scripts in here and it quickly becomes _very_ convenient.
30 |
31 | Since all our tests, lints, and builds will be done through npm scripts (which are just commands piped to the terminal) we don't need any tools like Grunt, Gulp, or the like. Anyone who has ever maintained a Gruntfile knows why this is such a big deal.
32 |
33 | [eslint-docs]: http://eslint.org/
34 | [standard-checks]: https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
35 | [semistandard]: https://github.com/Flet/semistandard
36 | [standard-format]: https://github.com/maxogden/standard-format
--------------------------------------------------------------------------------
/_sass/_base.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Reset some basic elements
3 | */
4 | body, h1, h2, h3, h4, h5, h6,
5 | p, blockquote, pre, hr,
6 | dl, dd, ol, ul, figure {
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 |
12 |
13 | /**
14 | * Basic styling
15 | */
16 | body {
17 | font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;
18 | color: $text-color;
19 | background-color: $background-color;
20 | -webkit-text-size-adjust: 100%;
21 | -webkit-font-feature-settings: "kern" 1;
22 | -moz-font-feature-settings: "kern" 1;
23 | -o-font-feature-settings: "kern" 1;
24 | font-feature-settings: "kern" 1;
25 | font-kerning: normal;
26 | }
27 |
28 |
29 |
30 | /**
31 | * Set `margin-bottom` to maintain vertical rhythm
32 | */
33 | h1, h2, h3, h4, h5, h6,
34 | p, blockquote, pre,
35 | ul, ol, dl, figure,
36 | %vertical-rhythm {
37 | margin-bottom: $spacing-unit / 2;
38 | }
39 |
40 |
41 |
42 | /**
43 | * Images
44 | */
45 | img {
46 | max-width: 100%;
47 | vertical-align: middle;
48 | }
49 |
50 |
51 |
52 | /**
53 | * Figures
54 | */
55 | figure > img {
56 | display: block;
57 | }
58 |
59 | figcaption {
60 | font-size: $small-font-size;
61 | }
62 |
63 |
64 |
65 | /**
66 | * Lists
67 | */
68 | ul, ol {
69 | margin-left: $spacing-unit;
70 | }
71 |
72 | li {
73 | > ul,
74 | > ol {
75 | margin-bottom: 0;
76 | }
77 | }
78 |
79 |
80 |
81 | /**
82 | * Headings
83 | */
84 | h1, h2, h3, h4, h5, h6 {
85 | font-weight: $base-font-weight;
86 | }
87 |
88 |
89 |
90 | /**
91 | * Links
92 | */
93 | a {
94 | color: $brand-color;
95 | text-decoration: none;
96 |
97 | &:visited {
98 | color: darken($brand-color, 15%);
99 | }
100 |
101 | &:hover {
102 | color: $text-color;
103 | text-decoration: underline;
104 | }
105 | }
106 |
107 |
108 |
109 | /**
110 | * Blockquotes
111 | */
112 | blockquote {
113 | color: $grey-color;
114 | border-left: 4px solid $grey-color-light;
115 | padding-left: $spacing-unit / 2;
116 | font-size: 18px;
117 | letter-spacing: -1px;
118 | font-style: italic;
119 |
120 | > :last-child {
121 | margin-bottom: 0;
122 | }
123 | }
124 |
125 |
126 |
127 | /**
128 | * Code formatting
129 | */
130 | pre,
131 | code {
132 | font-size: 15px;
133 | border: 1px solid $grey-color-light;
134 | border-radius: 3px;
135 | background-color: #eef;
136 | }
137 |
138 | code {
139 | padding: 1px 5px;
140 | }
141 |
142 | pre {
143 | padding: 8px 12px;
144 | overflow-x: auto;
145 |
146 | > code {
147 | border: 0;
148 | padding-right: 0;
149 | padding-left: 0;
150 | }
151 | }
152 |
153 |
154 |
155 | /**
156 | * Wrapper
157 | */
158 | .wrapper {
159 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
160 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
161 | margin-right: auto;
162 | margin-left: auto;
163 | padding-right: $spacing-unit;
164 | padding-left: $spacing-unit;
165 | @extend %clearfix;
166 |
167 | @include media-query($on-laptop) {
168 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
169 | max-width: calc(#{$content-width} - (#{$spacing-unit}));
170 | padding-right: $spacing-unit / 2;
171 | padding-left: $spacing-unit / 2;
172 | }
173 | }
174 |
175 |
176 |
177 | /**
178 | * Clearfix
179 | */
180 | %clearfix {
181 |
182 | &:after {
183 | content: "";
184 | display: table;
185 | clear: both;
186 | }
187 | }
188 |
189 |
190 |
191 | /**
192 | * Icons
193 | */
194 | .icon {
195 |
196 | > svg {
197 | display: inline-block;
198 | width: 16px;
199 | height: 16px;
200 | vertical-align: middle;
201 |
202 | path {
203 | fill: $grey-color;
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/_posts/2016-01-18-fixing-tests.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Fixing Our Tests and Testing redux"
3 | ---
4 |
5 | So we broke all of our tests. They all fail now. High five! This is a big reason why I'm hesitant to test UI code: I find my tests break all the time just because I'm rewriting markup or other code. Nonetheless, let's refix our tests and add two for redux. As opposed to testing React which I don't do much of, I test the hell out of my redux code. redux code is very testable and you should cover all or nearly-all of your reducers with tests.
6 |
7 | Cool, open our spec doc. Let's fix these tests one-at-a-time. For the last two tests, change their method calls from it( ... ) to xit( ... ). This will prevent Mocha from running them so we can work on them one-at-a-time.
8 |
9 | The first test is actually testing Header so let's move it to its own describe too.
10 |
11 | {% highlight javascript %}
12 | // change what's pulled in from enzyme
13 | const { render } = enzyme
14 |
15 | // more requires
16 | const Header = require('../js/Header')
17 | const Store = require('../js/Store')
18 | const { store } = Store
19 |
20 | // new describe
21 | describe('const { Router, Route, hashHistory } = ReactRouter code looks foreign to you, check out [2ality's post on it][destructuring].
44 |
45 | So now we got react-router rolling, let's move onto our second page, the search page.
46 |
47 | Let's make our search page. We're going to start with some dummy data and work our way from there. Again, follow the same HTML structure and CSS naming as me and you'll get all the styling for free. Feel free to take a glance at public/data.json to see what's there. As you may have guessed, it's a bunch of Netflix shows. This whole workshop is actually just an elaborate advertisement for Netflix (just kidding; I promise.)
48 |
49 | webpack allows you to require in json files just like you would another JavaScript file so we'll take advantage of that when we start coding our new search page. However we do have add another loader to do that. Add the following object to your loaders array in your webpack.config.js.
50 |
51 | {% highlight javascript %}
52 | {
53 | test: /\.json$/,
54 | loader: 'json-loader'
55 | }
56 | {% endhighlight %}
57 |
58 | Create a new file called Search.jsx. In Search.jsx put:
59 |
60 | {% highlight javascript %}
61 | const React = require('react')
62 | const data = require('../public/data')
63 |
64 | const Search = () => (
65 | or Browse All to
92 |
93 | {% highlight javascript %}
94 | // add at top with other requires
95 | const ReactRouter = require('react-router')
96 | const { Link } = ReactRouter
97 |
98 | // change to
99 | or Browse All
100 | {% endhighlight %}
101 |
102 | Now you have another working route (which all it's doing is showing an h1) and a link to get there. When linking between routes with react-router, use the Link component. This allows you to refactor how routes work without having refactor all of your individual links (you could just make your a's href "#/search" and it would work for now but could break later.) Now your button should work to take you to the browser page and you should be able to use back and forward for free thanks to react-router.
103 |
104 | [react-router]: https://github.com/reactjs/react-router
105 | [destructuring]: http://www.2ality.com/2015/01/es6-destructuring.html#destructuring
--------------------------------------------------------------------------------
/_sass/_layout.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Site header
3 | */
4 | .site-header {
5 | border-top: 5px solid $grey-color-dark;
6 | border-bottom: 1px solid $grey-color-light;
7 | min-height: 56px;
8 |
9 | // Positioning context for the mobile navigation icon
10 | position: relative;
11 | }
12 |
13 | .site-title {
14 | font-size: 26px;
15 | font-weight: 300;
16 | line-height: 56px;
17 | letter-spacing: -1px;
18 | margin-bottom: 0;
19 | float: left;
20 |
21 | &,
22 | &:visited {
23 | color: $grey-color-dark;
24 | }
25 | }
26 |
27 | .site-nav {
28 | float: right;
29 | line-height: 56px;
30 |
31 | .menu-icon {
32 | display: none;
33 | }
34 |
35 | .page-link {
36 | color: $text-color;
37 | line-height: $base-line-height;
38 |
39 | // Gaps between nav items, but not on the last one
40 | &:not(:last-child) {
41 | margin-right: 20px;
42 | }
43 | }
44 |
45 | @include media-query($on-palm) {
46 | position: absolute;
47 | top: 9px;
48 | right: $spacing-unit / 2;
49 | background-color: $background-color;
50 | border: 1px solid $grey-color-light;
51 | border-radius: 5px;
52 | text-align: right;
53 |
54 | .menu-icon {
55 | display: block;
56 | float: right;
57 | width: 36px;
58 | height: 26px;
59 | line-height: 0;
60 | padding-top: 10px;
61 | text-align: center;
62 |
63 | > svg {
64 | width: 18px;
65 | height: 15px;
66 |
67 | path {
68 | fill: $grey-color-dark;
69 | }
70 | }
71 | }
72 |
73 | .trigger {
74 | clear: both;
75 | display: none;
76 | }
77 |
78 | &:hover .trigger {
79 | display: block;
80 | padding-bottom: 5px;
81 | }
82 |
83 | .page-link {
84 | display: block;
85 | padding: 5px 10px;
86 |
87 | &:not(:last-child) {
88 | margin-right: 0;
89 | }
90 | margin-left: 20px;
91 | }
92 | }
93 | }
94 |
95 |
96 |
97 | /**
98 | * Site footer
99 | */
100 | .site-footer {
101 | border-top: 1px solid $grey-color-light;
102 | padding: $spacing-unit 0;
103 | }
104 |
105 | .footer-heading {
106 | font-size: 18px;
107 | margin-bottom: $spacing-unit / 2;
108 | }
109 |
110 | .contact-list,
111 | .social-media-list {
112 | list-style: none;
113 | margin-left: 0;
114 | }
115 |
116 | .footer-col-wrapper {
117 | font-size: 15px;
118 | color: $grey-color;
119 | margin-left: -$spacing-unit / 2;
120 | @extend %clearfix;
121 | }
122 |
123 | .footer-col {
124 | float: left;
125 | margin-bottom: $spacing-unit / 2;
126 | padding-left: $spacing-unit / 2;
127 | }
128 |
129 | .footer-col-1 {
130 | width: -webkit-calc(35% - (#{$spacing-unit} / 2));
131 | width: calc(35% - (#{$spacing-unit} / 2));
132 | }
133 |
134 | .footer-col-2 {
135 | width: -webkit-calc(20% - (#{$spacing-unit} / 2));
136 | width: calc(20% - (#{$spacing-unit} / 2));
137 | }
138 |
139 | .footer-col-3 {
140 | width: -webkit-calc(45% - (#{$spacing-unit} / 2));
141 | width: calc(45% - (#{$spacing-unit} / 2));
142 | }
143 |
144 | @include media-query($on-laptop) {
145 | .footer-col-1,
146 | .footer-col-2 {
147 | width: -webkit-calc(50% - (#{$spacing-unit} / 2));
148 | width: calc(50% - (#{$spacing-unit} / 2));
149 | }
150 |
151 | .footer-col-3 {
152 | width: -webkit-calc(100% - (#{$spacing-unit} / 2));
153 | width: calc(100% - (#{$spacing-unit} / 2));
154 | }
155 | }
156 |
157 | @include media-query($on-palm) {
158 | .footer-col {
159 | float: none;
160 | width: -webkit-calc(100% - (#{$spacing-unit} / 2));
161 | width: calc(100% - (#{$spacing-unit} / 2));
162 | }
163 | }
164 |
165 |
166 |
167 | /**
168 | * Page content
169 | */
170 | .page-content {
171 | padding: $spacing-unit 0;
172 | }
173 |
174 | .page-heading {
175 | font-size: 20px;
176 | }
177 |
178 | .post-list {
179 | margin-left: 0;
180 | list-style: none;
181 |
182 | > li {
183 | margin-bottom: $spacing-unit;
184 | }
185 | }
186 |
187 | .post-meta {
188 | font-size: $small-font-size;
189 | color: $grey-color;
190 | }
191 |
192 | .post-link {
193 | display: block;
194 | font-size: 24px;
195 | }
196 |
197 |
198 |
199 | /**
200 | * Posts
201 | */
202 | .post-header {
203 | margin-bottom: $spacing-unit;
204 | }
205 |
206 | .post-title {
207 | font-size: 42px;
208 | letter-spacing: -1px;
209 | line-height: 1;
210 |
211 | @include media-query($on-laptop) {
212 | font-size: 36px;
213 | }
214 | }
215 |
216 | .post-content {
217 | margin-bottom: $spacing-unit;
218 |
219 | h2 {
220 | font-size: 32px;
221 |
222 | @include media-query($on-laptop) {
223 | font-size: 28px;
224 | }
225 | }
226 |
227 | h3 {
228 | font-size: 26px;
229 |
230 | @include media-query($on-laptop) {
231 | font-size: 22px;
232 | }
233 | }
234 |
235 | h4 {
236 | font-size: 20px;
237 |
238 | @include media-query($on-laptop) {
239 | font-size: 18px;
240 | }
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/_posts/2016-01-07-react-2.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "More React"
3 | ---
4 |
5 | So let's actually discuss what we're going to build today: a basic Netflix experience. This is going to afford us several fun experiences and use cases to explore with our new-found stack. Our app is going to have a home page, a browse/search page, and a video page. Over the next few chapters we are going to be talking about redux and react-router and their rather-central roles to the React eco-system. But first let's keep diving into React.
6 |
7 | Rather our toys we've made so far to demonstrate how to make React components, let's actually start building our web app. We are going to make our home page. The home page is going to have a sweet background image, our logo, a text input to search for a specific show, and another button to just browse all videos.
8 |
9 | I have pre-done all the CSS for you (since this is not a workshop on CSS) so if you just follow my CSS naming you will get all the CSS for free. It does bear mentioning here that libraries like [Radium][radium] exist that allow you to do all of your styles in JS and let React and Radium handle all the styles for you. This is a pretty big departure from normal web development and not something _most_ developers do so we're not going to cover it. But I will say, should you feel so inclined, go out and give it a shot. I did for a little side project and had a blast with it; it's a fun way to approach CSS since you have all the tools of JS at your disposal and don't really have to care how it compiles down to CSS like you do with PostCSS/Sass/less. Anyway, follow my CSS and you'll be golden. If at any time your styles look broken as compared to mine, chances are you misnamed something.
10 |
11 | Let's start building our app. You can delete MyTitle.jsx if you desire; you can also leave it. We're not going to be using it any further. Go ahead and clear out most ClientApp.jsx and let's start putting our app in here.
12 |
13 | Oh, and you also have to name your video service. I named mine svideo but name your app whatever you want!
14 |
15 | Right now it's going to be pretty simple. Drop in this:
16 |
17 | {% highlight javascript %}
18 |
19 | const React = require('react')
20 | const ReactDOM = require('react-dom')
21 |
22 | const App = () => (
23 | npm run build. If you followed the CSS naming and HTML structure, you should see a nice looking landing page. Also a good time to run npm run test to see if your code is still lint-compliant.
36 |
37 | So, another tooling detour here: I'm getting pretty sick of having to hit the terminal every single time to see run build. Furthermore, build for webpack is pretty slow despite how small our code is. So let's take advantage of webpack's watch feature. This will watch for every time you save rebuild automatically. Additionally it will keep the unchanged bits (like the React library) in memory so it doesn't have to rebuild it every time. Try running webpack --watch in your terminal. It will use the same config we already made. See how much faster it is after running? Let's add a new npm script.
38 |
39 | {% highlight json %}
40 | // in package.json in scripts
41 | "watch": "webpack --watch",
42 | {% endhighlight %}
43 |
44 | Great, right? So, another part that's been bothering me is that it's such a pain to have to re-run standard every time. Either that or you'll get a bunch of errors all at once when you run it before you commit. Luckily we can have webpack run standard each time it compiles. It will then notify you when you have errors.
45 |
46 | Just like we're using the babel-loader to transpile our code, we're going to use the eslint-loader to run standard for us. eslint is what standard uses under the hood, or in other words standard is just a specific configuration of eslint. We're going to lean on this fact and use a config file with the eslint-loader.
47 |
48 | Create a file called .eslintrc.json. That first period is significant. Inside the new file put:
49 |
50 | {% highlight json %}
51 | {
52 | "extends": ["standard", "standard-react"]
53 | }
54 | {% endhighlight %}
55 |
56 | This will configure eslint to check for everything standard has been checking. In addition, since we're already adding a config file, we're adding a few checks specific to React and JSX. Again, these will spare you bugs in the future and they're super helpful. Let's change our webpack config to use our new eslint-loader. eslint will automatically use our .eslintrc.json, regardless is called via the CLI or programmatically via webpack.
57 |
58 | {% highlight javascript %}
59 | // inside module, before loaders
60 | preLoaders: [
61 | {
62 | test: /\.jsx?$/,
63 | loader: "eslint-loader",
64 | exclude: /node_modules/
65 | }
66 | ],
67 | {% endhighlight %}
68 |
69 | Nice! Any time you save now using npm run watch it will both compile your code _and_ lint it. Pretty slick. And it all runs so much faster. We're going to get to how to make your code reloads _even faster_. But first let's keep going with our app.
70 |
71 | Our landing is pretty much done for now. We want to start working on the browse all page, but we need to move onto the router to do that real quick.
72 |
73 | [radium]: http://stack.formidable.com/radium/
--------------------------------------------------------------------------------
/_posts/2016-01-06-jsx.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "JSX"
3 | ---
4 |
5 | We have been writing our React with vanilla JavaScript up until now which frankly few people do. Most people take advantage of JSX which essentially adds HTML/XML-like syntax as a "primitive" type to JavaScript. In reality, what it does it takes the HTML you write for your components and translate them into the same calls we writing just using vanilla JavaScript.
6 |
7 | So why JSX? People usually get pretty grossed out by the HTML in the JavaScript and say it looks like 1998 when we were still writing JavaScript in our HTML. However, I assert that markup in JS is a good thing while JS in markup is a bad thing! Here, we're keeping all the concerns of a component in one place: the markup structure, the event listeners, the state, the state mutators, everything. If the component breaks, we know it broke there. That's really valuable.
8 |
9 | So try it. While the plain JS way isn't too bad, once you start writing JSX I _almost_ guarantee you won't want to go back. Let's convert what we have to JSX.
10 |
11 | Side note: good idea to install a JSX syntax highlighter. If you're using Sublime, I highly recommend the "Babel" package off of Package Control. If you're using VIM, good luck. I've heard people struggle to get it right with VIM.
12 |
13 |
14 | Open MyTitle.js and rename it to MyTitle.jsx. It doesn't actually matter since both are getting run through Babel but it does signify to all who follow that this file __must__ be compiled before being shipped out.
15 |
16 | {% highlight javascript %}
17 | const React = require('react')
18 | const div = React.DOM.div
19 | const h1 = React.DOM.h1
20 |
21 | const MyTitle = React.createClass({
22 | render () {
23 | const style = {color: this.props.color}
24 | return (
25 | this.props.title. They're there to let JSX know I want his to be a JS expression. If they weren't there, it would put the string 'this.props.title'. Notice the double curly braces surrounding the style value. The exterior set of curly braces are the same as the one as before: they're letting JSX know you want a JS expression. The interior set of curly braces represent a JavaScript object, meaning you're passing in a object to the style attribute. A little strange to get used to seeing that.
40 |
41 | Lastly, you may notice that I switched to an ES6 style here. This is synonymous with the function syntax; just a bit more terse. Feel free to write it in any syntax that fits your fancy; this is very readable to me but may not be to you. Basically, since it's an arrow function and lacks curly braces, the resulting component is implicitly returned automatically.
42 |
43 | Let's rewrite ClientApp.js. Rename it to ClientApp.jsx.
44 |
45 | {% highlight javascript %}
46 | const React = require('react')
47 | const ReactDOM = require('react-dom')
48 | const MyTitle = require('./MyTitle')
49 |
50 | const MyFirstComponent = () => (
51 | require.ensure) then webpack will _automatically_ start chunking your JS for. What's more is we don't have to write the glue code that will download the chunks as we need them: webpack just does this for us. All we have to do is identify the modules that can be async by treating them as if they were. Really cool.
10 |
11 | So we're going to treat all of our routes as async and luckily react-router is already instrumented for this for both server and client-side. So let's go make it happen!
12 |
13 | So first we need give a minor tweak to webpack.config.js.
14 |
15 | {% highlight javascript %}
16 | // inside of the output config object
17 | publicPath: '/public/'
18 |
19 | // inside stats
20 | chunks: true
21 | {% endhighlight%}
22 |
23 | First we need to tell webpack where to find all of its bundles instead when it calls back to the server to grab them. We also would like to see some webpack stats now on chunks since we're using them.
24 |
25 | Go to ClientApp
26 |
27 | {% highlight javascript %}
28 | // delete requires for Landing, Search, Details, Route, and IndexRoute
29 |
30 | // after the requires
31 | if (typeof module !== 'undefined' && module.require) {
32 | if (typeof require.ensure === 'undefined') {
33 | require.ensure = require('node-ensure')// shim for node.js
34 | }
35 | }
36 |
37 | // replace myRoutes
38 | const rootRoute = {
39 | component: Layout,
40 | path: '/',
41 | indexRoute: {
42 | getComponent (location, cb) {
43 | require.ensure([], (error) => {
44 | if (error) {
45 | return console.error('ClientApp Landing require.ensure error', error)
46 | }
47 | cb(null, require('./Landing'))
48 | })
49 | }
50 | },
51 | childRoutes: [
52 | {
53 | path: 'search',
54 | getComponent (location, cb) {
55 | require.ensure([], (error) => {
56 | if (error) {
57 | return console.error('ClientApp Search require.ensure error', error)
58 | }
59 | cb(null, require('./Search'))
60 | })
61 | }
62 | },
63 | {
64 | path: 'details/:id',
65 | getComponent (location, cb) {
66 | require.ensure([], (error) => {
67 | if (error) {
68 | return console.error('ClientApp Details require.ensure error', error)
69 | }
70 | cb(null, require('./Details'))
71 | })
72 | }
73 | }
74 | ]
75 | }
76 |
77 | // replace Router and its children
78 | module.exports = MyTitle. At the top of ClientApp.js, put the line var MyTitle = require('./MyTitle'). Let's try to compile that.
14 |
15 | You should have webpack 1.12+ installed (this is untested on 2.0+.) Go to the directory of the project in the terminal and run mkdir public. After that, run webpack js/ClientApp.js public/bundle.js. In your index.html, change the line of <​script src="js/ClientApp.js"> to <​script src="public/bundle.js">.
16 |
17 | Try your browser again. bundle.js has all the stuff for both files now. Now we can keep components in their own files which is a huge win for organization. But wait, we can use webpack for even greater code by using it to bring in node modules. In index.html, remove the other script tags so _just_ bundle.js is left.
18 |
19 | In ClientApp.js, remove the global comment at the top and add at the top
20 |
21 | {% highlight javascript %}
22 | var React = require('react')
23 | var ReactDOM = require('react-dom')
24 | {% endhighlight %}
25 |
26 | Likewise at the top of MyTitle, add the line var React = require('react'). Run your webpack command again. Try your browser again. Despite only including bundle.js, the whole app works! If you look at webpack, you'll see it's 99% React code and some of yours. Now we can go forth making new files and including new libraries without worrying about if they are being included!
27 |
28 | Let's add build to our npm scripts. Add to scripts in package.json "build": "webpack js/ClientApp.js public/bundle.js",. Run npm run build from your terminal in the project root directory and see if it works. Magic!
29 |
30 | ## Babel
31 |
32 | Babel is an amazing, amazing tool that has fundamentally altered the landscape of JavaScript as we know. Though they didn't invent the idea of transpiling future syntax of JavaScript into current syntax (thanks Traceur,) they did make it so damn _easy_. Couple that with the fact it worked with React's JSX (which we'll talk about in a sec) out of the box and it was a perfect storm the be massively successful. Babel, in short, is amazing and should be something you consider for every project.
33 |
34 | Babel 6 (the latest revision of Babel) took away a tiny bit of that ease of use but in return it became much easier to develop which is important when you're important when you're maintaining a massive project that's very important. In return it requires a bit of config now.
35 |
36 | Create a new file called .babelrc. Inside .babelrc, put:
37 |
38 | {% highlight json %}
39 | {
40 | "presets": ["react", "es2015"]
41 | }
42 | {% endhighlight %}
43 |
44 | Babel has the concept of plugins. Each transformation comes in the form a plugin. However ES6 and React each have a number of transformation and it's a huge pain to include each one. Thus Babel has the concept of presets, or in other words bundles of plugins. In this case, we're including _all_ of the React and ES6 transformations. If this were a production app we would definitely go through and only include the transformations we needed and _not_ all of them. For example, this includes a fairly large library to get ES6 generators working. Few people these days are actually using generators and thus it's better to leave them off if you don't need them. In our case, this is a teaching app so page weight isn't a big deal to us. This will also allow us to start using JSX.
45 |
46 | ## webpack loaders
47 |
48 | So, we could use the Babel CLI to compile our code but we are already using webpack and webpack has a good way to send your code through Babel via its loader mechanism. Loaders are utilities that webpack will pipe input into and accept output out of. You can use loaders to transpile things like CoffeeScript, TypeScript, or PureScript. Webpack loaders can do some other powerful things like allowing you to _include_ CSS, inline images via encoding, and transform SVGs. We're just going to be using the JS transformation abilities for now. Run the command webpack --module-bind 'js=babel' js/ClientApp.js public/bundle.js. Should take a bit longer since it's doing more. Since we're going to be using webpack for a few other things, let's abstract that configuration from inline to a config file. Add a `webpack.config.js` with the following.
49 |
50 | {% highlight javascript %}
51 | const path = require('path')
52 |
53 | module.exports = {
54 | context: __dirname,
55 | entry: './js/ClientApp.js',
56 | output: {
57 | path: path.join(__dirname, '/public'),
58 | filename: 'bundle.js'
59 | },
60 | resolve: {
61 | extensions: ['', '.js', '.jsx', '.json']
62 | },
63 | stats: {
64 | colors: true,
65 | reasons: true,
66 | chunks: false
67 | },
68 | module: {
69 | loaders: [
70 | {
71 | test: /\.jsx?$/,
72 | loader: 'babel-loader'
73 | }
74 | ]
75 | }
76 | }
77 |
78 | {% endhighlight %}
79 |
80 | What you see is essentially a config version of what we were doing with CLI flags. Now it's a bit more resilient and guaranteed consistent. If you run just the command webpack from your project directory you should get the same output. Neat. Go change you npm script to just be "webpack" now. Go try it and make sure it still works. Great! Also make sure all the files you've written are still up to snuff with standard via npm run test. Good? Good. This should get us to a point now where we can talk about JSX.
81 |
--------------------------------------------------------------------------------
/_posts/2016-01-11-react-testing.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Testing React with Mocha, Chai, and Enzyme"
3 | ---
4 |
5 | So now we have a component worth testing so let's do that. We're going to be using the old standbys [Mocha][mocha] and [Chai][chai]. These principles will generally apply to AVA or Jasmine too but I rolled with Mocha due to the fact that I've used it for so long that it's easy for me teach.
6 |
7 | We're also going to be using a testing helper from our friends at Airbnb called [Enzyme][enzyme]. Enzyme is just some helpers to make testing React components easier. You don't have to use it; the React testing utils are great too (and you can use both of them at the same time too.)
8 |
9 | So, first things first. Create a directory called test. In our test environment, we need a few things to happen. We need JSX/ES6 transpilation to happen or our tests will crash on the unfamiliar syntax. We also need a fake DOM for React to interact with which we'll get from a library called [jsdom][jsdom].
10 |
11 | So in your test directory, create another directory called helpers (these files are automatically excluded from being run as tests by Mocha) and create a file called setup.js. In setup.js, put
12 |
13 | {% highlight javascript %}
14 | require('babel-register')
15 | require('babel-polyfill')
16 |
17 | global.document = require('jsdom').jsdom('')
18 | global.window = document.defaultView
19 | global.navigator = window.navigator
20 | {% endhighlight %}
21 |
22 | This little setup is totally lifted from [Kent C. Dodds][kcd]. Thanks Kent.
23 |
24 | Now that we have our environment set up, let's get our tests set up as an npm script. Add the following to your scripts object in your package.json
25 |
26 | {% highlight json %}
27 | "test": "mocha --require test/helpers/setup.js",
28 | {% endhighlight %}
29 |
30 | You can add -R nyan to the end for fun too. This changes the reporter to be the [Nyan Cat][nyan] and I just can't help my self. There are other [reporters][reporters]. The --require part just makes sure that our setup gets run first before our specs too. I tend to leave it off since it doesn't report the tests that do pass and that's so satisfying for me
31 |
32 | Great! Let's start writing some tests. For this workshop we're just going to shove all our tests into one spec but I encourage you to do split them up into different files as appropriately split-up files. Create a new file called App.spec.js. The .spec.js convention is just to let your future self / other coders know that this file corresponds to the whole app. In this case it's not significant what it's called (the naming is significant in other testing frameworks.)
33 |
34 | In your new file, put:
35 |
36 | {% highlight javascript %}
37 | /* eslint-env mocha */
38 | const chai = require('chai')
39 | const { expect } = chai
40 |
41 | describe('console.log(wrapper.debug()). That will show you what it's dealing with. Let's add another more useful test.
70 |
71 | {% highlight javascript %}
72 | // add two more requires at the top
73 | const ShowCard = require('../js/ShowCard')
74 | const data = require('../public/data')
75 |
76 | // add another test inside the describe
77 | it('should render as many shows as there are data for', () => {
78 | const wrapper = shallow(<​script src="js/ClientApp.js"> so we can get some good code separation going. Once done with that, create the js directory and add the ClientApp.js file. In this file, let's put
49 |
50 | {% highlight javascript %}
51 | /* global React ReactDOM */
52 |
53 | var div = React.DOM.div
54 | var h1 = React.DOM.h1
55 |
56 | var MyTitle = React.createClass({
57 | render () {
58 | return (
59 | div(null,
60 | h1(null, 'Check out this component!')
61 | )
62 | )
63 | }
64 | })
65 |
66 | var MyFirstComponent = (
67 | div(null,
68 | React.createElement(MyTitle, null),
69 | React.createElement(MyTitle, null),
70 | React.createElement(MyTitle, null)
71 | )
72 | )
73 |
74 | ReactDOM.render(MyFirstComponent, document.getElementById('app'))
75 |
76 | {% endhighlight %}
77 |
78 | Cool, right!
79 |
80 | `MyTitle` is a `ReactComponent` class. This is a constructor function that, once invoked, is expected to return a JavaScript object with at least a `render` method on it.
81 |
82 | To invoke this constructor and create a new instance of the `MyTitle` component, you have to use the `React.createElement` method. The resulting instance is a `ReactElement`.
83 |
84 | We can use this element the same way we use any other HTML-native tag. This allows us to encapsulate style, behavior, and markup into one neat little package and reuse these components everywhere!
85 |
86 | To sum-up, we're using `createClass` to create the component and then we're using `createElement` to create an _instance_ of that class, resulting in an element that can be used in others components.
87 |
88 |
89 | ## Factories
90 |
91 | This is a bit verbose to write React.createElement so many damn times. Let's use a shortcut.
92 |
93 | {% highlight javascript %}
94 | // replace MyFirstComponent
95 | var MyTitleFact = React.createFactory(MyTitle)
96 |
97 | var MyFirstComponent = (
98 | div(null,
99 | MyTitleFact(null),
100 | MyTitleFact(null),
101 | MyTitleFact(null)
102 | )
103 | )
104 | {% endhighlight %}
105 |
106 | We can use createFactory to side step this. Now we can use our class like we use div. In fact React.createElement('div', null) works the same as React.DOM.div. The one I showed you is just a convenience factory method that React provides for all of the HTML tags.
107 |
108 | ## Props
109 |
110 | Our title component is cute but not super reuseable. Let's make it a bit more flexible by using some props. Props are variables that you pass from the parent to the child but the child cannot modify the props it gets. This simple restriction helps a lot in the future because when bugs arise, you know the child didn't modify the variable because it can't! Let's see how to do it.
111 |
112 | {% highlight javascript %}
113 | /* global React ReactDOM */
114 |
115 | var div = React.DOM.div
116 | var h1 = React.DOM.h1
117 |
118 | var MyTitle = React.createClass({
119 | render () {
120 | return (
121 | div(null,
122 | h1(null, this.props.title)
123 | )
124 | )
125 | }
126 | })
127 |
128 | var MyTitleFact = React.createFactory(MyTitle)
129 |
130 | var MyFirstComponent = (
131 | div(null,
132 | MyTitleFact({title: 'Props are great!'}),
133 | MyTitleFact({title: 'Use props everywhere!'}),
134 | MyTitleFact({title: 'Props are the best!'})
135 | )
136 | )
137 |
138 | ReactDOM.render(MyFirstComponent, document.getElementById('app'))
139 | {% endhighlight %}
140 |
141 | Now we can change the contents of the title. But since we can pass in lots of props, we can widely differing elements of the same class based on what props are passed into the element. Let's take it a step further (and show you how to do inline styles and attributes with React.)
142 |
143 | {% highlight javascript %}
144 | // change MyTitle's inside h1
145 | h1({ style: {color: this.props.color} }, this.props.title)
146 |
147 | // change MyFirstComponent inside div
148 | MyTitleFact({title: 'Props are great!', color: 'rebeccapurple'}),
149 | MyTitleFact({title: 'Use props everywhere!', color: 'mediumaquamarine'}),
150 | MyTitleFact({title: 'Props are the best!', color: 'peru'})
151 | {% endhighlight %}
152 |
153 | Let's stop there and switch our attention a bit to tooling. So far we've been writing React with no compile step which is pretty cool and not something enough do in the course of React. Certain things will just make sense because you know what it complies to. In any case, onward!
154 |
155 |
--------------------------------------------------------------------------------
/_posts/2016-01-19-universal-rendering.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Universal Rendering"
3 | ---
4 |
5 | Universal rendering, or the artist formerly known as isomorphic rendering. The idea here is that you server-side prerender your code so that when it gets down to the client, your browser can __instantly__ show the markup while your app bootstraps in the background. It makes everything feel very instantaneous.
6 |
7 | With just vanilla React, universal rendering is a cinch. Check out the [whole node file from another one of my workshops][es6-react]. It does server-side rendering in just a few lines.
8 |
9 | It's not quite so simple now that we have routing involved. We don't want to have to duplicate all of our routing info that we wrote for react-router. Rather, if possible, we just want to reuse the routes we already built for react-router. So let's do that (with some refactoring.)
10 |
11 | ## Move shows to redux
12 |
13 | We're passing shows everywhere. In order to simplify our app a lot by not having to worry about passing it in, let's make all the shows-based data just come from redux.
14 |
15 | Go to Store.jsx
16 |
17 | {% highlight javascript %}
18 | // another require
19 | const data = require('../public/data')
20 |
21 | // change initial state
22 | const initialState = {
23 | searchTerm: '',
24 | shows: data.shows
25 | }
26 |
27 | // add to mapStateToProps
28 | const mapStateToProps = (state) => ({ searchTerm: state.searchTerm, shows: data.shows })
29 | {% endhighlight %}
30 |
31 | Cool. Now these shows will available to any connected components. Let's go fix Details first.
32 |
33 | {% highlight javascript %}
34 | // bring in connector
35 | const Store = require('./Store')
36 | const { connector } = Store
37 |
38 | // pull show from redux
39 | const { title, description, year, poster, trailer } = this.props.shows[this.props.params.id]
40 |
41 | // add proptype
42 | shows: React.PropTypes.arrayOf(React.PropTypes.object)
43 |
44 | // connect component
45 | module.exports = connector(Details)
46 | {% endhighlight %}
47 |
48 | This should fix Details. Let's fix Search.
49 |
50 | {% highlight javascript %}
51 | // change the statement to map and filter
52 | {this.props.shows
53 |
54 | // overwrite the route proptype
55 | shows: React.PropTypes.arrayOf(React.PropTypes.object),
56 | {% endhighlight %}
57 |
58 | Just needed pull shows from a different part of the params. Not much else to change here. Now (while your tests and lint are failing at the moment) the app should still work but the shows are coming from redux. Let's go clean up ClientApp.jsx
59 |
60 | {% highlight javascript %}
61 | // remove data.json require
62 |
63 | // remove shows attribute from Search
64 |
65 | // remove assignShow onEnter from Details
66 |
67 | // remove assignShow method and constructor from App
68 | {% endhighlight %}
69 |
70 | Feels good to delete code. We successfully simplified our app a lot by moving that to redux. And now it will simplify us moving to render universally. Let's fix our test too.
71 |
72 | {% highlight javascript %}
73 | // change the Store init test's expect statement
74 | expect(state).to.deep.equal({ searchTerm: '', shows: data.shows })
75 | {% endhighlight %}
76 |
77 | All tests should pass now. Now we have a few more things to refactor before we can universally render successfully.
78 |
79 | ## Split out BrowserEntry.jsx
80 |
81 | The big key with universal rendering is being careful about referencing window and document as those aren't available in node environments. That isn't to say you can't interact with them: you just have to do if (window) { /* do stuff with window*/ }. Part of that means we need to split out the rendering of ClientApp from the declaration of the component. Remove the ReactDOM stuff from ClientApp, create a new called BrowserLanding.jsx and put this in there:
82 |
83 | {% highlight javascript %}
84 | const React = require('react')
85 | const ReactDOM = require('react-dom')
86 | const App = require('./ClientApp')
87 |
88 | ReactDOM.render(localhost:5050/#/search it's just going to be localhost:5050/search which we can do since we're doing server-side rendering and can control the routes.
92 |
93 | Go back to ClientApp.jsx
94 |
95 | {% highlight javascript %}
96 | // delete hashHistory from ReactRouter destructuring, add browserHistory
97 | const { Router, Route, IndexRoute, browserHistory } = ReactRouter
98 |
99 | // delete data.json require
100 |
101 | // change attribute of tree.prop = 'foo' doesn't work.) Rather, every time you want to modify the tree, you emit an __action__. Your action then kicks off what's called a __reducer__. A reducer is a special function that take a tree and parameter(s) and returns a new tree after applying whatever transformations it deems fit. The way it gets away with just one store is when you need more data you just add more branches to your data tree. Like React? You only have one tree of components and when you need more you just add more nodes (branches) to your components.
12 |
13 | So let's do the most basic addition of redux to our app and convert the Search to use redux. Again, this is using a sledgehammer to solve a tiny nail problem: huge overkill.
14 |
15 | Create Store.jsx. Because our use case is so small, I'm going to shove all our concerns in here. You should separate these out. But not today!
16 |
17 | {% highlight javascript %}
18 | const redux = require('redux')
19 | const reactRedux = require('react-redux')
20 |
21 | const SET_SEARCH_TERM = 'setSearchTerm'
22 |
23 | const reducer = (state = {searchTerm: ''}, action) => {
24 | switch (action.type) {
25 | case SET_SEARCH_TERM:
26 | const newState = {}
27 | Object.assign(newState, state, {searchTerm: action.value})
28 | return newState
29 | default:
30 | return state
31 | }
32 | }
33 |
34 | const store = redux.createStore(reducer)
35 |
36 | const mapStateToProps = (state) => ({ searchTerm: state.searchTerm })
37 | const mapDispatchToProps = (dispatch) => {
38 | return {
39 | setSearchTerm: (term) => {
40 | dispatch({type: SET_SEARCH_TERM, value: term})
41 | }
42 | }
43 | }
44 |
45 | const connector = reactRedux.connect(mapStateToProps, mapDispatchToProps)
46 |
47 | module.exports = { connector, store }
48 | {% endhighlight %}
49 |
50 | Here we're doing everything to bootstrap a redux store. You can see our root reducer function there. With the root reducer, you'll take in an action and then using a switch statement on the action type, you'll delegate that action to another reducer. If necessary, that reducer can delegate to yet another reducer and so on (I haven't had a compelling reason to do that yet.) Right now we don't have a reason to do so that we're just doing everything in the root reducer. You could change the body of the case SET_SEARCH_TERM: to be another reducer function.
51 |
52 | Then to create a new store you just give redux the root reducer and it's created! Cool!
53 |
54 | From there we're going to create a connector. react-redux is a library that provides some simple helpers to connect your redux store to your React app. Everything that react-redux does for you you can fairly easily do for yourself. For that matter, redux itself is a pretty simple library that would possible to write yourself (unlike React; good luck with that!) We're creating a mapStateToProps and a mapDispatchProps to props which are just helpers that will hand your pertinent components with the methods and state they'll need to be able to display it. Now whatever components your wrap with connector will have these bits of state and action creators available to them. We'll see how that works in a sec.
55 |
56 | Let's do some connecting with ClientApp.jsx
57 |
58 | {% highlight javascript %}
59 | // more requires
60 | const Store = require('./Store')
61 | const { store } = Store
62 | const reactRedux = require('react-redux')
63 | const { Provider } = reactRedux
64 |
65 | // change the ReactDOM render call
66 | ReactDOM.render(
67 | (
68 | Provider component makes our store and dispatches available where-ever we wrap components with connector calls, hence why it's necessary to wrap the whole app. Good news is once you do this it magically just works thought out your whole app. You'll only have to use once.
77 |
78 | Let's make it work with Header so it correctly displays and changes the redux store.
79 |
80 | {% highlight javascript %}
81 | // another require
82 | const Store = require('./Store')
83 | const { connector } = Store
84 |
85 | // wrap component
86 | module.exports = connector(Header)
87 |
88 | // two more methods for component
89 | constructor (props) {
90 | super(props)
91 |
92 | this.handleSearchTermChange = this.handleSearchTermChange.bind(this)
93 | }
94 | handleSearchTermChange (e) {
95 | this.props.setSearchTerm(e.target.value)
96 | }
97 |
98 | // change input
99 | utilSpace =
100 |
101 | // change propTypes
102 | Header.propTypes = {
103 | setSearchTerm: React.PropTypes.func,
104 | showSearch: React.PropTypes.bool,
105 | searchTerm: React.PropTypes.string
106 | }
107 | {% endhighlight %}
108 |
109 | Now you should be able to type in the header and see it immediately reflected. The search part doesn't work yet so let's go over to Search and make it reflected in the UI.
110 |
111 | {% highlight javascript %}
112 | // more requires
113 | const Store = require('./Store')
114 | const { connector } = Store
115 |
116 | // delete constructor
117 |
118 | // delete handleSearchTermChange method
119 |
120 | // change Header
121 | super(props). A necessary evil of boilerplate, I'm afraid. Anytime you have initial state of any sort you need to put that line in. Luckily if you forget, React has a friendly runtime error message to remind you.
110 |
111 | I replaced the brand momentarily so you can see the see the searchTerm change. You should see whatever you made the initial state for searchTerm show up as the brand. Neat, right? Alright, let's make it mutable now. Change the input in the header to be this:
112 |
113 | {% highlight javascript %}
114 |
115 | {% endhighlight %}
116 |
117 | Cool! Now your input should have the initial state of your searchTerm. Now try and type and/or delete anything. You can't! You broke the Internet! Just kidding. But to understand why this weird bug is happening you have to understand how React handles keypresses. Your state object on your component states that the value of searchTerm is 'this is the default searchTerm'. When a keypress happens, React kicks off a re-render. Since nothing modified the value of searchTerm, it's still the same string and thus it re-renders the same string there. Your state object is the source of truth. So let's make the value of searchTerm bound to the value of the input.
118 |
119 | {% highlight javascript %}
120 | class Search extends React.Component {
121 | constructor (props) {
122 | super(props)
123 |
124 | this.state = {
125 | searchTerm: 'this is the default searchTerm'
126 | }
127 |
128 | this.handleSearchTermChange = this.handleSearchTermChange.bind(this)
129 | }
130 | handleSearchTermChange (event) {
131 | this.setState({ searchTerm: event.target.value })
132 | }
133 | render () {
134 | return (
135 | this.setState, a method that allows you to mutate the state and then lets React re-render. If you don't call setState and instead mutate this.state yourself, React isn't privy to the fact the fact that you're changing stuff and thus doesn't know to re-render. In other words, never modify this.state directly and always just use setState. setState works like Object.assign in that it will do a merge of your objects (it's a shallow merge and not a deep merge) so you're free to just modify the keys you need to. Finally, in the constructor we added a line to bind the correct context of the event listener since we need to ensure that handleSearchTermChange is always called with the correct context.
152 |
153 | So go back now and change the brand to the correct title.
154 |
155 | Let's make the search actually _do_ something now. Since now we have our state being tracked, let's use it do a real time search on our titles.
156 |
157 | {% highlight javascript %}
158 | [stuff]/index.html#/details/1. You should see your new component here.
36 |
37 | Let's show you a neat debugging tip I totally stole from [Ryan Florence][ryflo]. replace that h1 with this:
38 |
39 | {% highlight javascript %}
40 | // instead of the h1 in render
41 |
42 | {JSON.stringify(this.props.params, null, 4)}
43 |
44 |
45 | // at the bottom to shut up lint
46 | Details.propTypes = {
47 | params: React.PropTypes.object
48 | }
49 | {% endhighlight %}
50 |
51 | This is a useful way to dump your params to the page. This is __awesome__ for state too; it shows you in real time what your state looks like. We'll dig into React Tools here in a sec for even better debugging but for now let's keep trucking with our Details.jsx
52 |
53 | We're going to show all the details of a show on this page and be able to play the trailer. There's a _big_ problem here that we don't have that data on this page though; it's available in the Search route. We _could_ require in data.json here given that our data is available that way but that typically isn't the case: we typically get this data from the server. If that's the case, you don't want to make two AJAX requests to get the same data. In other words, we need to share this state between components. The way you do this is by pushing up the state to the highest common ancestor component. In this case, that'd be the router in ClientApp. So let's first refactor Search to still work while it pulls in that data from Search.
54 |
55 | {% highlight javascript %}
56 | // another require
57 | const data = require('../public/data')
58 |
59 | // modify the existing route
60 | {description}
137 |{props.description}
265 |{show.description}
46 |{props.show.description}
70 | to . This will take all the properties from show and spread them out as individual properties on ShowCard. You _could_ write but that's a lot of writing and this cuts an easy corner. Let's go modify ShowCard to match.
125 |
126 | {% highlight javascript %}
127 | const React = require('react')
128 |
129 | const ShowCard = (props) => (
130 | {props.description}
136 |children is in particular an interesting beast. It allows you to make a tag and have access to whatever's inside. So:
195 |
196 | {% highlight html %}
197 |