├── .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
;
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 ;
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 {tasks.map((task) => {
18 | return -
19 |
26 |
;
27 | })}
28 |
;
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 |
--------------------------------------------------------------------------------