├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── src ├── app │ ├── actions │ │ └── index.js │ ├── components │ │ ├── TaskForm │ │ │ └── index.js │ │ ├── TaskItem │ │ │ └── index.js │ │ ├── TaskList │ │ │ └── index.js │ │ └── TaskStats │ │ │ └── index.js │ ├── containers │ │ ├── App.js │ │ └── style.css │ ├── index.js │ ├── reducers │ │ ├── index.js │ │ └── tasks.js │ ├── selector.js │ └── store.js └── endpoint │ └── index.html └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/endpoint/static 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical", 3 | "rules": { 4 | "react/prop-types": 0, 5 | "react/prefer-stateless-function": 0, 6 | "import/no-namespace": 0, 7 | "import/no-unassigned-import": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | endpoint/static 4 | *.log 5 | .* 6 | !.gitignore 7 | !.npmignore 8 | !.babelrc 9 | !.travis.yml 10 | !.eslintrc 11 | !.eslintignore 12 | !.editorconfig 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `redux-immutable` examples 2 | 3 | This app demonstrates: 4 | 5 | * How to use `redux-immutable` `combineReducers`. 6 | * How to make a reducer using Immutable data. 7 | * How to make a selector using Immutable data. 8 | * How to use middleware. 9 | * How to use [react-hot-reload](https://github.com/gaearon/react-hot-loader). 10 | 11 | To launch the app: 12 | 13 | ```bash 14 | git clone git@github.com:gajus/redux-immutable-examples.git 15 | cd ./redux-immutable-examples 16 | npm install 17 | npm start 18 | ``` 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "lint": "eslint ./src", 5 | "start": "webpack-dev-server", 6 | "precommit": "npm run lint" 7 | }, 8 | "devDependencies": { 9 | "babel": "^6.5.2", 10 | "babel-eslint": "^7.1.1", 11 | "babel-loader": "^6.2.10", 12 | "babel-preset-es2015": "^6.18.0", 13 | "babel-preset-react": "^6.16.0", 14 | "babel-preset-stage-0": "^6.16.0", 15 | "chai": "^3.5.0", 16 | "css-loader": "^0.26.1", 17 | "eslint": "^3.13.1", 18 | "eslint-config-canonical": "^6.1.0", 19 | "react-hot-loader": "^1.3.1", 20 | "redux-devtools": "^3.3.1", 21 | "style-loader": "^0.13.1", 22 | "webpack": "^1.14.0", 23 | "webpack-dev-server": "^1.16.2" 24 | }, 25 | "dependencies": { 26 | "babel-core": "^6.21.0", 27 | "classnames": "^2.2.5", 28 | "immutable": "^3.8.1", 29 | "lodash": "^4.17.4", 30 | "react": "^15.4.2", 31 | "react-dom": "^15.4.2", 32 | "react-redux": "^5.0.2", 33 | "redux": "^3.6.0", 34 | "redux-create-reducer": "^1.1.1", 35 | "redux-immutable": "^3.0.9", 36 | "redux-logger": "^2.7.4", 37 | "redux-thunk": "^2.1.0", 38 | "reselect": "^2.5.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/actions/index.js: -------------------------------------------------------------------------------- 1 | const taskAdd = (name) => { 2 | return { 3 | data: { 4 | name 5 | }, 6 | type: 'TASK_ADD' 7 | }; 8 | }; 9 | 10 | const taskDone = (id) => { 11 | return { 12 | data: { 13 | id 14 | }, 15 | type: 'TASK_DONE' 16 | }; 17 | }; 18 | 19 | const taskUndone = (id) => { 20 | return { 21 | data: { 22 | id 23 | }, 24 | type: 'TASK_UNDONE' 25 | }; 26 | }; 27 | 28 | export { 29 | taskAdd, 30 | taskDone, 31 | taskUndone 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/components/TaskForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class extends React.Component { 4 | static propTypes = { 5 | onSave: React.PropTypes.func.isRequired 6 | }; 7 | 8 | handleSubmit = (event) => { 9 | event.preventDefault(); 10 | 11 | const name = this.textInput.value; 12 | 13 | this.textInput.value = ''; 14 | 15 | this.props.onSave(name); 16 | }; 17 | 18 | render () { 19 | return
20 | { 22 | this.textInput = input; 23 | }} 24 | type='text' 25 | /> 26 | 27 |
; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/components/TaskItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export default class extends React.Component { 5 | static propTypes = { 6 | done: React.PropTypes.bool.isRequired, 7 | id: React.PropTypes.string.isRequired, 8 | onTaskDone: React.PropTypes.func.isRequired, 9 | onTaskUndone: React.PropTypes.func.isRequired 10 | }; 11 | 12 | handleToggleStatus = () => { 13 | const { 14 | id, 15 | done, 16 | onTaskDone, 17 | onTaskUndone 18 | } = this.props; 19 | 20 | if (done) { 21 | onTaskUndone(id); 22 | } else { 23 | onTaskDone(id); 24 | } 25 | }; 26 | 27 | render () { 28 | const { 29 | name, 30 | done 31 | } = this.props; 32 | 33 | const componentClassName = classNames('component-todo-item', { 34 | 'status-done': done 35 | }); 36 | 37 | return
38 | {name} 39 |
40 |
; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/TaskList/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Immutable from 'immutable'; 3 | import TaskItem from '../TaskItem'; 4 | 5 | export default class extends React.Component { 6 | static propTypes = { 7 | tasks: React.PropTypes.instanceOf(Immutable.List).isRequired 8 | }; 9 | 10 | render () { 11 | const { 12 | tasks, 13 | onTaskDone, 14 | onTaskUndone 15 | } = this.props; 16 | 17 | return ; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/TaskStats/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class extends React.Component { 4 | static propTypes = { 5 | taskCount: React.PropTypes.number.isRequired, 6 | undoneTaskCount: React.PropTypes.number.isRequired 7 | }; 8 | 9 | render () { 10 | const { 11 | taskCount, 12 | undoneTaskCount 13 | } = this.props; 14 | 15 | return
16 |
17 |
Task count:
18 |
{taskCount}
19 | 20 |
Undone task count:
21 |
{undoneTaskCount}
22 |
23 |
; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | connect 4 | } from 'react-redux'; 5 | import TaskForm from '../components/TaskForm'; 6 | import TaskList from '../components/TaskList'; 7 | import TaskStats from '../components/TaskStats'; 8 | import selector from '../selector'; 9 | import { 10 | taskAdd, 11 | taskDone, 12 | taskUndone 13 | } from '../actions'; 14 | import './style.css'; 15 | 16 | class App extends React.Component { 17 | handleTaskAdd = (name) => { 18 | this.props.dispatch(taskAdd(name)); 19 | }; 20 | 21 | handleTaskDone = (id) => { 22 | this.props.dispatch(taskDone(id)); 23 | }; 24 | 25 | handleTaskUndone = (id) => { 26 | this.props.dispatch(taskUndone(id)); 27 | }; 28 | 29 | render () { 30 | const { 31 | tasks, 32 | taskCount, 33 | doneTaskCount 34 | } = this.props; 35 | 36 | return
37 | 40 | 45 | 49 |
; 50 | } 51 | } 52 | 53 | export default connect(selector)(App); 54 | -------------------------------------------------------------------------------- /src/app/containers/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | ul, 4 | li, 5 | input, 6 | dl, 7 | dt, 8 | dd { 9 | margin: 0; padding: 0; outline: 0; 10 | } 11 | 12 | li { 13 | list-style: none; 14 | } 15 | 16 | body { 17 | font: 16px/22px 'helvetica-neue', 'Helvetica Neue', Helvetica, Arial, Sans-Serif; background: #E9E9E9; 18 | } 19 | 20 | #viewport { 21 | width: 440px; background: #fff; margin: 40px auto; 22 | } 23 | 24 | .component-task-form { 25 | 26 | } 27 | 28 | .component-task-form input { 29 | background: #4054B2; display: block; border: none; font: inherit; height: 84px; width: 400px; padding: 0 20px; font-size: 20px; color: #fff; 30 | } 31 | 32 | .component-task-form button { 33 | display: none; 34 | } 35 | 36 | .component-todo-item { 37 | min-height: 84px; border-bottom: 1px solid #ccc; 38 | } 39 | 40 | .component-todo-item > .name { 41 | float: left; padding: 20px; display: block; 42 | } 43 | 44 | .component-todo-item > .toggle-status { 45 | float: right; width: 84px; height: 84px; cursor: pointer; background: #F44336; 46 | } 47 | 48 | .component-todo-item.status-done > .toggle-status { 49 | background: #4CAF50; 50 | } 51 | 52 | .component-task-stats { 53 | overflow: hidden; height: 84px; 54 | } 55 | 56 | .component-task-stats dl { 57 | display: block; overflow: hidden; padding: 20px; 58 | } 59 | 60 | .component-task-stats dt, 61 | .component-task-stats dd { 62 | 63 | } 64 | 65 | .component-task-stats dt { 66 | float: left; margin: 0 5px 0 0; 67 | } 68 | 69 | .component-task-stats dd { 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { 5 | Provider 6 | } from 'react-redux'; 7 | import App from './containers/App'; 8 | import store from './store'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , document.getElementById('app')); 14 | -------------------------------------------------------------------------------- /src/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import tasks from './tasks'; 2 | 3 | export default { 4 | tasks 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/reducers/tasks.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | createReducer 4 | } from 'redux-create-reducer'; 5 | import Immutable from 'immutable'; 6 | 7 | const initialState = Immutable.fromJS([ 8 | { 9 | done: true, 10 | id: _.uniqueId(), 11 | name: 'foo' 12 | }, 13 | { 14 | done: false, 15 | id: _.uniqueId(), 16 | name: 'bar' 17 | }, 18 | { 19 | done: false, 20 | id: _.uniqueId(), 21 | name: 'baz' 22 | }, 23 | { 24 | done: false, 25 | id: _.uniqueId(), 26 | name: 'quux' 27 | } 28 | ]); 29 | 30 | /** 31 | * @param {Immutable.List} domain 32 | * @param {Object} action 33 | * @param {string} action.data.name 34 | * @returns {Immutable.List} 35 | */ 36 | const TASK_ADD = (domain, action) => { 37 | return domain 38 | .push(Immutable.Map({ 39 | done: false, 40 | id: _.uniqueId(), 41 | name: action.data.name 42 | })); 43 | }; 44 | 45 | /** 46 | * @param {Immutable.List} domain 47 | * @param {Object} action 48 | * @param {number} action.data.id 49 | * @returns {Immutable.List} 50 | */ 51 | const TASK_DONE = (domain, action) => { 52 | const index = domain.findIndex((item) => { 53 | return item.get('id') === action.data.id; 54 | }); 55 | 56 | return domain 57 | .update(index, (task) => { 58 | return task.set('done', true); 59 | }); 60 | }; 61 | 62 | /** 63 | * @param {Immutable.List} domain 64 | * @param {Object} action 65 | * @param {number} action.data.id 66 | * @returns {Immutable.List} 67 | */ 68 | const TASK_UNDONE = (domain, action) => { 69 | const index = domain 70 | .findIndex((item) => { 71 | return item.get('id') === action.data.id; 72 | }); 73 | 74 | return domain 75 | .update(index, (task) => { 76 | return task.set('done', false); 77 | }); 78 | }; 79 | 80 | export default createReducer(initialState, { 81 | TASK_ADD, 82 | TASK_DONE, 83 | TASK_UNDONE 84 | }); 85 | -------------------------------------------------------------------------------- /src/app/selector.js: -------------------------------------------------------------------------------- 1 | import { 2 | createSelector 3 | } from 'reselect'; 4 | 5 | const taskSelector = (state) => { 6 | return state.get('tasks'); 7 | }; 8 | 9 | const doneTaskSelector = createSelector([taskSelector], (tasks) => { 10 | return tasks.filter((task) => { 11 | return task.get('done'); 12 | }); 13 | }); 14 | 15 | export default (state) => { 16 | return { 17 | doneTaskCount: doneTaskSelector(state).count(), 18 | taskCount: taskSelector(state).count(), 19 | tasks: taskSelector(state) 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | applyMiddleware 4 | } from 'redux'; 5 | import { 6 | combineReducers 7 | } from 'redux-immutable'; 8 | import thunk from 'redux-thunk'; 9 | import createLogger from 'redux-logger'; 10 | import Immutable from 'immutable'; 11 | import reducers from './reducers'; 12 | 13 | const logger = createLogger(); 14 | 15 | const reducer = combineReducers(reducers); 16 | 17 | const store = createStore( 18 | reducer, 19 | Immutable.Map({}), 20 | applyMiddleware(thunk, logger) 21 | ); 22 | 23 | export default store; 24 | -------------------------------------------------------------------------------- /src/endpoint/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | 6 | const devServer = { 7 | contentBase: __dirname + '/src/endpoint', 8 | colors: true, 9 | quiet: false, 10 | noInfo: false, 11 | publicPath: '/static/', 12 | port: 8000, 13 | hot: true, 14 | stats: 'minimal' 15 | }; 16 | 17 | module.exports = { 18 | devtool: 'inline-source-map', 19 | debug: true, 20 | devServer, 21 | context: path.resolve(__dirname, 'src'), 22 | entry: { 23 | app: [ 24 | 'webpack-dev-server/client?http://127.0.0.1:' + devServer.port, 25 | 'webpack/hot/only-dev-server', 26 | './app' 27 | ] 28 | }, 29 | output: { 30 | path: path.resolve(__dirname, 'src/endpoint/static'), 31 | filename: '[name].js', 32 | publicPath: devServer.publicPath 33 | }, 34 | plugins: [ 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.optimize.OccurenceOrderPlugin(), 37 | new webpack.OldWatchingPlugin(), 38 | new webpack.optimize.DedupePlugin(), 39 | new webpack.NoErrorsPlugin() 40 | ], 41 | module: { 42 | loaders: [ 43 | { 44 | test: /\.js$/, 45 | include: path.resolve(__dirname, 'src'), 46 | loader: 'react-hot-loader' 47 | }, 48 | { 49 | test: /\.js$/, 50 | include: path.resolve(__dirname, 'src'), 51 | loader: 'babel-loader' 52 | }, 53 | { 54 | test: /\.css/, 55 | include: path.resolve(__dirname, 'src'), 56 | loader: 'style-loader!css-loader' 57 | } 58 | ] 59 | } 60 | }; 61 | --------------------------------------------------------------------------------