├── .babelrc ├── .eslintrc ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── index.html ├── package.json ├── server.js ├── src ├── actions │ ├── .gitkeep │ └── FriendsActions.js ├── components │ ├── AddFriendInput.css │ ├── AddFriendInput.js │ ├── FriendList.css │ ├── FriendList.js │ ├── FriendListItem.css │ ├── FriendListItem.js │ └── index.js ├── constants │ └── ActionTypes.js ├── containers │ ├── App.js │ ├── FriendListApp.css │ └── FriendListApp.js ├── index.js ├── reducers │ ├── friendlist.js │ └── index.js └── store_enhancers │ └── devTools.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | /* http://eslint.org/docs/rules/ */ 2 | { 3 | "parser": "babel-eslint", 4 | "ecmaFeatures": { 5 | "jsx": true 6 | }, 7 | "env": { 8 | "browser": true, 9 | "es6": true, 10 | "jasmine": true, 11 | "node": true 12 | }, 13 | "rules": { 14 | "camelcase": 2, 15 | "consistent-return": 2, 16 | "curly": [2, "all"], 17 | "dot-notation": 0, 18 | "eol-last": 2, 19 | "eqeqeq": 2, 20 | "max-len": [2, 80, 4], 21 | "new-cap": [2, {"capIsNew": false}], 22 | "no-eq-null": 2, 23 | "no-mixed-spaces-and-tabs": 2, 24 | "no-multiple-empty-lines": [2, {"max": 2}], 25 | "no-trailing-spaces": 2, 26 | "no-use-before-define": [2, "nofunc"], 27 | "no-undef": 2, 28 | "no-underscore-dangle": 0, 29 | "no-unused-vars": 2, 30 | "no-var": 2, 31 | "object-curly-spacing": [2, "always"], 32 | "quotes": [2, "single"], 33 | "react/display-name": 0, 34 | "react/jsx-boolean-value": 2, 35 | "react/jsx-quotes": 2, 36 | "react/jsx-no-undef": 2, 37 | "react/jsx-sort-props": 0, 38 | "react/jsx-uses-react": 2, 39 | "react/jsx-uses-vars": 2, 40 | "react/no-did-mount-set-state": 2, 41 | "react/no-did-update-set-state": 2, 42 | "react/no-multi-comp": 0, 43 | "react/no-unknown-property": 2, 44 | "react/prop-types": 2, 45 | "react/react-in-jsx-scope": 2, 46 | "react/self-closing-comp": 2, 47 | "react/wrap-multilines": 2, 48 | "semi": [2, "always"], 49 | "space-before-blocks": [2, "always"], 50 | "space-before-function-paren": [2, "always"], 51 | "space-in-brackets": [2, "always", { 52 | "singleValue": false, 53 | "objectsInArrays": false, 54 | "arraysInArrays": false, 55 | "arraysInObjects": true, 56 | "objectsInObjects": true, 57 | "propertyName": false 58 | }], 59 | "space-return-throw-case": 2, 60 | "strict": 0, 61 | "vars-on-top": 2, 62 | "no-warning-comments": [1, { 63 | "terms": ["todo", "fixme"], "location": "anywhere" 64 | }] 65 | }, 66 | "plugins": [ 67 | "react" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "newcap": false 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dan Abramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redux Tutorial : Friend list demo 2 | ===================== 3 | 4 | This repository is the source code for the blog post ["Getting started with Redux"](http://www.jchapron.com/2015/08/14/getting-started-with-redux/) (08/2015). 5 | 6 | You can follow along the tutorial or download this code and play with the demo. 7 | 8 | ### Usage 9 | 10 | ``` 11 | npm install 12 | DEBUG=true npm start 13 | open http://localhost:3000 14 | ``` 15 | 16 | If you are on windows, run `set DEBUG=true && npm start` instead. 17 | 18 | ### Dependencies 19 | 20 | * React 21 | * Webpack 22 | * [webpack-dev-server](https://github.com/webpack/webpack-dev-server) 23 | * [babel-loader](https://github.com/babel/babel-loader) 24 | * [react-hot-loader](https://github.com/gaearon/react-hot-loader) 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hot-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Boilerplate for ReactJS project with hot code reloading", 5 | "scripts": { 6 | "start": "node server.js", 7 | "lint": "eslint src" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/gaearon/react-hot-boilerplate.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "reactjs", 16 | "boilerplate", 17 | "hot", 18 | "reload", 19 | "hmr", 20 | "live", 21 | "edit", 22 | "webpack" 23 | ], 24 | "author": "Dan Abramov (http://github.com/gaearon)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/gaearon/react-hot-boilerplate/issues" 28 | }, 29 | "homepage": "https://github.com/gaearon/react-hot-boilerplate", 30 | "devDependencies": { 31 | "babel-core": "^5.8.3", 32 | "babel-eslint": "^4.0.5", 33 | "babel-loader": "^5.3.2", 34 | "css-loader": "^0.15.6", 35 | "cssnext-loader": "^1.0.1", 36 | "eslint": "^0.24.1", 37 | "eslint-plugin-react": "^3.1.0", 38 | "extract-text-webpack-plugin": "^0.8.2", 39 | "html-webpack-plugin": "^1.6.1", 40 | "react-hot-loader": "^1.2.7", 41 | "redux-devtools": "^1.0.2", 42 | "style-loader": "^0.12.3", 43 | "webpack": "^1.9.6", 44 | "webpack-dev-server": "^1.8.2" 45 | }, 46 | "dependencies": { 47 | "classnames": "^2.1.3", 48 | "lodash": "^3.10.1", 49 | "react": "^0.13.0", 50 | "react-redux": "^0.2.2", 51 | "redux": "^1.0.0-rc" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', function (err, result) { 10 | if (err) { 11 | console.log(err); 12 | } 13 | 14 | console.log('Listening at localhost:3000'); 15 | }); 16 | -------------------------------------------------------------------------------- /src/actions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchapron/redux-friendlist-demo/ff4b3de82e13c0bd450c62881573148c1674aaf2/src/actions/.gitkeep -------------------------------------------------------------------------------- /src/actions/FriendsActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export function addFriend(name) { 4 | return { 5 | type: types.ADD_FRIEND, 6 | name 7 | }; 8 | } 9 | 10 | export function deleteFriend(id) { 11 | return { 12 | type: types.DELETE_FRIEND, 13 | id 14 | }; 15 | } 16 | 17 | export function starFriend(id) { 18 | return { 19 | type: types.STAR_FRIEND, 20 | id 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/AddFriendInput.css: -------------------------------------------------------------------------------- 1 | :local(.addFriendInput) { 2 | border-radius: 0; 3 | border-color: #ABAAAA; 4 | border-left: 0; 5 | border-right: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/AddFriendInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | import styles from './AddFriendInput.css'; 4 | 5 | export default class AddFriendInput extends Component { 6 | static propTypes = { 7 | addFriend: PropTypes.func.isRequired 8 | } 9 | 10 | render () { 11 | return ( 12 | 20 | ); 21 | } 22 | 23 | constructor (props, context) { 24 | super(props, context); 25 | this.state = { 26 | name: this.props.name || '', 27 | }; 28 | } 29 | 30 | handleChange (e) { 31 | this.setState({ name: e.target.value }); 32 | } 33 | 34 | handleSubmit (e) { 35 | const name = e.target.value.trim(); 36 | if (e.which === 13) { 37 | this.props.addFriend(name); 38 | this.setState({ name: '' }); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/components/FriendList.css: -------------------------------------------------------------------------------- 1 | :local(.friendList) { 2 | padding-left: 0; 3 | margin-bottom: 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/FriendList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import mapValues from 'lodash/object/mapValues'; 3 | 4 | import styles from './FriendList.css'; 5 | import FriendListItem from './FriendListItem'; 6 | 7 | export default class FriendList extends Component { 8 | static propTypes = { 9 | friends: PropTypes.object.isRequired, 10 | actions: PropTypes.object.isRequired 11 | } 12 | 13 | render () { 14 | return ( 15 | 27 | ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/components/FriendListItem.css: -------------------------------------------------------------------------------- 1 | :local(.friendListItem) { 2 | list-style: none; 3 | padding: 20px 10px 20px 20px; 4 | background-color: white; 5 | border-bottom: 1px solid #E3E3E3; 6 | display: flex; 7 | } 8 | 9 | :local(.friendInfos) { 10 | flex: 1 0 auto; 11 | } 12 | 13 | :local(.friendInfos span) { 14 | font-weight: bold; 15 | } 16 | 17 | :local(.friendActions) { 18 | flex: 0 0 90px; 19 | } 20 | 21 | :local(.btnAction), :local(.btnAction):active, :local(.btnAction):focus, :local(.btnAction):hover { 22 | margin-right: 5px; 23 | color: #5C75B0; 24 | } 25 | 26 | button:focus { 27 | outline: 0 !important; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/FriendListItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | import styles from './FriendListItem.css'; 4 | 5 | export default class FriendListItem extends Component { 6 | static propTypes = { 7 | id: PropTypes.number.isRequired, 8 | name: PropTypes.string.isRequired, 9 | starred: PropTypes.bool, 10 | starFriend: PropTypes.func.isRequired, 11 | deleteFriend: PropTypes.func.isRequired 12 | } 13 | 14 | render () { 15 | return ( 16 |
  • 17 |
    18 |
    {this.props.name}
    19 |
    xx friends in common
    20 |
    21 |
    22 | 25 | 28 |
    29 |
  • 30 | ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AddFriendInput } from './AddFriendInput'; 2 | export { default as FriendList } from './FriendList'; 3 | export { default as FriendListItem } from './FriendListItem'; 4 | -------------------------------------------------------------------------------- /src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_FRIEND = 'ADD_FRIEND'; 2 | export const STAR_FRIEND = 'STAR_FRIEND'; 3 | export const DELETE_FRIEND = 'DELETE_FRIEND'; 4 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { combineReducers } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import { createStore, renderDevTools } from '../store_enhancers/devTools'; 6 | 7 | import FriendListApp from './FriendListApp'; 8 | import * as reducers from '../reducers'; 9 | 10 | const reducer = combineReducers(reducers); 11 | const store = createStore(reducer); 12 | 13 | export default class App extends Component { 14 | render() { 15 | return ( 16 |
    17 | 18 | {() => } 19 | 20 | 21 | {renderDevTools(store)} 22 |
    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/containers/FriendListApp.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F4F3F0; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | :local(.friendListApp) { 9 | width: 300px; 10 | padding-top: 18px; 11 | background-color: #5C75B0; 12 | border: 1px solid #E3E3E3; 13 | } 14 | 15 | :local(.friendListApp h1) { 16 | color: white; 17 | font-size: 16px; 18 | line-height: 20px; 19 | margin-bottom: 10px; 20 | margin-top: 0; 21 | padding-left: 10px; 22 | font-family: Helvetica; 23 | } 24 | -------------------------------------------------------------------------------- /src/containers/FriendListApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import styles from './FriendListApp.css'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | 6 | import * as FriendsActions from '../actions/FriendsActions'; 7 | import { FriendList, AddFriendInput } from '../components'; 8 | 9 | @connect(state => ({ 10 | friendlist: state.friendlist 11 | })) 12 | export default class FriendListApp extends Component { 13 | 14 | static propTypes = { 15 | friendsById: PropTypes.object.isRequired, 16 | dispatch: PropTypes.func.isRequired 17 | } 18 | 19 | render () { 20 | const { friendlist: { friendsById }, dispatch } = this.props; 21 | const actions = bindActionCreators(FriendsActions, dispatch); 22 | 23 | return ( 24 |
    25 |

    The FriendList

    26 | 27 | 28 |
    29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './containers/App'; 3 | 4 | React.render(, document.getElementById('root')); 5 | -------------------------------------------------------------------------------- /src/reducers/friendlist.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import omit from 'lodash/object/omit'; 3 | import assign from 'lodash/object/assign'; 4 | import mapValues from 'lodash/object/mapValues'; 5 | 6 | const initialState = { 7 | friends: [1, 2, 3], 8 | friendsById: { 9 | 1: { 10 | id: 1, 11 | name: 'Theodore Roosevelt' 12 | }, 13 | 2: { 14 | id: 2, 15 | name: 'Abraham Lincoln' 16 | }, 17 | 3: { 18 | id: 3, 19 | name: 'George Washington' 20 | } 21 | } 22 | }; 23 | 24 | export default function friends(state = initialState, action) { 25 | switch (action.type) { 26 | 27 | case types.ADD_FRIEND: 28 | const newId = state.friends[state.friends.length-1] + 1; 29 | return { 30 | ...state, 31 | friends: state.friends.concat(newId), 32 | friendsById: { 33 | ...state.friendsById, 34 | [newId]: { 35 | id: newId, 36 | name: action.name 37 | } 38 | }, 39 | } 40 | 41 | case types.DELETE_FRIEND: 42 | return { 43 | ...state, 44 | friends: state.friends.filter(id => id !== action.id), 45 | friendsById: omit(state.friendsById, action.id) 46 | } 47 | 48 | case types.STAR_FRIEND: 49 | return { 50 | ...state, 51 | friendsById: mapValues(state.friendsById, (friend) => { 52 | return friend.id === action.id ? 53 | assign({}, friend, { starred: !friend.starred }) : 54 | friend 55 | }) 56 | } 57 | 58 | default: 59 | return state; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as friendlist } from './friendlist'; 2 | -------------------------------------------------------------------------------- /src/store_enhancers/devTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore as initialCreateStore, compose } from 'redux'; 3 | 4 | export let createStore = initialCreateStore; 5 | 6 | if (__DEV__) { 7 | createStore = compose( 8 | require('redux-devtools').devTools(), 9 | require('redux-devtools').persistState( 10 | window.location.href.match(/[?&]debug_session=([^&]+)\b/) 11 | ), 12 | createStore 13 | ); 14 | } 15 | 16 | export function renderDevTools(store) { 17 | if (__DEV__) { 18 | let {DevTools, DebugPanel, LogMonitor} = require('redux-devtools/lib/react'); 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | return null; 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var devFlagPlugin = new webpack.DefinePlugin({ 5 | __DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false')) 6 | }); 7 | 8 | module.exports = { 9 | devtool: 'eval', 10 | entry: [ 11 | 'webpack-dev-server/client?http://localhost:3000', 12 | 'webpack/hot/only-dev-server', 13 | './src/index' 14 | ], 15 | output: { 16 | path: path.join(__dirname, 'dist'), 17 | filename: 'bundle.js', 18 | publicPath: '/static/' 19 | }, 20 | plugins: [ 21 | new webpack.HotModuleReplacementPlugin(), 22 | new webpack.NoErrorsPlugin(), 23 | devFlagPlugin, 24 | new ExtractTextPlugin('app.css') 25 | ], 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.jsx?$/, 30 | loaders: ['react-hot', 'babel'], 31 | include: path.join(__dirname, 'src') 32 | }, 33 | { test: /\.css$/, loader: ExtractTextPlugin.extract('css-loader?module!cssnext-loader') } 34 | ] 35 | }, 36 | resolve: { 37 | extensions: ['', '.js', '.json'] 38 | } 39 | }; 40 | --------------------------------------------------------------------------------