├── .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 |
16 | {
17 | mapValues(this.props.friends, (friend) => {
18 | return ();
24 | })
25 | }
26 |
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 |
--------------------------------------------------------------------------------