├── .editorconfig ├── .gitignore ├── README.md ├── guide-1-react-router ├── .babelrc ├── README.md ├── app │ ├── app.js │ ├── components │ │ ├── home.js │ │ ├── main-layout.js │ │ ├── search-layout.js │ │ ├── user-list.js │ │ ├── user-profile.js │ │ └── widget-list.js │ └── router.js ├── gulpfile.js ├── index.html ├── package.json ├── public │ ├── css │ │ └── styles.css │ └── js │ │ ├── bundle.js │ │ └── bundle.map ├── server.js └── webpack.config.js ├── guide-2-container-components ├── .babelrc ├── README.md ├── app │ ├── api │ │ ├── user-api.js │ │ └── widget-api.js │ ├── app.js │ ├── components │ │ ├── containers │ │ │ ├── user-list-container.js │ │ │ ├── user-profile-container.js │ │ │ └── widget-list-container.js │ │ ├── home.js │ │ ├── layouts │ │ │ ├── main-layout.js │ │ │ └── search-layout.js │ │ └── views │ │ │ ├── user-list.js │ │ │ ├── user-profile.js │ │ │ └── widget-list.js │ └── router.js ├── data │ └── restore.json ├── docs │ └── state-and-props.md ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── css │ │ └── styles.css │ └── js │ │ ├── bundle.js │ │ └── bundle.map ├── server.js └── webpack.config.js └── guide-3-redux ├── .babelrc ├── README.md ├── app ├── actions │ ├── action-types.js │ ├── search-layout-actions.js │ ├── user-actions.js │ └── widget-actions.js ├── api │ ├── user-api.js │ └── widget-api.js ├── app.js ├── components │ ├── containers │ │ ├── search-form-container.js │ │ ├── search-layout-container.js │ │ ├── user-list-container.js │ │ ├── user-profile-container.js │ │ └── widget-list-container.js │ ├── home.js │ ├── layouts │ │ ├── main-layout.js │ │ └── search-layout.js │ └── views │ │ ├── search-form.js │ │ ├── user-list.js │ │ ├── user-profile.js │ │ └── widget-list.js ├── reducers │ ├── index.js │ ├── search-layout-reducer.js │ ├── user-reducer.js │ └── widget-reducer.js ├── router.js └── store.js ├── data └── restore.json ├── docs ├── action-strategies.md └── preview.gif ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json ├── public ├── css │ └── styles.css └── js │ ├── bundle.js │ └── bundle.map ├── server.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | db.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ** WARNING *** 2 | 3 | If you're looking at the articles I wrote at CSS-Tricks and subsequently this code, they are pretty old at this point. 4 | 5 | - React Router is currently on v5 and is going to be v6 soon (at the time I wrote this note in Oct 2019). I wrote a more updated article on v4 after this series at CSS-Tricks back in 2017: https://css-tricks.com/react-router-4/. FYI, v4 and v5 are practically the same API ([see why](https://reacttraining.com/blog/react-router-v5/)) so reading v4 is still pretty good if you're on v5 6 | - Reading on Container Components (Smart vs Dumb Components) might give you some good ideas but in general that pattern isn't very popular these days. 7 | - The Redux article is still pretty good and relevant. 8 | 9 | # CSS-Tricks: Leveling Up With React, A Three-Part Series 10 | 11 | This repo is for a [CSS-Tricks series on React](https://css-tricks.com/learning-react-router/). The documentation in this repo will show you many things that weren't shown in the series, such as 12 | 13 | - Steps for installing and running the code 14 | - An explanation of the Webpack and Babel setup 15 | - Extra Tips and Tricks 16 | - New ES6 Syntax 17 | 18 | ## Guide Documentation 19 | 20 | Each series comes with its own guide in this repo. Each guide will have a README file for its specific documentation: 21 | 22 | - [Guide 1: React Router](https://github.com/bradwestfall/CSS-Tricks-React-Series/tree/master/guide-1-react-router) 23 | - [Guide 2: Container Components](https://github.com/bradwestfall/CSS-Tricks-React-Series/tree/master/guide-2-container-components) 24 | - [Guide 3: Redux](https://css-tricks.com/learning-react-redux/) 25 | 26 | 27 | ## Installing and Running Code 28 | 29 | Each of the three guides needs to be npm-installed and ran separately. Start by cloning this repo and installing the first guide for React Router: 30 | 31 | ```sh 32 | cd path/to/guide-1-react-router 33 | npm install 34 | gulp 35 | ``` 36 | 37 | > The server will be available at localhost:3000 38 | 39 | To run the code from the other guides, `cd` to their folders and run the `npm` steps again from their folder. 40 | 41 | If you want to edit the React code, you'll have to re-build the `public/js/bundle.js` file with Webpack. You'll probably want to open a new terminal tab so you can keep your server running. To rebuild with Webpack, type: 42 | 43 | ```sh 44 | gulp watch 45 | ``` 46 | 47 | Also note that you'll need to [globally install Gulp](https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md) first if you haven't already 48 | 49 | 50 | # Implementation Details 51 | 52 | The articles at CSS-Tricks will be focused on their respective topics. They don't cover 100% of the concepts and implementation details of the code in the GitHub Guides. However, each guide will come with its own _README.md_ file that tries to cover some of the implementation details, especially for ES6 concepts. 53 | 54 | It should also be noted that the guides leave out many formalities like validation, security (XSS, CSRF) and organizational details to stay focused on the topics. These guides are trying to convey the "bigger picture" of how React _can_ work in a Single Page Application. They do not necessarily serve as a "best-practices" starting point. 55 | 56 | 57 | ### Server 58 | 59 | Each guide uses a very simple Express server which should take no configuration on your part to setup. The `gulp` step will launch the server so you can visit _localhost:3000_ in the browser to see the guide. Type `CTRL+C` to stop the server, and remember that only one guide can be ran at any given time since you'll `cd` to each guide and run its server separately. 60 | 61 | 62 | ## Webpack 63 | 64 | Webpack is a bundler that allows you to author multiple JavaScript files and have them bundled into one file for sending to the browser. If you're new to Webpack, here's a quick overview... 65 | 66 | Your project (or in this case, each guide at this repo) will have a `webpack.config.js` file. This file tells Webpack about which JavaScript file is your main entry point. That entry file will "include" other JavaScript files that it needs, which are it's "dependencies". In turn, those files can "include" even more dependencies. Webpack takes all the files in this process and bundles them into one output file. You can define where Wepback saves that file also in `webpack.config.js`. These guides will bundle their code to `/public/js/bundle.js`. 67 | 68 | The `bundle.js` file that Webpack creates will be the only JavaScript file sent to the browser. So the browser will start with all the JavaScript it needs for the application without making additional requests back to the server. 69 | 70 | Webpack allows multiple ways to indicate dependencies in JavaScript files. One way you'll see commonly online uses `require()` statements. That's a pattern called CommonJS. But more recently, JavaScript is in the process of adopting a new syntax called "ES6 modules" which use `include` statements. The browser doesn't understand either of these approaches, so Webpack will convert code written with CommonJS or ES6 modules into ES5 which the browser does understand. But Webpack will need a third party tool called Babel to use ES6 modules -- which is what these guides use for the app. 71 | 72 | If this all sounds chaotic and difficult to setup, don't worry, all the work is already done. All you need to do is run the `npm install` and then the `gulp` and/or `gulp watch` commands from the install instructions. 73 | 74 | Note that simply running `gulp` will launch the Node server whereas `gulp watch` takes care of the React/Webpack part. So you'll want to run these commands in two separate tabs if you want to have the server running and to be making React code changes. 75 | 76 | 77 | ## Babel 78 | 79 | Babel will tell Webpack how to convert ES6 (and even ES7) code to ES5. You might ask why we would want to write in future versions of JavaScript that aren't even fully supported? Well, there's new JavaScript syntax which is really nice to use. Plus, ES6 was finalized in 2015, which is why it's also called ES2015. So why should we have to wait for all browsers to catch up to a standard that's from 2015? 80 | 81 | Many React guides use ES6, so getting familiar with it will also help you learn React. Also note that the a common way to use Babel is to put it's list of desired "presets" in a `.babelrc` file. This is the strategy that we're using and this file is already created for you. 82 | 83 | 84 | # Extra Tips and Tricks 85 | 86 | ## JSX with Sublime 87 | 88 | If you use Sublime Text Editor, you may notice the JSX syntax highlighting is weird in `.js` files. That's because the JavaScript syntax highlighter isn't familiar with markup. You'll probably want to install the [babel-sublime](https://github.com/babel/babel-sublime) plugin which encourages you to use the _JavaScript (Babel)_ syntax for your files over the _JavaScript_ syntax. 89 | 90 | You might also notice that Emmet shortcuts don't work in JSX. Wes Bos wrote a [great guide](http://wesbos.com/emmet-react-jsx-sublime/) for setting that up. 91 | 92 | 93 | ## The multiple ways of creating components 94 | 95 | For myself, I prefer the `React.createClass` way over the `extends React.Component` way. Pete Hunt (former Facebook React team developer) [once wrote](https://github.com/petehunt/react-howto#learning-es6): 96 | 97 | > "You may see some talk about ES6 classes being the preferred way to create React components. This is untrue. Most people (including Facebook) are using React.createClass()." 98 | 99 | I'm not saying there's anything wrong with the ES6 way, I'm just saying you don't have to feel bad or behind if you do it the older `React.createClass` way. 100 | 101 | 102 | ## More to come... 103 | 104 | If you want to make more suggestions for this section that help beginners break through the hurdles, start a GitHub issue and perhaps we can add more tips here. 105 | -------------------------------------------------------------------------------- /guide-1-react-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /guide-1-react-router/README.md: -------------------------------------------------------------------------------- 1 | # Guide 1: React Router 2 | 3 | ## Installing and Running 4 | 5 | To start, make sure you're in the `guide-1-react-router` folder in command-line. 6 | 7 | ```sh 8 | # Install Node Modules 9 | npm install 10 | 11 | # Start the Server 12 | gulp 13 | 14 | # If you want to edit the react code, this rebuilds 15 | gulp watch 16 | ``` 17 | 18 | > The server will be available at localhost:3000 19 | 20 | If you want to edit the React code, you'll have to re-build the `public/js/bundle.js` file with Webpack. You'll probably want to open a new terminal tab so you can keep your server running. To rebuild with webpack, type: 21 | 22 | ```sh 23 | gulp watch 24 | ``` 25 | 26 | 27 | # Implementation Details 28 | 29 | Here are some details for this guide that weren't covered in the tutorial: 30 | 31 | 32 | ## Keeping it simple 33 | 34 | For simplicity, components in this guide are missing some formalities like [prop types](https://facebook.github.io/react/docs/reusable-components.html). I've tried to keep the components as simple as possible to get them working with the router. Since it's the minimal code to get the router working, the guide is missing many React best practices. 35 | 36 | 37 | ## ES6 Modules 38 | 39 | In the [CodePen](http://codepen.io/bradwestfall/pen/reaWYL) demo from the CSS-Tricks tutorial, we brought React and React Router into our application via CDNs. When a JavaScript tool is brought in through CDN, the tool globally available in the code, which is why the objects `React` and `ReactRouter` were available to us in the file. With bundlers like Webpack though, we are trying to avoid placing things in the global namespace. Each JavaScript module that we want to use (third-part or our own) will have to use `include` statements to get access to it. This is why you'll see this at the top of most the files in the `/app` folder: 40 | 41 | ```js 42 | include React from 'react' 43 | ``` 44 | 45 | Importing works similarly to CommonJS' `require()` statement which automatically looks for our module (`react`) inside of the `node_modules` folder. 46 | 47 | In the `app.js` file (the entry file for our application), you'll see this code: 48 | 49 | ``` 50 | import Router from './router'; 51 | ``` 52 | 53 | Since it's a relative path, the `import` won't look inside `node_modules`, but rather wherever our relative path points. The import statement is going to take whatever the `/router.js` file wants to export, and it will place that content (usually an object) in our `Router` variable. Then we can take the `Router` variable and mount it to the DOM `'root'`: 54 | 55 | ```js 56 | ReactDOM.render(Router, document.getElementById('root')); 57 | ``` 58 | 59 | This may seen different from the CSS-Tricks article, but it's actually the same. If you look at what the `/router.js` file is exporting, you'll see that it's a ``. Just think of this as a way to organize our code so not everything is in the `app.js` file. 60 | 61 | 62 | ## ES6 Destructuring 63 | 64 | [Destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) allows us to extract the intermal parts of an object into normal variables. So you may see lines of code like this: 65 | 66 | ```js 67 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'; 68 | ``` 69 | 70 | This is a combination of destructuring and imports. It's saying we want to import `react-router`, but instead of getting the `ReactRouter` object back, we want to extract certain properties from that object and create normal variables like `Router`, `Route` etc... 71 | 72 | ## Components 73 | 74 | Here is a sample of the `WidgetList` component. Notice it must `import` React to work. Also notice that it's creating a constant called `WidgetList` instead of using `var`. As you read more about these new ES6 ways of creating "variables", you'll see that we should only use `var` if we truly need something to _be_ variable -- in other words, something that will change over time. In our case, the reference to `WidgetList` (for this file) will _always_ be the same thing and is not something that incurs variable change. Therefore it is a constant. 75 | 76 | ```js 77 | import React from 'react'; 78 | 79 | const WidgetList = React.createClass({ 80 | render: function() { 81 | return ( 82 | 87 | ); 88 | } 89 | }); 90 | 91 | export default WidgetList; 92 | ``` 93 | 94 | ## Exports 95 | 96 | One of the more confusing parts about ES6 modules (to those familiar with `require()`) is the word `default`. With ES6 modules, each module can actually export more than one thing. Using `default` means that this `export` is the primary export of this file. It's more than just a formality though, using the `default` syntax also makes importing easier. With the `WidgetList` example from above, we are exporting `WidgetList` by default. This means that when we `import` WidgetList, we will get the exact same thing that was exported: 97 | 98 | ```js 99 | import WidgetList from './components/widget-list' 100 | ``` 101 | 102 | Had we not specified the `default` and wrote the code as `export WidgetList`, then the export would be an object with `WidgetList` inside of it. That means the `import` statement that receives it would have to look like this: 103 | 104 | ```js 105 | import SomeObject from './components/widget-list' 106 | const WidgetList = SomeObject.WidgetList 107 | ``` 108 | 109 | > I'm using "SomeObject" as a name just to show that it's obnoxious to receive the thing we want (`WidgetList`) wrapped in something else. 110 | 111 | Or, we could use destructuring syntax like this to unwrap the `WidgetList` into it's own variable: 112 | 113 | ```js 114 | import { WidgetList } from './components/widget-list' 115 | ``` 116 | 117 | With all these options, it's just easier to use the `default` way. 118 | 119 | If you look at all the components in Guide 1, you'll see that they all follow the same pattern of declaring a constant first, then exporting that constant as a `default`. However, it should also be understood that we can export the component as a `default` directly without creating a constant which does the exact same thing: 120 | 121 | ```js 122 | import React from 'react'; 123 | 124 | export default React.createClass({ 125 | render: function() { 126 | return ( 127 | 132 | ); 133 | } 134 | }); 135 | ``` 136 | 137 | This just saves us some keystrokes, but as stated before, the examples use the other way with the constant because it might be easier for beginners to ES6 modules to grasp. 138 | -------------------------------------------------------------------------------- /guide-1-react-router/app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | // Notice that we've organized all of our routes into a separate file. 5 | import Router from './router'; 6 | 7 | // Now we can attach the router to the 'root' element like this: 8 | ReactDOM.render(Router, document.getElementById('root')); 9 | -------------------------------------------------------------------------------- /guide-1-react-router/app/components/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |

The app has React Router

8 |

9 | While the CSS-Tricks article for 10 | this guide covers an explanation of React Router, there 11 | are still many implementation details in this code that the article 12 | doesn't cover. For a better understanding of those details, see 13 | the Github documentation for 14 | this guide. 15 |

16 |

17 | As far as the [Search Title] and [Total Results] that you'll see on the results page, 18 | those are static for now. We will make them dynamic in the third guide. 19 |

20 |
21 | ); 22 | } 23 | }); 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /guide-1-react-router/app/components/main-layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const MainLayout = React.createClass({ 5 | render: function() { 6 | return ( 7 |
8 |
9 | 16 |
17 | {this.props.children} 18 |
19 |
20 | ); 21 | } 22 | }); 23 | 24 | export default MainLayout; 25 | -------------------------------------------------------------------------------- /guide-1-react-router/app/components/search-layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SearchLayout = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |
8 | [Search Title] 9 |
10 |
11 | {this.props.children} 12 |
13 |
14 | [Total Results] 15 |
16 |
17 | ); 18 | } 19 | }); 20 | 21 | export default SearchLayout; 22 | -------------------------------------------------------------------------------- /guide-1-react-router/app/components/user-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const UserList = React.createClass({ 5 | render: function() { 6 | return ( 7 | 15 | ); 16 | } 17 | }); 18 | 19 | export default UserList; 20 | -------------------------------------------------------------------------------- /guide-1-react-router/app/components/user-profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserProfile = React.createClass({ 4 | render: function() { 5 | return (

User Profile for userId: {this.props.params.userId}

); 6 | } 7 | }); 8 | 9 | export default UserProfile; -------------------------------------------------------------------------------- /guide-1-react-router/app/components/widget-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const WidgetList = React.createClass({ 4 | render: function() { 5 | return ( 6 | 11 | ); 12 | } 13 | }); 14 | 15 | export default WidgetList; 16 | -------------------------------------------------------------------------------- /guide-1-react-router/app/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'; 3 | 4 | // Layouts 5 | import MainLayout from './components/main-layout'; 6 | import SearchLayout from './components/search-layout'; 7 | 8 | // Pages 9 | import Home from './components/home'; 10 | import UserList from './components/user-list'; 11 | import UserProfile from './components/user-profile'; 12 | import WidgetList from './components/widget-list'; 13 | 14 | export default ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /guide-1-react-router/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var del = require('del'); 3 | var webpack = require('webpack-stream'); 4 | var webpackConfig = require('./webpack.config.js'); 5 | var nodemon = require('gulp-nodemon'); 6 | var path = require('path'); 7 | 8 | 9 | /** 10 | * Build (Webpack) 11 | */ 12 | 13 | gulp.task('clean:build', function() { 14 | del('./public/js/*') 15 | }) 16 | 17 | gulp.task('build', ['clean:build'], function() { 18 | return gulp.src('./app/app.js') 19 | .pipe(webpack(webpackConfig)) 20 | .on('error', function handleError() { 21 | this.emit('end'); // Recover from errors 22 | }) 23 | .pipe(gulp.dest('./')); 24 | }); 25 | 26 | gulp.task('watch:build', function() { 27 | return gulp.watch('./app/**/*', ['build']); 28 | }); 29 | 30 | 31 | /** 32 | * Node Server (Express) 33 | */ 34 | 35 | gulp.task('serve:node', function(done) { 36 | nodemon({ 37 | exec: 'node ./node_modules/babel-cli/bin/babel-node.js ./server.js', 38 | watch: ['server.js'], 39 | ext: 'js html' 40 | }); 41 | }); 42 | 43 | 44 | /** 45 | * Main tasks 46 | */ 47 | 48 | gulp.task('serve', ['serve:node']); 49 | gulp.task('watch', ['build', 'watch:build']); 50 | gulp.task('default', ['serve']); 51 | -------------------------------------------------------------------------------- /guide-1-react-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Router Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /guide-1-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-guide", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "start": "gulp" 7 | }, 8 | "author": "Brad Westfall ", 9 | "license": "ISC", 10 | "dependencies": { 11 | "express": "^4.13.4", 12 | "react": "0.14.7", 13 | "react-dom": "0.14.7", 14 | "react-router": "2.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-cli": "^6.5.1", 18 | "babel-core": "^6.5.2", 19 | "babel-loader": "^6.2.4", 20 | "babel-preset-es2015": "^6.5.0", 21 | "babel-preset-react": "^6.5.0", 22 | "babel-preset-stage-2": "^6.5.0", 23 | "del": "^2.2.0", 24 | "gulp": "^3.9.1", 25 | "gulp-nodemon": "^2.0.6", 26 | "gulp-rename": "^1.2.2", 27 | "webpack": "^1.12.14", 28 | "webpack-stream": "^3.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /guide-1-react-router/public/css/styles.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | } 4 | 5 | aside.primary-aside { 6 | width: 200px; 7 | } 8 | 9 | aside.primary-aside a.active { 10 | font-weight: bold; 11 | } 12 | 13 | main { 14 | flex: 1; 15 | } -------------------------------------------------------------------------------- /guide-1-react-router/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is just a dummy server to facilidate our React SPA examples. 3 | * For a more professional setup of Express, see... 4 | * http://expressjs.com/en/starter/generator.html 5 | */ 6 | 7 | import express from 'express'; 8 | import path from 'path'; 9 | const app = express(); 10 | 11 | 12 | /** 13 | * Anything in public can be accessed statically without 14 | * this express router getting involved 15 | */ 16 | 17 | app.use(express.static(path.join(__dirname, 'public'), { 18 | dotfiles: 'ignore', 19 | index: false 20 | })); 21 | 22 | 23 | /** 24 | * Always serve the same HTML file for all requests 25 | */ 26 | 27 | app.get('*', function(req, res, next) { 28 | console.log('Request: [GET]', req.originalUrl) 29 | res.sendFile(path.resolve(__dirname, 'index.html')); 30 | }); 31 | 32 | 33 | /** 34 | * Error Handling 35 | */ 36 | 37 | app.use(function(req, res, next) { 38 | console.log('404') 39 | let err = new Error('Not Found'); 40 | err.status = 404; 41 | next(err); 42 | }); 43 | 44 | app.use(function(err, req, res, next) { 45 | res.sendStatus(err.status || 500); 46 | }); 47 | 48 | 49 | /** 50 | * Start Server 51 | */ 52 | 53 | const port = 3000; 54 | app.listen(port); 55 | 56 | console.log('Visit: localhost:' + port); -------------------------------------------------------------------------------- /guide-1-react-router/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./app/app.js", 5 | output: { 6 | filename: "public/js/bundle.js", 7 | sourceMapFilename: "public/js/bundle.map" 8 | }, 9 | devtool: '#source-map', 10 | module: { 11 | loaders: [ 12 | { 13 | loader: 'babel', 14 | exclude: /node_modules/ 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /guide-2-container-components/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /guide-2-container-components/README.md: -------------------------------------------------------------------------------- 1 | # Guide 2: Container Components 2 | 3 | ## Installing and Running 4 | 5 | To start, make sure you're in the `guide-2-container-components` folder in command-line. 6 | 7 | ```sh 8 | # Install Node Modules 9 | npm install 10 | 11 | # Start the Server 12 | gulp 13 | 14 | # If you want to edit the react code, this rebuilds 15 | gulp watch 16 | ``` 17 | 18 | > The server will be available at localhost:3000 19 | 20 | If you want to edit the React code, you'll have to re-build the `public/js/bundle.js` file with Webpack. You'll probably want to open a new terminal tab so you can keep your server running. To rebuild with Webpack, type: 21 | 22 | ```sh 23 | gulp watch 24 | ``` 25 | 26 | # Learning the code 27 | 28 | If you jump into the code, I would advise looking at `Widgets` before `Users`. There's much less code to look at for `Widgets`. The `Users` code is similar to `Widgets`, but there's more with profiles. 29 | 30 | 31 | # Implementation Details 32 | 33 | Here are some details for this guide that weren't covered in the tutorial: 34 | 35 | 36 | ## JSON Server 37 | 38 | For Guide 2 and 3, we will use __JSON Server__ to give us the feel of having a real database. It will need to run on a different port from our Node server though, so it runs on _localhost:3001_. 39 | 40 | Launching the Node server with `gulp` now also launches JSON Server. 41 | 42 | They have [great documentation](https://github.com/typicode/json-server) if you want to learn more about how it works, but in short, they create a RESTful API for us to `GET`, `POST`, `PUT`, and `DELETE` to. In this guide, we can use those HTTP verbs on the `/users` path as follows: 43 | 44 | A `GET` request to _localhost:3001/users_ will return a JSON array which resembles: 45 | 46 | ``` 47 | [ 48 | { 49 | "id": 3, 50 | "name": "Dan Abramov", 51 | "github": "gaearon", 52 | "twitter": "dan_abramov", 53 | "worksOn": "Redux" 54 | }, 55 | 56 | ... 57 | 58 | ] 59 | ``` 60 | 61 | A `DELETE` request to _localhost:3001/users/3_ will delete the record where `id:3`. 62 | 63 | Since I knew that you might mess with the data (like a few deletes), I made it so each time you restart the server with the `gulp` command, the original database data will be restored - so delete away! 64 | 65 | 66 | ## Organization 67 | 68 | The `/app/components` folder is now organized by: 69 | 70 | - containers 71 | - layouts 72 | - views 73 | 74 | This was just the simplest way to organize this small codebase. I make no claims that this is amazing organization :) 75 | 76 | 77 | ## Search Layout 78 | 79 | The main purpose of the Search Layout component was to convey nested layouts in the first tutorial. It doesn't yet serve us any in the Container Components tutorial to utilize it. Therefore, it just has some static information which is not yet hooked up to state. In the third guide, we will make this information more meaningful. 80 | 81 | 82 | ## Axios 83 | 84 | As discussed in the tutorial, we use [axios](https://github.com/mzabriskie/axios) for our Ajax (XHR) requests. However, the components don't make XHR requests directly from their `componentDidMount()` methods as the tutorial showed. Instead, all database API requests exist in the `/app/api` folder. The `componentDidMount()` methods will use those outside files for XHR requests. This just helps keep the component size down and helps them to look cleaner. 85 | 86 | 87 | ## ES6 Arrow Functions 88 | 89 | ES6 arrow functions are very popular in React tutorials online. While the CSS-Tricks tutorial doesn't use ES6 features, the code at this guide will. Here's a brief explanation of how they work: 90 | 91 | ```js 92 | // Old way with ES5 93 | componentDidMount: function() { 94 | userApi.getList().then(function(users) { 95 | this.setState({users: users}); 96 | }); 97 | }, 98 | 99 | // New way with ES6 Arrow Functions 100 | componentDidMount: function() { 101 | userApi.getList().then(users => { 102 | this.setState({users: users}); 103 | }); 104 | } 105 | ``` 106 | 107 | Here's another example with Axios promises: 108 | 109 | ```js 110 | // Old way with ES5 111 | export function getList() { 112 | return axios.get('http://localhost:3001/users') 113 | .then(function(response) { 114 | return response.data; 115 | }); 116 | } 117 | 118 | 119 | // New way with ES6 Arrow Functions 120 | export function getList() { 121 | return axios.get('http://localhost:3001/users') 122 | .then(response => response.data); 123 | } 124 | ``` 125 | 126 | Are arrow functions just syntax sugar for less typing? No, they actually have different rules for scope which can sometimes be beneficial for callback functions. For this guide, we'll only use them for callback functions so you can get used to them in small doses. 127 | 128 | If you're interested in learning more, [I wrote a blog post](http://bradwestfall.com/articles/dont-get-javascript-es6-arrow-functions). 129 | 130 | 131 | ## ES6 Spread Operator 132 | 133 | ES6 now has a [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator). React borrowed the idea for passing attributes into components. To understand this new feature, consider the following scenarios... 134 | 135 | Imagine we wanted to pass an object from parent component to child component: 136 | 137 | ```js 138 | // Parent Component's render method 139 | render: function() { 140 | const user = { 141 | name: 'Brad', 142 | occupation: 'Web Development', 143 | state: 'Arizona' 144 | }; 145 | 146 | return (); 147 | } 148 | ``` 149 | 150 | This works but the child component must access the name by doing `this.props.user.name`. It might be nicer to just be able to type `this.props.name`. But in order to have that option, we would have to itemize and list each property when we pass them into the child component: 151 | 152 | ```js 153 | // Parent Component's render method 154 | render: function() { 155 | const user = { 156 | name: 'Brad', 157 | occupation: 'Web Development', 158 | state: 'Arizona' 159 | }; 160 | 161 | return (); 162 | } 163 | ``` 164 | 165 | Now, the child component can do `this.props.name`. This is nicer for the child component, but it's obnoxious to have to list out each property. 166 | 167 | ### Spread Attributes to the rescue! 168 | 169 | With React's [Spread Attributes](https://facebook.github.io/react/docs/jsx-spread.html#spread-attributes), we can do this: 170 | 171 | ```js 172 | // Parent Component's render method 173 | render: function() { 174 | const user = { 175 | name: 'Brad', 176 | occupation: 'Web Development', 177 | state: 'Arizona' 178 | }; 179 | 180 | return (); 181 | } 182 | ``` 183 | 184 | This is a nice way to write code for the parent and the child gets to access the props like this: `this.props.name`, `this.props.occupation` and `this.props.state`. 185 | 186 | In the guide, you can see this behavior on the [`user-profile-container.js`](https://github.com/bradwestfall/CSS-Tricks-React-Series/blob/master/guide-2-container-components/app/components/containers/user-profile-container.js#L32) file. 187 | 188 | ## Delete Strategy 189 | 190 | In the CSS-Tricks tutorial, we showed how [events can be passed from Container Components down to Presentational Components](https://css-tricks.com/learning-react-container-components/#article-header-id-6). Thinks are slightly more complex in this guide though. We have a new problem to solve that wasn't covered well in the tutorial. 191 | 192 | The problem is that sometimes functions like `deleteUser()` need to be called with an argument. In this case it's the `userId`. The `onClick` can't _call_ the `deleteUser()` method with the argument right away. That would lead to the `deleteUser()` method getting called as soon as the page loads for all the users. Instead, it needs to ensure that _when_ the `onClick` happens, to call the function with an argument. For that we'll use `.bind()`. 193 | 194 | #### .bind() 195 | 196 | This is how we'll indicate that when the `onClick` event happens, to call `deleteUser()` and pass the correct `user.id` as the first argument: 197 | 198 | ```js 199 | {props.users.map(user => { 200 | return ( 201 | 202 | ); 203 | })} 204 | ``` 205 | 206 | #### Updating the user list after removal 207 | 208 | In the XHR callbacks to delete `Users` and `Widgets`, the code makes a copy of the state, then updates and replaces the state with the copy. We do this so our state is "immutable". This is a topic that's covered in the third CSS-Tricks article on Redux. 209 | 210 | ```js 211 | deleteUser: function(userId) { 212 | userApi.deleteUser(userId).then(() => { 213 | const newUsers = _.filter(this.state.users, user => user.id != userId); 214 | this.setState({users: newUsers}) 215 | }); 216 | } 217 | ``` 218 | 219 | Note that [lodash](https://lodash.com/) is being used to filter the current state by making a copy of it with all users that don't match the ID. The copy without the matched user will replace the state. 220 | -------------------------------------------------------------------------------- /guide-2-container-components/app/api/user-api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /** 4 | * Get users 5 | */ 6 | 7 | export function getUsers() { 8 | return axios.get('http://localhost:3001/users') 9 | .then(response => response.data); 10 | } 11 | 12 | /** 13 | * Delete a user 14 | */ 15 | 16 | export function deleteUser(userId) { 17 | return axios.delete('http://localhost:3001/users/' + userId); 18 | } 19 | 20 | /** 21 | * getProfile() is much more complex because it has to make 22 | * three XHR requests to get all the profile info. 23 | */ 24 | 25 | export function getProfile(userId) { 26 | 27 | // Start with an empty profile object and build it up 28 | // from multiple XHR requests. 29 | let profile = {}; 30 | 31 | // Get the user data from our local database. 32 | return axios.get('http://localhost:3001/users/' + userId) 33 | .then(response => { 34 | 35 | let user = response.data; 36 | profile.name = user.name; 37 | profile.twitter = user.twitter; 38 | profile.worksOn = user.worksOn; 39 | 40 | // Then use the github attribute from the previous request to 41 | // sent two XHR requests to GitHub's API. The first for their 42 | // general user info, and the second for their repos. 43 | return Promise.all([ 44 | axios.get('https://api.github.com/users/' + user.github), 45 | axios.get('https://api.github.com/users/' + user.github + '/repos') 46 | ]).then(results => { 47 | 48 | let githubProfile = results[0].data; 49 | let githubRepos = results[1].data; 50 | 51 | profile.imageUrl = githubProfile.avatar_url; 52 | profile.repos = githubRepos; 53 | 54 | return profile; 55 | 56 | }); 57 | 58 | }); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /guide-2-container-components/app/api/widget-api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /** 4 | * Get widgets 5 | */ 6 | 7 | export function getWidgets() { 8 | return axios.get('http://localhost:3001/widgets') 9 | .then(response => response.data); 10 | } 11 | 12 | /** 13 | * Delete a widget 14 | */ 15 | 16 | export function deleteWidget(widgetId) { 17 | return axios.delete('http://localhost:3001/widgets/' + widgetId); 18 | } 19 | -------------------------------------------------------------------------------- /guide-2-container-components/app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | require('es6-promise').polyfill(); 4 | 5 | // Notice that we've organized all of our routes into a separate file. 6 | import Router from './router'; 7 | 8 | // Now we can attach the router to the 'root' element like this: 9 | ReactDOM.render(Router, document.getElementById('root')); 10 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/containers/user-list-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import UserList from '../views/user-list'; 4 | import * as userApi from '../../api/user-api'; 5 | 6 | const UserListContainer = React.createClass({ 7 | 8 | getInitialState: function() { 9 | return { 10 | users: [] 11 | } 12 | }, 13 | 14 | componentDidMount: function() { 15 | userApi.getUsers().then(users => { 16 | this.setState({users: users}) 17 | }); 18 | }, 19 | 20 | deleteUser: function(userId) { 21 | userApi.deleteUser(userId).then(() => { 22 | const newUsers = _.filter(this.state.users, user => user.id != userId); 23 | this.setState({users: newUsers}) 24 | }); 25 | }, 26 | 27 | render: function() { 28 | return ( 29 | 30 | ); 31 | } 32 | 33 | }); 34 | 35 | export default UserListContainer; 36 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/containers/user-profile-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserProfile from '../views/user-profile'; 3 | import * as userApi from '../../api/user-api'; 4 | 5 | const UserProfileContainer = React.createClass({ 6 | 7 | getInitialState: function() { 8 | return { 9 | name: null, 10 | imageUrl: null, 11 | twitter: null, 12 | worksOn: null, 13 | repos: [] 14 | } 15 | }, 16 | 17 | componentDidMount: function() { 18 | let userId = this.props.params.userId 19 | userApi.getProfile(userId).then(profile => { 20 | this.setState({ 21 | name: profile.name, 22 | imageUrl: profile.imageUrl, 23 | twitter: profile.twitter, 24 | worksOn: profile.worksOn, 25 | repos: profile.repos 26 | }); 27 | }); 28 | }, 29 | 30 | render: function() { 31 | return ( 32 | 33 | ); 34 | } 35 | 36 | }); 37 | 38 | export default UserProfileContainer; 39 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/containers/widget-list-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import WidgetList from '../views/widget-list'; 4 | import * as widgetApi from '../../api/widget-api'; 5 | 6 | const WidgetListContainer = React.createClass({ 7 | 8 | getInitialState: function() { 9 | return { 10 | widgets: [] 11 | } 12 | }, 13 | 14 | componentDidMount: function() { 15 | widgetApi.getWidgets().then(widgets => { 16 | this.setState({widgets: widgets}) 17 | }); 18 | }, 19 | 20 | deleteWidget: function(widgetId) { 21 | widgetApi.deleteWidget(widgetId).then(() => { 22 | const newWidgets = _.filter(this.state.widgets, widget => widget.id != widgetId); 23 | this.setState({widgets: newWidgets}) 24 | }); 25 | }, 26 | 27 | render: function() { 28 | return ( 29 | 30 | ); 31 | } 32 | 33 | }); 34 | 35 | export default WidgetListContainer; 36 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |

The app now has Container Components

8 |

9 | While the CSS-Tricks article for 10 | this guide covers an explanation of Container Components, there 11 | are still many implementation details in this code that the article 12 | doesn't cover. For a better understanding of those details, see 13 | the Github documentation for 14 | this guide. 15 |

16 |

17 | As far as the [Search Title] and [Total Results] that you'll see on the results page, 18 | those are static for now. We will make them dynamic in the third guide. 19 |

20 |
21 | ); 22 | } 23 | }); 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/layouts/main-layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 |
9 | 16 |
17 | {props.children} 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/layouts/search-layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Using "Stateless Functional Components" 4 | export default function(props) { 5 | return ( 6 |
7 |
8 | [Search Title] 9 |
10 |
11 | {props.children} 12 |
13 |
14 | [Total Results] 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/views/user-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 | 9 | {props.users.map(user => { 10 | 11 | return ( 12 |
13 |
14 | {user.name} 15 |
16 |
17 | 18 |
19 |
20 | ); 21 | 22 | })} 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/views/user-profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Using "Stateless Functional Components" 4 | export default function(props) { 5 | return ( 6 |
7 | 8 |
9 |

{props.name}

10 | @{props.twitter} 11 |

Works on {props.worksOn}

12 |

Github Repos:

13 |
    14 | 15 | {props.repos.map(repo => { 16 | 17 | return (
  • {repo.name}
  • ); 18 | 19 | })} 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /guide-2-container-components/app/components/views/widget-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 | 9 | {props.widgets.map(widget => { 10 | 11 | return ( 12 |
13 |
{widget.name}
14 |
15 | 16 |
17 |
18 | ); 19 | 20 | })} 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /guide-2-container-components/app/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'; 3 | 4 | // Layouts 5 | import MainLayout from './components/layouts/main-layout'; 6 | import SearchLayout from './components/layouts/search-layout'; 7 | 8 | // Pages 9 | import Home from './components/home'; 10 | import UserListContainer from './components/containers/user-list-container'; 11 | import UserProfileContainer from './components/containers/user-profile-container'; 12 | import WidgetListContainer from './components/containers/widget-list-container'; 13 | 14 | export default ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /guide-2-container-components/data/restore.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "name": "Michael Jackson", 6 | "github": "mjackson", 7 | "twitter": "mjackson", 8 | "worksOn": "React Router" 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Ryan Florence", 13 | "github": "ryanflorence", 14 | "twitter": "ryanflorence", 15 | "worksOn": "React Router" 16 | }, 17 | { 18 | "id": 3, 19 | "name": "Dan Abramov", 20 | "github": "gaearon", 21 | "twitter": "dan_abramov", 22 | "worksOn": "Redux" 23 | }, 24 | { 25 | "id": 4, 26 | "name": "Matt Zabriskie", 27 | "github": "mzabriskie", 28 | "twitter": "mzabriskie", 29 | "worksOn": "Axios" 30 | }, 31 | { 32 | "id": 5, 33 | "name": "Tobias Koppers", 34 | "github": "sokra", 35 | "worksOn": "Webpack" 36 | }, 37 | { 38 | "id": 6, 39 | "name": "Sebastian McKenzie", 40 | "github": "kittens", 41 | "twitter": "sebmck", 42 | "worksOn": "Babel" 43 | } 44 | ], 45 | "widgets": [ 46 | { 47 | "id": 1, 48 | "name": "Widget One" 49 | }, 50 | { 51 | "id": 2, 52 | "name": "Widget Two" 53 | }, 54 | { 55 | "id": 3, 56 | "name": "Widget Three" 57 | }, 58 | { 59 | "id": 4, 60 | "name": "Widget Four" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /guide-2-container-components/docs/state-and-props.md: -------------------------------------------------------------------------------- 1 | # Beginner guide to State and Props 2 | 3 | In the tutorial, we made reference to this code: 4 | 5 | ```js 6 | var UserList = React.createClass({ 7 | getInitialState: function() { 8 | return { 9 | users: [] 10 | } 11 | }, 12 | 13 | componentDidMount: function() { 14 | var _this = this; 15 | $.get('/path/to/user-api').then(function(response) { 16 | _this.setState({users: response}) 17 | }); 18 | }, 19 | 20 | render: function() { 21 | return ( 22 |
    23 | {this.state.users.map(function(user) { 24 | return ( 25 |
  • 26 | {user.name} 27 |
  • 28 | ); 29 | })} 30 |
31 | ); 32 | } 33 | }); 34 | ``` 35 | 36 | This guide will talk you through each piece so you can better understand it. 37 | 38 | ## State 39 | 40 | State is just a term for where we can store data that is going to change over time. The `getInitialState()` method just needs to return an Object Literal that contains our initial state. In our case, it's an empty users array. It's important to know that this method is only invoked once when the component is used. 41 | 42 | Technically, the `componentDidMount()` method happens next. But it just fires off an Ajax request which is asynchronous so `componentDidMount()` method doesn't do much to change the state when it's called initially. 43 | 44 | Then the `render` method is called and it loops over the `state.users` array which is still empty. No list items are created at this time. 45 | 46 | Then a moment later, the Ajax call returns which `componentDidMount()` started. That will call a callback function which will set the state of the users array to be filled with the response of the Ajax call. 47 | 48 | The cool part is, whenever state changes in a component, the component will automatically re-render. So even though React called `render` once already when the users array was empty, now it's called again, only this time the `users` array has data. 49 | 50 | For more information on these types of "Lifecycle" concepts, see the [React Documentation](https://facebook.github.io/react/docs/component-specs.html) 51 | -------------------------------------------------------------------------------- /guide-2-container-components/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var cp = require('child_process'); 3 | var del = require('del'); 4 | var webpack = require('webpack-stream'); 5 | var webpackConfig = require('./webpack.config.js'); 6 | var nodemon = require('gulp-nodemon'); 7 | var rename = require('gulp-rename'); 8 | 9 | 10 | /** 11 | * Build (Webpack) 12 | */ 13 | 14 | gulp.task('clean:build', function() { 15 | del('./public/js/*') 16 | }) 17 | 18 | gulp.task('build', ['clean:build'], function() { 19 | return gulp.src('./app/app.js') 20 | .pipe(webpack(webpackConfig)) 21 | .on('error', function handleError() { 22 | this.emit('end'); // Recover from errors 23 | }) 24 | .pipe(gulp.dest('./')); 25 | }); 26 | 27 | gulp.task('watch:build', function() { 28 | return gulp.watch('./app/**/*', ['build']); 29 | }); 30 | 31 | 32 | /** 33 | * API Server (Database) 34 | */ 35 | 36 | gulp.task('restore-database', function() { 37 | return gulp.src('./data/restore.json') 38 | .pipe(rename('db.json')) 39 | .pipe(gulp.dest('./data')); 40 | }); 41 | 42 | gulp.task('serve:api', ['restore-database'], function(done) { 43 | cp.exec('node ./node_modules/json-server/bin/index.js --watch ./data/db.json --port 3001', {stdio: 'inherit'}) 44 | .on('close', done); 45 | }); 46 | 47 | 48 | /** 49 | * Node Server (Express) 50 | */ 51 | 52 | gulp.task('serve:node', function(done) { 53 | nodemon({ 54 | exec: 'node ./node_modules/babel-cli/bin/babel-node.js ./server.js', 55 | watch: ['server.js'], 56 | ext: 'js html' 57 | }); 58 | }); 59 | 60 | 61 | /** 62 | * Main tasks 63 | */ 64 | 65 | gulp.task('serve', ['serve:node', 'serve:api']); 66 | gulp.task('watch', ['build', 'watch:build']); 67 | gulp.task('default', ['serve']); 68 | -------------------------------------------------------------------------------- /guide-2-container-components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Container Components Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /guide-2-container-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-guide", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "start": "gulp" 7 | }, 8 | "author": "Brad Westfall ", 9 | "license": "ISC", 10 | "dependencies": { 11 | "express": "^4.13.4", 12 | "json-server": "^0.8.8", 13 | "lodash": "^4.6.1", 14 | "react": "0.14.7", 15 | "react-dom": "0.14.7", 16 | "react-router": "2.0.0" 17 | }, 18 | "devDependencies": { 19 | "axios": "^0.9.1", 20 | "babel-cli": "^6.5.1", 21 | "babel-core": "^6.5.2", 22 | "babel-loader": "^6.2.4", 23 | "babel-preset-es2015": "^6.5.0", 24 | "babel-preset-react": "^6.5.0", 25 | "babel-preset-stage-2": "^6.5.0", 26 | "del": "^2.2.0", 27 | "es6-promise": "^4.0.5", 28 | "gulp": "^3.9.1", 29 | "gulp-nodemon": "^2.0.6", 30 | "gulp-rename": "^1.2.2", 31 | "webpack": "^1.12.14", 32 | "webpack-stream": "^3.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /guide-2-container-components/public/css/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | color: #222; 4 | font: 12pt Arial; 5 | } 6 | 7 | *, *::before, *::after { 8 | box-sizing: inherit; 9 | color: inherit; 10 | } 11 | 12 | 13 | 14 | /**************************************** 15 | Elements 16 | *****************************************/ 17 | 18 | body { 19 | margin: 3rem; 20 | } 21 | 22 | a { 23 | color: #2297D1; 24 | text-decoration: none; 25 | cursor: pointer; 26 | } 27 | 28 | * > h1 { 29 | margin-top: 0; 30 | } 31 | 32 | 33 | /**************************************** 34 | Components 35 | *****************************************/ 36 | 37 | 38 | /** 39 | * Button 40 | */ 41 | 42 | button { 43 | border: none; 44 | border-radius: 3px; 45 | background-color: #2297D1; 46 | font-size: 0.7em; 47 | padding: 0.5em 1em; 48 | cursor: pointer; 49 | color: #fff; 50 | } 51 | 52 | button.delete { 53 | background-color: #B83B3B; 54 | } 55 | 56 | 57 | /** 58 | * Data List 59 | */ 60 | 61 | .data-list-item { 62 | padding: 0.6em; 63 | display: flex; 64 | border-bottom: 1px solid #bbb; 65 | } 66 | 67 | .data-list-item .details { 68 | flex: 1; 69 | } 70 | 71 | 72 | /** 73 | * User Profile 74 | */ 75 | 76 | .user-profile { 77 | display: flex; 78 | } 79 | 80 | .user-profile img { 81 | display: block; 82 | width: 100px; 83 | height: 100px; 84 | } 85 | 86 | .user-profile .details { 87 | flex: 1; 88 | margin-left: 1em; 89 | } 90 | 91 | .user-profile h1 { 92 | margin: 0; 93 | } 94 | 95 | .user-profile .repos { 96 | overflow: hidden; 97 | list-style: none; 98 | padding: 0; 99 | } 100 | 101 | .user-profile .repos li { 102 | float: left; 103 | width: 250px; 104 | margin-right: 2em; 105 | white-space: nowrap; 106 | overflow: hidden; 107 | text-overflow: ellipsis; 108 | } 109 | 110 | 111 | /**************************************** 112 | Layout 113 | *****************************************/ 114 | 115 | /** 116 | * Main Layout 117 | */ 118 | 119 | .app { 120 | display: flex; 121 | min-width: 800px; 122 | } 123 | 124 | .primary-aside { 125 | width: 12rem; 126 | } 127 | 128 | .primary-aside ul { 129 | list-style: none; 130 | margin: 0; 131 | } 132 | 133 | .primary-aside a { 134 | display: block; 135 | padding: 0.6em; 136 | } 137 | 138 | .primary-aside a.active { 139 | color: #fff; 140 | background-color: #2297D1; 141 | } 142 | 143 | main { 144 | flex: 1; 145 | margin-left: 2em; 146 | } 147 | 148 | /** 149 | * Search Layout 150 | */ 151 | 152 | .search-header, .search-footer { 153 | font-weight: bold; 154 | } 155 | 156 | .search-results { 157 | margin: 1em 0; 158 | } 159 | -------------------------------------------------------------------------------- /guide-2-container-components/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is just a dummy server to facilidate our React SPA examples. 3 | * For a more professional setup of Express, see... 4 | * http://expressjs.com/en/starter/generator.html 5 | */ 6 | 7 | import express from 'express'; 8 | import path from 'path'; 9 | const app = express(); 10 | 11 | 12 | /** 13 | * Anything in public can be accessed statically without 14 | * this express router getting involved 15 | */ 16 | 17 | app.use(express.static(path.join(__dirname, 'public'), { 18 | dotfiles: 'ignore', 19 | index: false 20 | })); 21 | 22 | 23 | /** 24 | * Always serve the same HTML file for all requests 25 | */ 26 | 27 | app.get('*', function(req, res, next) { 28 | console.log('Request: [GET]', req.originalUrl) 29 | res.sendFile(path.resolve(__dirname, 'index.html')); 30 | }); 31 | 32 | 33 | /** 34 | * Error Handling 35 | */ 36 | 37 | app.use(function(req, res, next) { 38 | console.log('404') 39 | let err = new Error('Not Found'); 40 | err.status = 404; 41 | next(err); 42 | }); 43 | 44 | app.use(function(err, req, res, next) { 45 | res.sendStatus(err.status || 500); 46 | }); 47 | 48 | 49 | /** 50 | * Start Server 51 | */ 52 | 53 | const port = 3000; 54 | app.listen(port); 55 | 56 | console.log('Serving: localhost:' + port); -------------------------------------------------------------------------------- /guide-2-container-components/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./app/app.js", 5 | output: { 6 | filename: "public/js/bundle.js", 7 | sourceMapFilename: "public/js/bundle.map" 8 | }, 9 | devtool: '#source-map', 10 | module: { 11 | loaders: [ 12 | { 13 | loader: 'babel', 14 | exclude: /node_modules/ 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /guide-3-redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"], 3 | "plugins": ["transform-object-assign"] 4 | } 5 | -------------------------------------------------------------------------------- /guide-3-redux/README.md: -------------------------------------------------------------------------------- 1 | # Guide 3: Redux 2 | 3 | ## Final Guide 4 | 5 | This is the final Guide in this series. Our code builds a Single Page Application which resembles this functionality: 6 | 7 | ![Final Preview](https://raw.githubusercontent.com/bradwestfall/CSS-Tricks-React-Series/master/guide-3-redux/docs/preview.gif) 8 | 9 | ## Installing and Running 10 | 11 | To start, make sure you're in the `guide-3-redux` folder in command-line. 12 | 13 | ```sh 14 | # Install Node Modules 15 | npm install 16 | 17 | # Start the Server 18 | gulp 19 | 20 | # If you want to edit the react code, this rebuilds 21 | gulp watch 22 | ``` 23 | 24 | > The server will be available at localhost:3000 25 | 26 | If you want to edit the React code, you'll have to re-build the `public/js/bundle.js` file with Webpack. You'll probably want to open a new terminal tab so you can keep your server running. To rebuild with Webpack, type: 27 | 28 | ```sh 29 | gulp watch 30 | ``` 31 | 32 | # Implementation Details 33 | 34 | If you're coming to this guide for the first time and haven't looked at Guide 1 or 2, be sure to look at their README files for implementation details that led to guide 3. 35 | 36 | Here are some details for this guide that weren't covered in the tutorial: 37 | 38 | ## Organization 39 | 40 | The `/app` folder now has a folder for `/actions` and `reducers`. 41 | 42 | ### Action Creators and Action Type Constants 43 | 44 | The `/actions` folder now contains action creators and action type constants as per some of the strategies discussed in [this guide](https://github.com/bradwestfall/CSS-Tricks-React-Series/blob/master/guide-3-redux/docs/action-strategies.md). 45 | 46 | ### Reducers and Immutable State 47 | 48 | Immutable state was discussed in the article. To accomplish this, you'll notice the `Object.assign()` usage to create object copies and also the use of [lodash](https://lodash.com/) as a utility to filter through arrays of objects (sometimes called collections). 49 | 50 | The [`user-reducer.js` and `widget-reducer.js`](https://github.com/bradwestfall/CSS-Tricks-React-Series/tree/master/guide-3-redux/app/reducers) files each use a lodash filter like this: 51 | 52 | ```js 53 | const newWidgets = _.filter(state.widgets, widget => widget.id != action.widgetId); 54 | ``` 55 | 56 | The use of [arrow functions might look confusing](http://bradwestfall.com/articles/dont-get-javascript-es6-arrow-functions) at first. Without arrow functions, it would look like this: 57 | 58 | ```js 59 | const newWidgets = _.filter(state.widgets, function(widget) { 60 | return widget.id != action.widgetId 61 | }); 62 | ``` 63 | 64 | The `_.filter` method of lodash looks in an array of objects and returns a new version of the array based on the rules of the filter. In our case, we're looking to return the full list except for where the `widget.id` matches the one we're trying to remove. 65 | 66 | 67 | # API Methods 68 | 69 | The [api methods](https://github.com/bradwestfall/CSS-Tricks-React-Series/tree/master/guide-3-redux/app/api) used by the components now perform an additional task of dispatching actions: 70 | 71 | ```js 72 | export function getUsers() { 73 | return axios.get('http://localhost:3001/users') 74 | .then(response => { 75 | store.dispatch(getUsersSuccess(response.data)); 76 | return response; 77 | }); 78 | } 79 | ``` 80 | 81 | # User Component 82 | 83 | The `user-list-container.js` and `widget-list-container.js` are probably some of the more interesting file changes from guide-2 to guide-3. There's a lot of new things going on: 84 | 85 | ```diff 86 | import React from 'react'; 87 | + import { connect } from 'react-redux'; 88 | - import _ from 'lodash'; 89 | import UserList from '../views/user-list'; 90 | import * as userApi from '../../api/user-api'; 91 | + import store from '../../store'; 92 | + import { loadSearchLayout } from '../../actions/search-layout-actions'; 93 | 94 | const UserListContainer = React.createClass({ 95 | 96 | - getInitialState: function() { 97 | - return { 98 | - users: [] 99 | - } 100 | - }, 101 | 102 | componentDidMount: function() { 103 | - userApi.getUsers().then(users => { 104 | - this.setState({users: users}) 105 | - }); 106 | + userApi.getUsers(); 107 | + store.dispatch(loadSearchLayout('users', 'User Results')); 108 | }, 109 | 110 | - deleteUser: function(userId) { 111 | - userApi.deleteUser(userId).then(() => { 112 | - const newUsers = _.filter(this.state.users, user => user.id != userId); 113 | - this.setState({users: newUsers}) 114 | - }); 115 | - }, 116 | 117 | render: function() { 118 | return ( 119 | - 120 | + 121 | ); 122 | } 123 | 124 | }); 125 | 126 | + const mapStateToProps = function(store) { 127 | + return { 128 | + users: store.userState.users 129 | + }; 130 | + }; 131 | 132 | - export default UserListContainer; 133 | + export default connect(mapStateToProps)(UserListContainer); 134 | ``` 135 | 136 | The overall strategy reflects a paradigm change from each component being in charge of it's own state, to each component "connecting" to Redux and "dispatching" it's state changes. Take note that this component doesn't even alert itself directly when state changes. The new call to `userApi.getUser()` simply doesn't concern itself about when the response comes back. Instead, that method will dispatch the state to Redux and this component will know about the change the same way any other component in the application can find out, by subscribing. However, when using `react-redux` and the `connect()` method, the `mapStateToProps()` is our way of subscribing instead of Redux's `.subscribe()` method. 137 | 138 | This code is also dispatching some static information to the `search-layout.js` component: 139 | 140 | ```js 141 | store.dispatch(loadSearchLayout('users', 'User Results')) 142 | ``` 143 | 144 | This way the search layout can better represent users and widgets. 145 | 146 | 147 | # Search Layout 148 | 149 | In guide-2, the search-layout was only a view. But now it needs to coordinate with state so it has a Container Component. Unlike some of the other Container Components, it's a perfect example of us not needing to make our own Container Component to wrap `react-redux` around: 150 | 151 | ```js 152 | import React from 'react'; 153 | import { connect } from 'react-redux'; 154 | import SearchLayout from '../layouts/search-layout'; 155 | 156 | const mapStateToProps = function(store) { 157 | 158 | let searchType = store.searchLayoutState.searchType; 159 | let totalResults = 0; 160 | 161 | if (searchType === 'users') { 162 | totalResults = store.userState.users.length; 163 | } else if (searchType === 'widgets') { 164 | totalResults = store.widgetState.widgets.length; 165 | } 166 | 167 | return { 168 | searchType, 169 | title: store.searchLayoutState.title, 170 | totalResults 171 | }; 172 | 173 | }; 174 | 175 | export default connect(mapStateToProps)(SearchLayout); 176 | ``` 177 | 178 | This `mapStateToProps()` function is also a great example of how we can convert specific parts of state into the exact props that we need. 179 | -------------------------------------------------------------------------------- /guide-3-redux/app/actions/action-types.js: -------------------------------------------------------------------------------- 1 | // Users 2 | export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS'; 3 | export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS'; 4 | export const USER_PROFILE_SUCCESS = 'USER_PROFILE_SUCCESS'; 5 | 6 | // Widgets 7 | export const GET_WIDGETS_SUCCESS = 'GET_WIDGETS_SUCCESS'; 8 | export const DELETE_WIDGET_SUCCESS = 'DELETE_WIDGET_SUCCESS'; 9 | 10 | // Search Layout 11 | export const LOAD_SEARCH_LAYOUT = 'LOAD_SEARCH_LAYOUT'; 12 | -------------------------------------------------------------------------------- /guide-3-redux/app/actions/search-layout-actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | export function loadSearchLayout(searchType, title) { 4 | return { 5 | type: types.LOAD_SEARCH_LAYOUT, 6 | searchType, 7 | title 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /guide-3-redux/app/actions/user-actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | export function getUsersSuccess(users) { 4 | return { 5 | type: types.GET_USERS_SUCCESS, 6 | users 7 | }; 8 | } 9 | 10 | export function deleteUserSuccess(userId) { 11 | return { 12 | type: types.DELETE_USER_SUCCESS, 13 | userId 14 | }; 15 | } 16 | 17 | export function userProfileSuccess(userProfile) { 18 | return { 19 | type: types.USER_PROFILE_SUCCESS, 20 | userProfile 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /guide-3-redux/app/actions/widget-actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | export function getWidgetsSuccess(widgets) { 4 | return { 5 | type: types.GET_WIDGETS_SUCCESS, 6 | widgets 7 | }; 8 | } 9 | 10 | export function deleteWidgetSuccess(widgetId) { 11 | return { 12 | type: types.DELETE_WIDGET_SUCCESS, 13 | widgetId 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /guide-3-redux/app/api/user-api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import store from '../store'; 3 | import { getUsersSuccess, deleteUserSuccess, userProfileSuccess } from '../actions/user-actions'; 4 | 5 | /** 6 | * Get all users 7 | */ 8 | 9 | export function getUsers() { 10 | return axios.get('http://localhost:3001/users') 11 | .then(response => { 12 | store.dispatch(getUsersSuccess(response.data)); 13 | return response; 14 | }); 15 | } 16 | 17 | /** 18 | * Search users 19 | */ 20 | 21 | export function searchUsers(query = '') { 22 | return axios.get('http://localhost:3001/users?q='+ query) 23 | .then(response => { 24 | store.dispatch(getUsersSuccess(response.data)); 25 | return response; 26 | }); 27 | } 28 | 29 | /** 30 | * Delete a user 31 | */ 32 | 33 | export function deleteUser(userId) { 34 | return axios.delete('http://localhost:3001/users/' + userId) 35 | .then(response => { 36 | store.dispatch(deleteUserSuccess(userId)); 37 | return response; 38 | }); 39 | } 40 | 41 | /** 42 | * getProfile() is much more complex because it has to make 43 | * three XHR requests to get all the profile info. 44 | */ 45 | 46 | export function getProfile(userId) { 47 | 48 | // Start with an empty profile object and build it up 49 | // from multiple XHR requests. 50 | let profile = {}; 51 | 52 | // Get the user data from our local database. 53 | return axios.get('http://localhost:3001/users/' + userId) 54 | .then(response => { 55 | 56 | let user = response.data; 57 | profile.name = user.name; 58 | profile.twitter = user.twitter; 59 | profile.worksOn = user.worksOn; 60 | 61 | // Then use the github attribute from the previous request to 62 | // sent two XHR requests to GitHub's API. The first for their 63 | // general user info, and the second for their repos. 64 | return Promise.all([ 65 | axios.get('https://api.github.com/users/' + user.github), 66 | axios.get('https://api.github.com/users/' + user.github + '/repos') 67 | ]).then(results => { 68 | 69 | let githubProfile = results[0].data; 70 | let githubRepos = results[1].data; 71 | 72 | profile.imageUrl = githubProfile.avatar_url; 73 | profile.repos = githubRepos; 74 | 75 | store.dispatch(userProfileSuccess(profile)); 76 | 77 | return; 78 | 79 | }); 80 | 81 | }); 82 | 83 | } 84 | -------------------------------------------------------------------------------- /guide-3-redux/app/api/widget-api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import store from '../store'; 3 | import { getWidgetsSuccess, deleteWidgetSuccess } from '../actions/widget-actions'; 4 | 5 | /** 6 | * Get widgets 7 | */ 8 | 9 | export function getWidgets() { 10 | return axios.get('http://localhost:3001/widgets') 11 | .then(response => { 12 | store.dispatch(getWidgetsSuccess(response.data)); 13 | return response; 14 | }); 15 | } 16 | 17 | /** 18 | * Search Widgets 19 | */ 20 | 21 | export function searchWidgets(query = '') { 22 | return axios.get('http://localhost:3001/widgets?q='+ query) 23 | .then(response => { 24 | store.dispatch(getWidgetsSuccess(response.data)); 25 | return response; 26 | }); 27 | } 28 | 29 | /** 30 | * Delete a widget 31 | */ 32 | 33 | export function deleteWidget(widgetId) { 34 | return axios.delete('http://localhost:3001/widgets/' + widgetId) 35 | .then(response => { 36 | store.dispatch(deleteWidgetSuccess(widgetId)); 37 | return response; 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /guide-3-redux/app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from './store'; 5 | import router from './router'; 6 | require('es6-promise').polyfill(); 7 | 8 | // Provider is a top-level component that wrapps our entire application, including 9 | // the Router. We pass it a reference to the store so we can use react-redux's 10 | // connect() method for Component Containers. 11 | ReactDOM.render( 12 | {router}, 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/containers/search-form-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as userApi from '../../api/user-api'; 3 | import * as widgetApi from '../../api/widget-api'; 4 | import { loadSearchLayout } from '../../actions/search-layout-actions'; 5 | import SearchForm from '../views/search-form'; 6 | 7 | const SearchFormContainer = React.createClass({ 8 | 9 | search: function(event) { 10 | event.preventDefault(); 11 | 12 | // By assigning a "child" ref to , we 13 | // can use that reference to gain access to the 14 | // .getQuery() method. See the code for 15 | // to see how it returns a value. 16 | let query = this.refs.child.getQuery(); 17 | 18 | if (this.props.searchType === 'users') { 19 | userApi.searchUsers(query); 20 | } else if (this.props.searchType === 'widgets') { 21 | widgetApi.searchWidgets(query); 22 | } 23 | }, 24 | 25 | render: function() { 26 | return ( 27 | 28 | ); 29 | } 30 | 31 | }); 32 | 33 | export default SearchFormContainer; 34 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/containers/search-layout-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import SearchLayout from '../layouts/search-layout'; 4 | 5 | const mapStateToProps = function(store) { 6 | 7 | let searchType = store.searchLayoutState.searchType; 8 | let totalResults = 0; 9 | 10 | if (searchType === 'users') { 11 | totalResults = store.userState.users.length; 12 | } else if (searchType === 'widgets') { 13 | totalResults = store.widgetState.widgets.length; 14 | } 15 | 16 | return { 17 | searchType, 18 | title: store.searchLayoutState.title, 19 | totalResults 20 | }; 21 | 22 | }; 23 | 24 | export default connect(mapStateToProps)(SearchLayout); 25 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/containers/user-list-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import UserList from '../views/user-list'; 4 | import * as userApi from '../../api/user-api'; 5 | import store from '../../store'; 6 | import { loadSearchLayout } from '../../actions/search-layout-actions'; 7 | 8 | const UserListContainer = React.createClass({ 9 | 10 | componentDidMount: function() { 11 | userApi.getUsers(); 12 | store.dispatch(loadSearchLayout('users', 'User Results')); 13 | }, 14 | 15 | render: function() { 16 | return ( 17 | 18 | ); 19 | } 20 | 21 | }); 22 | 23 | const mapStateToProps = function(store) { 24 | return { 25 | users: store.userState.users 26 | }; 27 | }; 28 | 29 | export default connect(mapStateToProps)(UserListContainer); 30 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/containers/user-profile-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import UserProfile from '../views/user-profile'; 4 | import * as userApi from '../../api/user-api'; 5 | 6 | const UserProfileContainer = React.createClass({ 7 | 8 | componentDidMount: function() { 9 | let userId = this.props.params.userId 10 | userApi.getProfile(userId) 11 | }, 12 | 13 | render: function() { 14 | return ( 15 | 16 | ); 17 | } 18 | 19 | }); 20 | 21 | const mapStateToProps = function(store) { 22 | return { 23 | profile: store.userState.userProfile 24 | }; 25 | }; 26 | 27 | export default connect(mapStateToProps)(UserProfileContainer); 28 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/containers/widget-list-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import WidgetList from '../views/widget-list'; 4 | import * as widgetApi from '../../api/widget-api'; 5 | import store from '../../store'; 6 | import { loadSearchLayout } from '../../actions/search-layout-actions'; 7 | 8 | const WidgetListContainer = React.createClass({ 9 | 10 | componentDidMount: function() { 11 | widgetApi.getWidgets(); 12 | store.dispatch(loadSearchLayout('widgets', 'Widget Results')); 13 | }, 14 | 15 | render: function() { 16 | return ( 17 | 18 | ); 19 | } 20 | 21 | }); 22 | 23 | const mapStateToProps = function(store) { 24 | return { 25 | widgets: store.widgetState.widgets 26 | }; 27 | }; 28 | 29 | export default connect(mapStateToProps)(WidgetListContainer); 30 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |

The app is now using Redux

8 |

9 | While the CSS-Tricks article for 10 | this guide covers an explanation of Redux, there 11 | are still many implementation details in this code that the article 12 | doesn't cover. For a better understanding of those details, see 13 | the Github documentation for 14 | this guide. 15 |

16 |
17 | ); 18 | } 19 | }); 20 | 21 | export default Home; 22 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/layouts/main-layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 |
9 | 16 |
17 | {props.children} 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/layouts/search-layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchFormContainer from '../containers/search-form-container'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 |
9 | {props.title} 10 | 11 |
12 |
13 | {props.children} 14 |
15 |
16 | {props.totalResults} Results 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/views/search-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | 5 | getQuery: function() { 6 | return this.refs.search.value; 7 | }, 8 | 9 | render: function() { 10 | return ( 11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/views/user-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 | 9 | {props.users.map(user => { 10 | 11 | return ( 12 |
13 |
14 | {user.name} 15 |
16 |
17 | 18 |
19 |
20 | ); 21 | 22 | })} 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/views/user-profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Using "Stateless Functional Components" 4 | export default function(props) { 5 | return ( 6 |
7 | 8 |
9 |

{props.name}

10 | @{props.twitter} 11 |

Works on {props.worksOn}

12 |

Github Repos:

13 |
    14 | 15 | {props.repos.map(repo => { 16 | 17 | return (
  • {repo.name}
  • ); 18 | 19 | })} 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /guide-3-redux/app/components/views/widget-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | // Using "Stateless Functional Components" 5 | export default function(props) { 6 | return ( 7 |
8 | 9 | {props.widgets.map(widget => { 10 | 11 | return ( 12 |
13 |
{widget.name}
14 |
15 | 16 |
17 |
18 | ); 19 | 20 | })} 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /guide-3-redux/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | // Reducers 4 | import userReducer from './user-reducer'; 5 | import widgetReducer from './widget-reducer'; 6 | import searchLayoutReducer from './search-layout-reducer'; 7 | 8 | // Combine Reducers 9 | var reducers = combineReducers({ 10 | userState: userReducer, 11 | widgetState: widgetReducer, 12 | searchLayoutState: searchLayoutReducer 13 | }); 14 | 15 | export default reducers; 16 | -------------------------------------------------------------------------------- /guide-3-redux/app/reducers/search-layout-reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | const initialState = { 4 | searchType: '', 5 | title: '' 6 | }; 7 | 8 | const searchLayoutReducer = function(state = initialState, action) { 9 | 10 | switch(action.type) { 11 | 12 | case types.LOAD_SEARCH_LAYOUT: 13 | return Object.assign({}, state, { 14 | searchType: action.searchType, 15 | title: action.title 16 | }); 17 | 18 | } 19 | 20 | return state; 21 | 22 | } 23 | 24 | export default searchLayoutReducer; 25 | -------------------------------------------------------------------------------- /guide-3-redux/app/reducers/user-reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | import _ from 'lodash'; 3 | 4 | const initialState = { 5 | users: [], 6 | userProfile: { 7 | repos: [] 8 | } 9 | }; 10 | 11 | const userReducer = function(state = initialState, action) { 12 | 13 | switch(action.type) { 14 | 15 | case types.GET_USERS_SUCCESS: 16 | return Object.assign({}, state, { users: action.users }); 17 | 18 | case types.DELETE_USER_SUCCESS: 19 | 20 | // Use lodash to create a new user array without the user we want to remove 21 | const newUsers = _.filter(state.users, user => user.id != action.userId); 22 | return Object.assign({}, state, { users: newUsers }); 23 | 24 | case types.USER_PROFILE_SUCCESS: 25 | return Object.assign({}, state, { userProfile: action.userProfile }); 26 | 27 | } 28 | 29 | return state; 30 | 31 | } 32 | 33 | export default userReducer; 34 | -------------------------------------------------------------------------------- /guide-3-redux/app/reducers/widget-reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | import _ from 'lodash'; 3 | 4 | const initialState = { 5 | widgets: [] 6 | }; 7 | 8 | const widgetReducer = function(state = initialState, action) { 9 | 10 | switch(action.type) { 11 | 12 | case types.GET_WIDGETS_SUCCESS: 13 | return Object.assign({}, state, { widgets: action.widgets }); 14 | 15 | case types.DELETE_WIDGET_SUCCESS: 16 | 17 | // Use lodash to create a new widget array without the widget we want to remove 18 | const newWidgets = _.filter(state.widgets, widget => widget.id != action.widgetId); 19 | return Object.assign({}, state, { widgets: newWidgets }) 20 | 21 | } 22 | 23 | return state; 24 | 25 | } 26 | 27 | export default widgetReducer; 28 | -------------------------------------------------------------------------------- /guide-3-redux/app/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'; 3 | 4 | // Layouts 5 | import MainLayout from './components/layouts/main-layout'; 6 | import SearchLayoutContainer from './components/containers/search-layout-container'; 7 | 8 | // Pages 9 | import Home from './components/home'; 10 | import UserListContainer from './components/containers/user-list-container'; 11 | import UserProfileContainer from './components/containers/user-profile-container'; 12 | import WidgetListContainer from './components/containers/widget-list-container'; 13 | 14 | export default ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /guide-3-redux/app/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import reducers from './reducers'; 3 | 4 | const store = createStore(reducers); 5 | export default store; 6 | -------------------------------------------------------------------------------- /guide-3-redux/data/restore.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "name": "Michael Jackson", 6 | "github": "mjackson", 7 | "twitter": "mjackson", 8 | "worksOn": "React Router" 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Ryan Florence", 13 | "github": "ryanflorence", 14 | "twitter": "ryanflorence", 15 | "worksOn": "React Router" 16 | }, 17 | { 18 | "id": 3, 19 | "name": "Dan Abramov", 20 | "github": "gaearon", 21 | "twitter": "dan_abramov", 22 | "worksOn": "Redux" 23 | }, 24 | { 25 | "id": 4, 26 | "name": "Matt Zabriskie", 27 | "github": "mzabriskie", 28 | "twitter": "mzabriskie", 29 | "worksOn": "Axios" 30 | }, 31 | { 32 | "id": 5, 33 | "name": "Tobias Koppers", 34 | "github": "sokra", 35 | "worksOn": "Webpack" 36 | }, 37 | { 38 | "id": 6, 39 | "name": "Sebastian McKenzie", 40 | "github": "kittens", 41 | "twitter": "sebmck", 42 | "worksOn": "Babel" 43 | } 44 | ], 45 | "widgets": [ 46 | { 47 | "id": 1, 48 | "name": "Widget One" 49 | }, 50 | { 51 | "id": 2, 52 | "name": "Widget Two" 53 | }, 54 | { 55 | "id": 3, 56 | "name": "Widget Three" 57 | }, 58 | { 59 | "id": 4, 60 | "name": "Widget Four" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /guide-3-redux/docs/action-strategies.md: -------------------------------------------------------------------------------- 1 | # Action Strategies 2 | 3 | [The article](https://css-tricks.com/learning-react-redux/) covers actions to an extent, but there's a lot more to consider. As stated before, actions are plain objects. Redux is mostly unopinionated about what types of things should or can go into the action, as long as it has a `type` attribute and as long as it remains [serializable](http://redux.js.org/docs/Glossary.html#action). 4 | 5 | Here are some examples of actions from Redux's documentation: 6 | 7 | ``` 8 | { type: 'ADD_TODO', text: 'Use Redux' } 9 | { type: 'REMOVE_TODO', id: 42 } 10 | { type: 'LOAD_ARTICLE', response: { ... } } 11 | ``` 12 | 13 | String literals are just one common way to distinguish one type from another. They are not required but this is the more conventional way distinguish action types. It's important that they are globally unique though. Remember, all reducers will receive the action when it's dispatched so there's no way to conduct a dispatch that goes to one particular reducer. Therefore, having an action type called `"ADD"` is too ambiguous. 14 | 15 | To help ensure uniqueness, many developers make "action types" as constants: 16 | 17 | ```js 18 | const ADD_TODO = 'ADD_TODO'; 19 | const REMOVE_TODO = 'REMOVE_TODO'; 20 | const LOAD_ARTICLE = 'LOAD_ARTICLE'; 21 | 22 | store.dispatch({ type: ADD_TODO, text: 'Use Redux' }); 23 | store.dispatch({ type: REMOVE_TODO, id: 42 }); 24 | store.dispatch({ type: LOAD_ARTICLE, response: { ... } }); 25 | ``` 26 | 27 | > ES2015 Alert! Simply having `const` doesn't mean it's value is ensured to be unique. Using `const` just means its value can't change. But as a strategy, some devs create one file with all their action type constants inside which helps to see them all in one place, and to visually ensure they are unique. 28 | 29 | Some strategies regarding actions seem tedious with lots of boilerplate code. Redux Docs offer a suggestion for [reducing boilerplate code](http://redux.js.org/docs/recipes/ReducingBoilerplate.html). One strategy it offers in addition to using constants is to use "action creators". 30 | 31 | ### Action Creators 32 | 33 | Instead of dispatching an object literal directly and repeating the same action object throughout the application, you could call a function that creates and returns the action object: 34 | 35 | ```js 36 | const ADD_TODO = 'ADD_TODO'; 37 | 38 | // Action Creator 39 | var addTodo = function(text) { 40 | return { type: ADD_TODO, text } 41 | } 42 | 43 | // Dispatch still sends a plain object 44 | store.dispatch(addTodo('Use Redux')); 45 | ``` 46 | 47 | This function is called an "action creator". Now you don't have to remember the name of the action type constant. Plus if you needed to change the type constant, this strategy allows us to be more [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). 48 | 49 | Arguably though, this is even more boilerplate since your application is going to potentially have many action type constants and many action creators. 50 | 51 | > ES2015 Alert! You may have noticed what seems to be a typo in the example. The action object has a `text` property but no value is being applied. This is not an error. In ES2015, this is called a [shorthand property name](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#New_notations_in_ECMAScript_2015). Having `text` all by itself is simultaneously creating a property called `text` and adding the value of the `text` variable to it. 52 | 53 | ## Reduce Boilerplate Code with `redux-act` 54 | 55 | If you're not liking all the boilerplate hassle that can go into creating action type constants and action creators, consider using [redux-act](https://github.com/pauldijou/redux-act), a third party tool that abstracts the making of action creators and action types so you don't even have to think about them. Its documentation can speak for itself, but I personally use this tool and I love it. 56 | 57 | ## Flux Standard Action (FSA) 58 | 59 | Since the rules around creating actions are so loose, the [Flux Standard Action](https://github.com/acdlite/flux-standard-action) was created to be a set of rules around how to create actions. They are totally optional, but `redux-act` does require them. 60 | 61 | ## Serializable Actions and State 62 | 63 | Actions and state should only contain data which is "serializable", in other words, it converts to JSON well. This isn't a strict rule but it's highly recommended by the Redux docs. 64 | -------------------------------------------------------------------------------- /guide-3-redux/docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradwestfall/CSS-Tricks-React-Series/21e5100832ac4ddf262f9bc386c72024801a900c/guide-3-redux/docs/preview.gif -------------------------------------------------------------------------------- /guide-3-redux/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var cp = require('child_process'); 3 | var del = require('del'); 4 | var webpack = require('webpack-stream'); 5 | var webpackConfig = require('./webpack.config.js'); 6 | var nodemon = require('gulp-nodemon'); 7 | var rename = require('gulp-rename'); 8 | 9 | /** 10 | * Build (Webpack) 11 | */ 12 | 13 | gulp.task('clean:build', function() { 14 | del('./public/js/*') 15 | }) 16 | 17 | gulp.task('build', ['clean:build'], function() { 18 | return gulp.src('./app/app.js') 19 | .pipe(webpack(webpackConfig)) 20 | .on('error', function handleError() { 21 | this.emit('end'); // Recover from errors 22 | }) 23 | .pipe(gulp.dest('./')); 24 | }); 25 | 26 | gulp.task('watch:build', function() { 27 | return gulp.watch('./app/**/*', ['build']); 28 | }); 29 | 30 | 31 | /** 32 | * API Server (Database) 33 | */ 34 | 35 | gulp.task('restore-database', function() { 36 | return gulp.src('./data/restore.json') 37 | .pipe(rename('db.json')) 38 | .pipe(gulp.dest('./data')); 39 | }); 40 | 41 | gulp.task('serve:api', ['restore-database'], function(done) { 42 | cp.exec('node ./node_modules/json-server/bin/index.js --watch ./data/db.json --port 3001', {stdio: 'inherit'}) 43 | .on('close', done); 44 | }); 45 | 46 | 47 | /** 48 | * Node Server (Express) 49 | */ 50 | 51 | gulp.task('serve:node', function(done) { 52 | nodemon({ 53 | exec: 'node ./node_modules/babel-cli/bin/babel-node.js ./server.js', 54 | watch: ['server.js'], 55 | ext: 'js html' 56 | }); 57 | }); 58 | 59 | 60 | /** 61 | * Main tasks 62 | */ 63 | 64 | gulp.task('serve', ['serve:node', 'serve:api']); 65 | gulp.task('watch', ['build', 'watch:build']); 66 | gulp.task('default', ['serve']); 67 | -------------------------------------------------------------------------------- /guide-3-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React and Redux Example 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /guide-3-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-guide", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "start": "gulp" 7 | }, 8 | "author": "Brad Westfall ", 9 | "license": "ISC", 10 | "dependencies": { 11 | "express": "^4.13.4", 12 | "json-server": "^0.8.8", 13 | "lodash": "^4.6.1", 14 | "react": "0.14.7", 15 | "react-dom": "0.14.7", 16 | "react-redux": "^4.4.1", 17 | "react-router": "2.0.0", 18 | "redux": "^3.3.1" 19 | }, 20 | "devDependencies": { 21 | "axios": "^0.9.1", 22 | "babel-cli": "^6.5.1", 23 | "babel-core": "^6.5.2", 24 | "babel-loader": "^6.2.4", 25 | "babel-plugin-transform-object-assign": "^6.8.0", 26 | "babel-preset-es2015": "^6.5.0", 27 | "babel-preset-react": "^6.5.0", 28 | "babel-preset-stage-2": "^6.5.0", 29 | "del": "^2.2.0", 30 | "es6-promise": "^4.0.5", 31 | "gulp": "^3.9.1", 32 | "gulp-nodemon": "^2.0.6", 33 | "gulp-rename": "^1.2.2", 34 | "webpack": "^1.12.14", 35 | "webpack-stream": "^3.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /guide-3-redux/public/css/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | color: #222; 4 | font: 12pt Arial; 5 | } 6 | 7 | *, *::before, *::after { 8 | box-sizing: inherit; 9 | color: inherit; 10 | } 11 | 12 | 13 | 14 | /**************************************** 15 | Elements 16 | *****************************************/ 17 | 18 | body { 19 | margin: 3rem; 20 | } 21 | 22 | a { 23 | color: #2297D1; 24 | text-decoration: none; 25 | cursor: pointer; 26 | } 27 | 28 | * > h1 { 29 | margin-top: 0; 30 | } 31 | 32 | 33 | /**************************************** 34 | Components 35 | *****************************************/ 36 | 37 | /** 38 | * Input 39 | */ 40 | 41 | input[type="text"] { 42 | border: 1px solid #aaa; 43 | border-radius: 3px; 44 | font-size: 0.7em; 45 | padding: 0.5em 1em; 46 | } 47 | 48 | input[type="text"]:focus { 49 | outline: none; 50 | border-color: #2297D1; 51 | } 52 | 53 | 54 | /** 55 | * Button 56 | */ 57 | 58 | button { 59 | border: none; 60 | border-radius: 3px; 61 | background-color: #2297D1; 62 | font-size: 0.7em; 63 | padding: 0.5em 1em; 64 | cursor: pointer; 65 | color: #fff; 66 | } 67 | 68 | button.delete { 69 | background-color: #B83B3B; 70 | } 71 | 72 | 73 | /** 74 | * Search Form 75 | */ 76 | 77 | form.search { 78 | margin-top: 1em; 79 | } 80 | 81 | form.search button{ 82 | margin-left: 0.5em; 83 | } 84 | 85 | 86 | /** 87 | * Data List 88 | */ 89 | 90 | .data-list-item { 91 | padding: 0.6em; 92 | display: flex; 93 | border-bottom: 1px solid #bbb; 94 | } 95 | 96 | .data-list-item .details { 97 | flex: 1; 98 | } 99 | 100 | 101 | /** 102 | * User Profile 103 | */ 104 | 105 | .user-profile { 106 | display: flex; 107 | } 108 | 109 | .user-profile img { 110 | display: block; 111 | width: 100px; 112 | height: 100px; 113 | } 114 | 115 | .user-profile .details { 116 | flex: 1; 117 | margin-left: 1em; 118 | } 119 | 120 | .user-profile h1 { 121 | margin: 0; 122 | } 123 | 124 | .user-profile .repos { 125 | overflow: hidden; 126 | list-style: none; 127 | padding: 0; 128 | } 129 | 130 | .user-profile .repos li { 131 | float: left; 132 | width: 250px; 133 | margin-right: 2em; 134 | white-space: nowrap; 135 | overflow: hidden; 136 | text-overflow: ellipsis; 137 | } 138 | 139 | 140 | /**************************************** 141 | Layout 142 | *****************************************/ 143 | 144 | /** 145 | * Main Layout 146 | */ 147 | 148 | .app { 149 | display: flex; 150 | min-width: 800px; 151 | } 152 | 153 | .primary-aside { 154 | width: 12rem; 155 | } 156 | 157 | .primary-aside ul { 158 | list-style: none; 159 | margin: 0; 160 | } 161 | 162 | .primary-aside a { 163 | display: block; 164 | padding: 0.6em; 165 | } 166 | 167 | .primary-aside a.active { 168 | color: #fff; 169 | background-color: #2297D1; 170 | } 171 | 172 | main { 173 | flex: 1; 174 | margin-left: 2em; 175 | } 176 | 177 | /** 178 | * Search Layout 179 | */ 180 | 181 | .search-header, .search-footer { 182 | font-weight: bold; 183 | } 184 | 185 | .search-results { 186 | margin: 1em 0; 187 | } 188 | -------------------------------------------------------------------------------- /guide-3-redux/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is just a dummy server to facilidate our React SPA examples. 3 | * For a more professional setup of Express, see... 4 | * http://expressjs.com/en/starter/generator.html 5 | */ 6 | 7 | import express from 'express'; 8 | import path from 'path'; 9 | const app = express(); 10 | 11 | 12 | /** 13 | * Anything in public can be accessed statically without 14 | * this express router getting involved 15 | */ 16 | 17 | app.use(express.static(path.join(__dirname, 'public'), { 18 | dotfiles: 'ignore', 19 | index: false 20 | })); 21 | 22 | 23 | /** 24 | * Always serve the same HTML file for all requests 25 | */ 26 | 27 | app.get('*', function(req, res, next) { 28 | console.log('Request: [GET]', req.originalUrl) 29 | res.sendFile(path.resolve(__dirname, 'index.html')); 30 | }); 31 | 32 | 33 | /** 34 | * Error Handling 35 | */ 36 | 37 | app.use(function(req, res, next) { 38 | console.log('404') 39 | let err = new Error('Not Found'); 40 | err.status = 404; 41 | next(err); 42 | }); 43 | 44 | app.use(function(err, req, res, next) { 45 | res.sendStatus(err.status || 500); 46 | }); 47 | 48 | 49 | /** 50 | * Start Server 51 | */ 52 | 53 | const port = 3000; 54 | app.listen(port); 55 | 56 | console.log('Serving: localhost:' + port); -------------------------------------------------------------------------------- /guide-3-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./app/app.js", 5 | output: { 6 | filename: "public/js/bundle.js", 7 | sourceMapFilename: "public/js/bundle.map" 8 | }, 9 | devtool: '#source-map', 10 | module: { 11 | loaders: [ 12 | { 13 | loader: 'babel', 14 | exclude: /node_modules/ 15 | } 16 | ] 17 | } 18 | } 19 | --------------------------------------------------------------------------------