Redux是JavaScript应用的状态容器,为应用提供可预测的状态管理, 可以和许多JavaScript库结合使用, 如React, Angular, Ember。

Redux三大原则

1.不管是多复杂的应用, 应用的state均保存在一个JavaScript对象中, 并且我们能够实时观察到state的变化.

2.state是只读的, 不可以直接改变它的值, 如果想要改变必须通过dispatch action进行, action也是一个JavaScript对象, 描述state的变化.

3.state改变均由reducer完成, reducer是纯函数, 在函数内需要声明根据action的不同, state是应该如何变化, 下面是一个reducer的例子:

1
2
3
4
5
6
7
8
9
10
 const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

counter接受两个参数, 初始state和action, 返回更新后的state, state根据不同的action.type变化.

Redux store的三个重要方法

利用Redux的createStore()方法可以创建store用以保存app的状态:

1
const store = createStore(counter);

store遵循了redux的三个基本原则:

  1. 保存应用当前的状态: getState()方法;
  2. 能够分发action: dispatch()方法;
  3. 创建store时需要声明reducer函数.

store关联的三个重要方法分别是

  • getState()
  • dispatch()
  • subscribe()

jsbin中的js和html区域中输入以下代码(将js区域的标准设置为ES6/Babel), 通过点击output的空白区域测试app的功能. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

const { createStore } = Redux;
// ⬆️运用了ES6的解构(destructuring), 等同于:
// var createStore = Redux.createStore;

// ES6 module中这样引入:
// import { createStore } from 'redux';

//将reducer传入createStore函数中创建store
const store = createStore(counter);

// store关联了3个重要的方法:

// 1. getState() 获取store中当前的state
console.log(store.getState());

// 2. dispatch() 分发action, 并返回这个action, 是唯一能改变store中数据的方式

store.dispatch({ type: 'INCREMENT'});
console.log(store.getState());

store.dispatch({ type: 'DECREMENT'});
console.log(store.getState());

// 3. subscribe() 注册监听函数, 在store发生变化时被调用

const render = () => {
document.body.innerText = store.getState();
}
store.subscribe(render);
render();

document.addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT'});
})

html模板(后面几例均使用此模板):
(同时加入了react和react-dom库以方便之后的app使用, 在Vanilla JS与redux结合完成的计数器app中并没有用到react库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script src="https://fb.me/react-15.1.0.js"></script>
<script src="https://fb.me/react-dom-15.1.0.js"></script>
<script src="https://unpkg.com/expect/umd/expect.min.js"></script>
<script src="https://wzrd.in/standalone/deep-freeze@latest"></script>
<title>JS Bin</title>
</head>
<body>
<div id='root'></div>
</body>
</html>

createStore以及三个重要方法的实现(简化版)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const counter = (state = 0, action ) => {
switch (action.type){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

const createStore = (reducer) => {
let state;
let listeners = [];

// getState()
const getState = () => state;

// dispatch()
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};

// subscribe()
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l!== listener);
// unsubscribe
};
};

dispatch({});
// call it initially

return { getState, dispatch, subscribe };
};

const store = createStore(counter);

const render = () => {
document.body.innerText = store.getState();
}

store.subscribe(render);
render();

document.addEventListener('click',() => {
store.dispatch( { type: 'INCREMENT' });
});

subscribe()注册一个监听函数, 在store发生变化时被调用, 一般在与某个系统(如React)结合时使用, 上述例子中是与Vanilla JS结合使用, 将store的状态变化以数字的形式呈现在页面上.subscribe()方法返回的函数提供了unsubscribe的方法, 将某个监听函数从监听函数列表中移除, 这样的话, 如果想要unsubsribe某个监听函数, 可以进行如下操作:

1
2
3
4
5
6
let foo = () => {...};

let unsubscribeFoo = store.subscribe(foo);

// 之后若想要unsubscribe, 这样声明:
unsubscribeFoo(); // 一旦该函数执行, store有变化时函数foo就不会被调用

Redux与React结合(基础)

上一个例子中使用Vanilla JS直接操作DOM, 对性能不利, 下面介绍用React与Redux结合制作一个简单的计数器app的方法, html模板不变, 使用下面的js代码. 示例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const counter = (state = 0, action) => {
switch (action.type){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

const Counter = ({
value,
onIncrement,
onDecrement
}) => {
return (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
)
};

const { createStore } = Redux;

const store = createStore(counter);


const render = () => {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() =>
store.dispatch({
type: 'INCREMENT'
})
}
onDecrement={() =>
store.dispatch({
type: 'DECREMENT'
})
}
/>,
document.getElementById('root')
);
};

store.subscribe(render);

render();

分析以上计数器app, 将app分解为视图层和数据层.

react负责两部分视图的渲染, 一是当前的计数值, 二是控制数字变化的部分, 即增加减少按钮.

redux负责保存当前计数器的state, 即按钮被实际按下的次数, 以及当下的state应该如何渲染.

Counter组件是dumb component, 不负责任何业务逻辑(business logic), 它的任务是说明当前store的state应该如何转化为可以被渲染的输出(value), 以及通过props传递的回调函数是如何与事件处理函数绑定的(onIncrement, onDecrement).

在render函数中, 我们将Counter组件渲染到页面中, 因此这里render函数关联了react库和redux库, 将redux负责的state渲染到react库负责的视图中. 其中value值渲染的是当前的状态, 然后根据用户点击的不同按钮dispatch不同的action, 点击按钮’+’时, dispatch的action是’INCREMENT’, state值就增加1, 因为通过store.subscribe(render)声明Counter中value属性值需要根据store中state的变化而变化, 因此value值就会增加1, 页面上的显示的数字自然也加上1.

combineReducers简单实现

(updated at 2017-09-13)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](
state[key],
action
);
return nextState;
},
{}
);
};
};

combineReducers接受的参数是 mapping key-value 返回一个reducer函数

React Redux TodoList

原始版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});

const { createStore } = Redux;
const store = createStore(todoApp);


const { Component } = React;

// FilterLink react component
const FilterLink = ({
filter,
currentFilter,
children
}) => {
if(filter === currentFilter){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter
});
}}
>
{children}
</a>
)
};

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}

let nextTodoId = 0;
class TodoApp extends Component {
render() {
const {
todos,
visibilityFilter
} = this.props;
const visibleTodos = getVisibleTodos(
todos,
visibilityFilter
);
return (
<div>
<input ref={node => {
this.input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
text: this.input.value,
id: nextTodoId++
});
this.input.value = '';
}}>
Add Todo
</button>
<ul>
{visibleTodos.map(todo =>
<li key={todo.id}
onClick={() => {
store.dispatch({
type: 'TOGGLE_TODO',
id: todo.id
});
}}
style={{
textDecoration:
todo.completed ?
'line-through' :
'none'
}}>
{todo.text}
</li>
)}
</ul>
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
currentFilter={visibilityFilter}
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"
currentFilter={visibilityFilter}
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
currentFilter={visibilityFilter}
>
Completed
</FilterLink>
</p>
</div>
)
};
}

// See Section 8 for earlier `render()` example
const render = () => {
ReactDOM.render(
// Render the TodoApp Component to the <div> with id 'root'
<TodoApp
{...store.getState()}
/>,
document.getElementById('root')

)
};

store.subscribe(render);
render();

提取 presentational components

presentational components: Todo, TodoList, AddTodo, Footer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});

const { createStore } = Redux;
const store = createStore(todoApp);


const { Component } = React;

// FilterLink react component
const FilterLink = ({
filter,
currentFilter,
children,
onClick
}) => {
if(filter === currentFilter){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
onClick(filter)
}}
>
{children}
</a>
)
};

const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}>
{text}
</li>
);

const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);

const AddTodo = ({
onAddClick
}) => {
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
onAddClick(input.value);
input.value = "";
}}>
Add Todo
</button>
</div>
);
};

const Footer = ({
visibilityFilter,
onFilterClick
}) => (
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
currentFilter={visibilityFilter}
onClick={onFilterClick}
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"
currentFilter={visibilityFilter}
onClick={onFilterClick}
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
currentFilter={visibilityFilter}
onClick={onFilterClick}
>
Completed
</FilterLink>
</p>
)

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}

let nextTodoId = 0;
const TodoApp = ({
todos,
visibilityFilter
}) => (
<div>
<AddTodo
onAddClick = {text =>
store.dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
}
/>
<TodoList
todos={
getVisibleTodos(
todos,
visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}
/>
<Footer
visibilityFilter={visibilityFilter}
onFilterClick={filter =>
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter
})
}/>
</div>
);

// See Section 8 for earlier `render()` example
const render = () => {
ReactDOM.render(
// Render the TodoApp Component to the <div> with id 'root'
<TodoApp
{...store.getState()}
/>,
document.getElementById('root')

)
};

store.subscribe(render);
render();

store变化 todoApp 重新渲染, store 中 todos, visibilityFilter 属性

首先渲染第一个组件 AddTodo, AddTodo 是presentational component 含有input 和 button 元素 点击button 后调用 onAddClick函数, onAddClick是 AddTodo 组件的prop, specified by TodoApp, 当button click之后, dispatch 一个action type为ADD_TODO, 调用reducer更新全局store, 重新渲染todoApp组件, todoItem本身由todolist 展示组件控制, 由两个属性控制, 可见的todos以及onTodoClick函数, todolist 函数接受todos数组 分别渲染到Todo组件中, 使用…传递todo的属性到Todo组件

提取 container components

FilterLink, VisibleTodoList, AddTodo

container 连接 presentational component 和 redux store, 使用container component 避免在组件中传递太多属性导致混乱

decouple from behavior and data that its child components needed

specify the data and behavior that it need

render method 里使用state 因此需要forceUpdate

containers 不再需要来自 state 的 props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});

const { createStore } = Redux;
const store = createStore(todoApp);


const { Component } = React;



const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}>
{text}
</li>
);

const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);

// 难界定AddTodo是presentational 还是 container, 因为input 和 button 都是处理UI 而button 中的 onClick 属性dispatch action.

// 因为是比较简单的处理 所以混杂了这两者 逻辑复杂之后再分离.

const AddTodo = () => {
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = "";
}}>
Add Todo
</button>
</div>
);
};

// Presentaional component - Link

const Link = ({
active,
children,
onClick
}) => {
if(active){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
onClick()
}}
>
{children}
</a>
)
};

// container component - FilterLink
class FilterLink extends Component {
// dispatch了action但是state不会自动更新 因此forceUpfate()
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const props = this.props;
const state = store.getState();

return (
<Link
active={
props.filter === state.visibilityFilter
}
onClick = {() =>
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: props.filter
})
}
>
{props.children}
</Link>
)
}
}

// presentational component
const Footer = () => (
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"x
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
>
Completed
</FilterLink>
</p>
)

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}

class VisibleTodoList extends Component {
// dispatch了action但是state不会自动更新 因此forceUpfate()
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const props = this.props;
const state = store.getState();

return (
<TodoList
todos={
getVisibleTodos(
state.todos,
state.visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}/>
)
}
}

let nextTodoId = 0;
const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);

ReactDOM.render(
<TodoApp />,
document.getElementById('root')
)

// container components it render are going to subscribe to the store themselves, 因此, 不需要再手动 subscribe render

pass store down explicitly via props

以上方法直接利用以下方式处理store的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取store
store.getState();

// 更新store
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}
componentWillUnmount() {
this.unsubscribe();
}

// dispatch action

store.dispatch({
type: 'TOGGLE_TODO',
id
})

在一个JS文件中比较方便, 但是

  • container component 难测试, reference to a specific store, 难 mock store
  • 难以实现 universal 应用, server 对于每个不同请求提供不同的 store 实例,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});



const { Component } = React;



const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}>
{text}
</li>
);

const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);

// 难界定AddTodo是presentational 还是 container, 因为input 和 button 都是处理UI 而button 中的 onClick 属性dispatch action.

// 因为是比较简单的处理 所以混杂了这两者 逻辑复杂之后再分离.

const AddTodo = ({ store }) => {
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = "";
}}>
Add Todo
</button>
</div>
);
};

// Presentaional component - Link

const Link = ({
active,
children,
onClick
}) => {
if(active){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
onClick()
}}
>
{children}
</a>
)
};

// container component - FilterLink
class FilterLink extends Component {
// dispatch了action但是state不会自动更新 因此forceUpfate()
componentDidMount() {
const { store } = this.props;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const props = this.props;
const { store } = props;
const state = store.getState();

return (
<Link
active={
props.filter === state.visibilityFilter
}
onClick = {() =>
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: props.filter
})
}
>
{props.children}
</Link>
)
}
}

// presentational component
const Footer = ({ store }) => (
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
store={store}
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"
store={store}
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
store={store}
>
Completed
</FilterLink>
</p>
)

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}

class VisibleTodoList extends Component {
componentDidMount() {
// 现在使用 this.props.store 获取 store 内的数据
const { store } = this.props;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const props = this.props;
const { store } = props;
const state = store.getState();

return (
<TodoList
todos={
getVisibleTodos(
state.todos,
state.visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}/>
)
}
}

let nextTodoId = 0;

// every container component needs a reference to the store
const TodoApp = ({ store }) => (
<div>
<AddTodo store={store} />
<VisibleTodoList store={store}/>
<Footer store={store}/>
</div>
);

const { createStore } = Redux;

ReactDOM.render(
<TodoApp store = {createStore(todoApp)}/>,
document.getElementById('root')
)

pass the store down implicitly via context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});



const { Component } = React;



const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}>
{text}
</li>
);

const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);

let nextTodoId = 0;

const AddTodo = (props, { store }) => {
// context.store
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = "";
}}>
Add Todo
</button>
</div>
);
};

AddTodo.contextTypes = {
store: React.PropTypes.object
};

// Presentaional component - Link

const Link = ({
active,
children,
onClick
}) => {
if(active){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
onClick()
}}
>
{children}
</a>
)
};

// container component - FilterLink
class FilterLink extends Component {
// dispatch了action但是state不会自动更新 因此forceUpfate()
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();

return (
<Link
active={
props.filter === state.visibilityFilter
}
onClick = {() =>
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: props.filter
})
}
>
{props.children}
</Link>
)
}
}

FilterLink.contextTypes = {
store: React.PropTypes.object
};

// presentational component
const Footer = () => (
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
>
Completed
</FilterLink>
</p>
)

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}

class VisibleTodoList extends Component {
componentDidMount() {
// 现在使用 this.props.store 获取 store 内的数据
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
// save the reference to the unsubscribe function
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();

return (
<TodoList
todos={
getVisibleTodos(
state.todos,
state.visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}/>
)
}
}

VisibleTodoList.contextTypes = {
// must
store: React.PropTypes.object
};


// every container component needs a reference to the store
const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);

class Provider extends Component {
// 利用 react context pass redux store 到所有 children component 包括 grandchildren...(work at any depth)
getChildContext() {
return {
store: this.props.store
};
}
render() {
// render whatever you provide to it
return this.props.children;
}
}

Provider.childContextTypes = {
// must be specified for store to be passed by context
store: React.PropTypes.object
};

const { createStore } = Redux;

ReactDOM.render(
<Provider store = {createStore(todoApp)}>
<TodoApp />
</Provider>,
document.getElementById('root')
)

// context 很方便 但是违反了 React 所遵循的 explicit data flow 的原则
// context 提供了一个全局变量以便 React 组件获取 redux store 内的值, 但是提供全局变量并不是一个好办法, 除非用于 dependency injection, 否则不要轻易使用 context; 同时 context API 不稳定 不要太过依赖 context

React-Redux Provider

react-redux库 提供了Provider

<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.min.js"></script>

1
const { Provider } = ReactRedux;

替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Provider extends Component {
// 利用 react context pass redux store 到所有 children component 包括 grandchildren...(work at any depth)
getChildContext() {
return {
store: this.props.store
};
}
render() {
// render whatever you provide to it
return this.props.children;
}
}

Provider.childContextTypes = {
// must be specified for store to be passed by context
store: React.PropTypes.object
};

Generate Containers with connect

connect, mapStateToProps, mapDispatchToProps

如果每个component在不同文件中, 就可以直接使用mapStateToProps, mapDispatchToProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});

const { Component } = React;



const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}>
{text}
</li>
);

const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);


const { connect } = ReactRedux;
// connect()() 返回 container


let nextTodoId = 0;

let AddTodo = ({ dispatch }) => {
// context.store
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = "";
}}>
Add Todo
</button>
</div>
);
};

AddTodo = connect()(AddTodo);
//null, null: default behavior: inject just dispatch function, not subscribe to the store


// Presentational component - Link

const Link = ({
active,
children,
onClick
}) => {
if(active){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
onClick()
}}
>
{children}
</a>
)
};

const mapStateToLinkProps = (
state,
ownProps
) => {
return {
active:
ownProps.filter === state.visibilityFilter
};
};

const mapDispatchToLinkProps = (
dispatch,
ownProps
) => {
return {
onClick: () =>
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
})
};
}

const FilterLink = connect(
mapStateToLinkProps,
mapDispatchToLinkProps
)(Link);

// presentational component
const Footer = () => (
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
>
Completed
</FilterLink>
</p>
)

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}





// takes state, return the props to the presentational components, props会在state变化的时候自动更新.

// map redux store to the props of TodoList component
const mapStateToTodoListProps = (state) => {
return {
todos: getVisibleTodos(
state.todos,
state.visibilityFilter
)
};
};

// map redux store to the callback props of TodoList component

const mapDispatchToTodoListProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch({
type: 'TOGGLE_TODO',
id
})
}
};
};

const VisibleTodoList = connect(
mapStateToTodoListProps,
mapDispatchToTodoListProps
)(TodoList);
// curried function that needs to be called twice

// TodoList is the presentational component I want to pass the state to, 不再需要手动subscribe, unsubscribe, 也不用声明 propType, connect handles them for us


// every container component needs a reference to the store
const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);

const { Provider } = ReactRedux;
const { createStore } = Redux;

ReactDOM.render(
<Provider store = {createStore(todoApp)}>
<TodoApp />
</Provider>,
document.getElementById('root')
)

Extract Action Creators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}

return {
...state,
completed: !state.completed
};
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
);
default:
return state;
}
};

// reducer 接受 state 和 action
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};

const { combineReducers } = Redux;
const todoApp = combineReducers({
todos,
visibilityFilter
});

const { Component } = React;



const Todo = ({
onClick,
completed,
text
}) => (
<li
onClick={onClick}
style={{
textDecoration:
completed ?
'line-through' :
'none'
}}>
{text}
</li>
);

const TodoList = ({
todos,
onTodoClick
}) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);


const { connect } = ReactRedux;
// connect()() 返回 container


let nextTodoId = 0;

// extract actionCreators from components

const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
};
};

const toggleTodo = (id) => {
return {
type: 'TOGGLE_TODO',
id
};
};

const setVisibilityFilter = (filter) => {
return {
type: 'SET_VISIBILITY_FILTER',
filter
};
};

let AddTodo = ({ dispatch }) => {
// context.store
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
dispatch(addTodo(input.value));
input.value = "";
}}>
Add Todo
</button>
</div>
);
};

AddTodo = connect()(AddTodo);
//null, null: default behavior: inject just dispatch function, not subscribe to the store


// Presentational component - Link

const Link = ({
active,
children,
onClick
}) => {
if(active){
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault();
onClick()
}}
>
{children}
</a>
)
};

const mapStateToLinkProps = (
state,
ownProps
) => {
return {
active:
ownProps.filter === state.visibilityFilter
};
};

const mapDispatchToLinkProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch(
setVisibilityFilter(ownProps.filter)
);
}
};
}

const FilterLink = connect(
mapStateToLinkProps,
mapDispatchToLinkProps
)(Link);

// presentational component
const Footer = () => (
<p>
Show:
{' '}
<FilterLink
filter="SHOW_ALL"
>
All
</FilterLink>
{' '}
<FilterLink
filter="SHOW_ACTIVE"
>
Active
</FilterLink>
{' '}
<FilterLink
filter="SHOW_COMPLETED"
>
Completed
</FilterLink>
</p>
)

// getVisbleTodos : helper function
const getVisibleTodos = (
todos,
filter
) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(
t => t.completed
);
case 'SHOW_ACTIVE':
return todos.filter(
t => !t.completed
);
}
}





// takes state, return the props to the presentational components, props会在state变化的时候自动更新.

// map redux store to the props of TodoList component
const mapStateToTodoListProps = (state) => {
return {
todos: getVisibleTodos(
state.todos,
state.visibilityFilter
)
};
};

// map redux store to the callback props of TodoList component

const mapDispatchToTodoListProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
};
};

const VisibleTodoList = connect(
mapStateToTodoListProps,
mapDispatchToTodoListProps
)(TodoList);
// curried function that needs to be called twice

// TodoList is the presentational component I want to pass the state to, 不再需要手动subscribe, unsubscribe, 也不用声明 propType, connect handles them for us


// every container component needs a reference to the store
const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);

const { Provider } = ReactRedux;
const { createStore } = Redux;

ReactDOM.render(
<Provider store = {createStore(todoApp)}>
<TodoApp />
</Provider>,
document.getElementById('root')
)

参考