1
1
import React from 'react' ;
2
2
import ReactDOM from 'react-dom' ;
3
- import { combineReducers , createStore } from 'redux' ;
3
+ import { applyMiddleware , combineReducers , createStore } from 'redux' ;
4
4
import { Provider , connect } from 'react-redux' ;
5
+ import { createLogger } from 'redux-logger' ;
6
+ import createSagaMiddleware , { delay } from 'redux-saga' ;
7
+ import { put , takeEvery } from 'redux-saga/effects' ;
8
+ import { schema , normalize } from 'normalizr' ;
9
+ import uuid from 'uuid/v4' ;
5
10
import './index.css' ;
6
11
12
+ // filters
13
+
14
+ const VISIBILITY_FILTERS = {
15
+ SHOW_COMPLETED : item => item . completed ,
16
+ SHOW_INCOMPLETED : item => ! item . completed ,
17
+ SHOW_ALL : item => true ,
18
+ } ;
19
+
20
+ // schemas
21
+
22
+ const todoSchema = new schema . Entity ( 'todo' ) ;
23
+
7
24
// action types
8
25
9
26
const TODO_ADD = 'TODO_ADD' ;
10
27
const TODO_TOGGLE = 'TODO_TOGGLE' ;
11
28
const FILTER_SET = 'FILTER_SET' ;
29
+ const NOTIFICATION_HIDE = 'NOTIFICATION_HIDE' ;
30
+ const TODO_ADD_WITH_NOTIFICATION = 'TODO_ADD_WITH_NOTIFICATION' ;
12
31
13
32
// reducers
14
33
15
34
const todos = [
16
- { id : '0' , name : 'learn redux' } ,
17
- { id : '1' , name : 'learn mobx' } ,
35
+ { id : '1' , name : 'Hands On: Redux Standalone with advanced Actions' } ,
36
+ { id : '2' , name : 'Hands On: Redux Standalone with advanced Reducers' } ,
37
+ { id : '3' , name : 'Hands On: Bootstrap App with Redux' } ,
38
+ { id : '4' , name : 'Hands On: Naive Todo with React and Redux' } ,
39
+ { id : '5' , name : 'Hands On: Sophisticated Todo with React and Redux' } ,
40
+ { id : '6' , name : 'Hands On: Connecting State Everywhere' } ,
41
+ { id : '7' , name : 'Hands On: Todo with advanced Redux' } ,
42
+ { id : '8' , name : 'Hands On: Todo but more Features' } ,
43
+ { id : '9' , name : 'Hands On: Todo with Notifications' } ,
44
+ { id : '10' , name : 'Hands On: Hacker News with Redux' } ,
18
45
] ;
19
46
20
- function todoReducer ( state = todos , action ) {
47
+ const normalizedTodos = normalize ( todos , [ todoSchema ] ) ;
48
+
49
+ const initialTodoState = {
50
+ entities : normalizedTodos . entities . todo ,
51
+ ids : normalizedTodos . result ,
52
+ } ;
53
+
54
+ function todoReducer ( state = initialTodoState , action ) {
21
55
switch ( action . type ) {
22
56
case TODO_ADD : {
23
57
return applyAddTodo ( state , action ) ;
@@ -30,16 +64,18 @@ function todoReducer(state = todos, action) {
30
64
}
31
65
32
66
function applyAddTodo ( state , action ) {
33
- const todo = Object . assign ( { } , action . todo , { completed : false } ) ;
34
- return state . concat ( todo ) ;
67
+ const todo = { ...action . todo , completed : false } ;
68
+ const entities = { ...state . entities , [ todo . id ] : todo } ;
69
+ const ids = [ ...state . ids , action . todo . id ] ;
70
+ return { ...state , entities, ids } ;
35
71
}
36
72
37
73
function applyToggleTodo ( state , action ) {
38
- return state . map ( todo =>
39
- todo . id === action . todo . id
40
- ? Object . assign ( { } , todo , { completed : ! todo . completed } )
41
- : todo
42
- ) ;
74
+ const id = action . todo . id ;
75
+ const todo = state . entities [ id ] ;
76
+ const toggledTodo = { ... todo , completed : ! todo . completed } ;
77
+ const entities = { ... state . entities , [ id ] : toggledTodo } ;
78
+ return { ... state , entities } ;
43
79
}
44
80
45
81
function filterReducer ( state = 'SHOW_ALL' , action ) {
@@ -55,8 +91,47 @@ function applySetFilter(state, action) {
55
91
return action . filter ;
56
92
}
57
93
94
+ function notificationReducer ( state = { } , action ) {
95
+ switch ( action . type ) {
96
+ case TODO_ADD : {
97
+ return applySetNotifyAboutAddTodo ( state , action ) ;
98
+ }
99
+ case NOTIFICATION_HIDE : {
100
+ return applyRemoveNotification ( state , action ) ;
101
+ }
102
+ default : return state ;
103
+ }
104
+ }
105
+
106
+ function applySetNotifyAboutAddTodo ( state , action ) {
107
+ const { name, id } = action . todo ;
108
+ return { ...state , [ id ] : 'Todo Created: ' + name } ;
109
+ }
110
+
111
+ function applyRemoveNotification ( state , action ) {
112
+ const {
113
+ [ action . id ] : notificationToRemove ,
114
+ ...restNotifications ,
115
+ } = state ;
116
+ return restNotifications ;
117
+ }
118
+
58
119
// action creators
59
120
121
+ function doAddTodoWithNotification ( id , name ) {
122
+ return {
123
+ type : TODO_ADD_WITH_NOTIFICATION ,
124
+ todo : { id, name } ,
125
+ } ;
126
+ }
127
+
128
+ function doHideNotification ( id ) {
129
+ return {
130
+ type : NOTIFICATION_HIDE ,
131
+ id
132
+ } ;
133
+ }
134
+
60
135
function doAddTodo ( id , name ) {
61
136
return {
62
137
type : TODO_ADD ,
@@ -78,27 +153,146 @@ function doSetFilter(filter) {
78
153
} ;
79
154
}
80
155
156
+ // selectors
157
+
158
+ function getTodosAsIds ( state ) {
159
+ return state . todoState . ids
160
+ . map ( id => state . todoState . entities [ id ] )
161
+ . filter ( VISIBILITY_FILTERS [ state . filterState ] )
162
+ . map ( todo => todo . id ) ;
163
+ }
164
+
165
+ function getTodo ( state , todoId ) {
166
+ return state . todoState . entities [ todoId ] ;
167
+ }
168
+
169
+ function getNotifications ( state ) {
170
+ return getArrayOfObject ( state . notificationState ) ;
171
+ }
172
+
173
+ function getArrayOfObject ( object ) {
174
+ return Object . keys ( object ) . map ( key => object [ key ] ) ;
175
+ }
176
+
177
+ // sagas
178
+
179
+ function * watchAddTodoWithNotification ( ) {
180
+ yield takeEvery ( TODO_ADD_WITH_NOTIFICATION , handleAddTodoWithNotification ) ;
181
+ }
182
+
183
+ function * handleAddTodoWithNotification ( action ) {
184
+ const { todo } = action ;
185
+ const { id, name } = todo ;
186
+ yield put ( doAddTodo ( id , name ) ) ;
187
+ yield delay ( 5000 ) ;
188
+ yield put ( doHideNotification ( id ) ) ;
189
+ }
190
+
81
191
// store
82
192
83
193
const rootReducer = combineReducers ( {
84
194
todoState : todoReducer ,
85
195
filterState : filterReducer ,
196
+ notificationState : notificationReducer ,
86
197
} ) ;
87
198
88
- const store = createStore ( rootReducer ) ;
199
+ const logger = createLogger ( ) ;
200
+ const saga = createSagaMiddleware ( ) ;
201
+
202
+ const store = createStore (
203
+ rootReducer ,
204
+ undefined ,
205
+ applyMiddleware ( saga , logger )
206
+ ) ;
207
+
208
+ saga . run ( watchAddTodoWithNotification ) ;
89
209
90
- // view layer
210
+ // components
91
211
92
212
function TodoApp ( ) {
93
- return < ConnectedTodoList /> ;
213
+ return (
214
+ < div >
215
+ < ConnectedFilter />
216
+ < ConnectedTodoCreate />
217
+ < ConnectedTodoList />
218
+ < ConnectedNotifications />
219
+ </ div >
220
+ ) ;
221
+ }
222
+
223
+ function Notifications ( { notifications } ) {
224
+ return (
225
+ < div >
226
+ { notifications . map ( note => < div key = { note } > { note } </ div > ) }
227
+ </ div >
228
+ ) ;
94
229
}
95
230
96
- function TodoList ( { todos } ) {
231
+ function Filter ( { onSetFilter } ) {
97
232
return (
98
233
< div >
99
- { todos . map ( todo => < ConnectedTodoItem
100
- key = { todo . id }
101
- todo = { todo }
234
+ Show
235
+ < button
236
+ type = "text"
237
+ onClick = { ( ) => onSetFilter ( 'SHOW_ALL' ) } >
238
+ All</ button >
239
+ < button
240
+ type = "text"
241
+ onClick = { ( ) => onSetFilter ( 'SHOW_COMPLETED' ) } >
242
+ Completed</ button >
243
+ < button
244
+ type = "text"
245
+ onClick = { ( ) => onSetFilter ( 'SHOW_INCOMPLETED' ) } >
246
+ Incompleted</ button >
247
+ </ div >
248
+ ) ;
249
+ }
250
+
251
+ class TodoCreate extends React . Component {
252
+ constructor ( props ) {
253
+ super ( props ) ;
254
+
255
+ this . state = {
256
+ value : '' ,
257
+ } ;
258
+
259
+ this . onCreateTodo = this . onCreateTodo . bind ( this ) ;
260
+ this . onChangeName = this . onChangeName . bind ( this ) ;
261
+ }
262
+
263
+ onChangeName ( event ) {
264
+ this . setState ( { value : event . target . value } ) ;
265
+ }
266
+
267
+ onCreateTodo ( event ) {
268
+ this . props . onAddTodo ( this . state . value ) ;
269
+ this . setState ( { value : '' } ) ;
270
+ event . preventDefault ( ) ;
271
+ }
272
+
273
+ render ( ) {
274
+ return (
275
+ < div >
276
+ < form onSubmit = { this . onCreateTodo } >
277
+ < input
278
+ type = "text"
279
+ placeholder = "Add Todo..."
280
+ value = { this . state . value }
281
+ onChange = { this . onChangeName }
282
+ />
283
+ < button type = "submit" > Add</ button >
284
+ </ form >
285
+ </ div >
286
+ ) ;
287
+ }
288
+ }
289
+
290
+ function TodoList ( { todosAsIds } ) {
291
+ return (
292
+ < div >
293
+ { todosAsIds . map ( todoId => < ConnectedTodoItem
294
+ key = { todoId }
295
+ todoId = { todoId }
102
296
/> ) }
103
297
</ div >
104
298
) ;
@@ -119,24 +313,53 @@ function TodoItem({ todo, onToggleTodo }) {
119
313
) ;
120
314
}
121
315
122
- function mapStateToProps ( state ) {
316
+ // Connecting React and Redux
317
+
318
+ function mapStateToPropsList ( state ) {
123
319
return {
124
- todos : state . todoState ,
320
+ todosAsIds : getTodosAsIds ( state ) ,
125
321
} ;
126
322
}
127
323
128
- function mapDispatchToProps ( dispatch ) {
324
+ function mapStateToPropsItem ( state , props ) {
325
+ return {
326
+ todo : getTodo ( state , props . todoId ) ,
327
+ } ;
328
+ }
329
+
330
+ function mapDispatchToPropsItem ( dispatch ) {
129
331
return {
130
332
onToggleTodo : id => dispatch ( doToggleTodo ( id ) ) ,
131
333
} ;
132
334
}
133
335
134
- const ConnectedTodoList = connect ( mapStateToProps ) ( TodoList ) ;
135
- const ConnectedTodoItem = connect ( null , mapDispatchToProps ) ( TodoItem ) ;
336
+ function mapDispatchToPropsCreate ( dispatch ) {
337
+ return {
338
+ onAddTodo : name => dispatch ( doAddTodoWithNotification ( uuid ( ) , name ) ) ,
339
+ } ;
340
+ }
341
+
342
+ function mapDispatchToPropsFilter ( dispatch ) {
343
+ return {
344
+ onSetFilter : filterType => dispatch ( doSetFilter ( filterType ) ) ,
345
+ } ;
346
+ }
347
+
348
+ function mapStateToPropsNotifications ( state , props ) {
349
+ return {
350
+ notifications : getNotifications ( state ) ,
351
+ } ;
352
+ }
353
+
354
+ const ConnectedTodoList = connect ( mapStateToPropsList ) ( TodoList ) ;
355
+ const ConnectedTodoItem = connect ( mapStateToPropsItem , mapDispatchToPropsItem ) ( TodoItem ) ;
356
+ const ConnectedTodoCreate = connect ( null , mapDispatchToPropsCreate ) ( TodoCreate ) ;
357
+ const ConnectedFilter = connect ( null , mapDispatchToPropsFilter ) ( Filter ) ;
358
+ const ConnectedNotifications = connect ( mapStateToPropsNotifications ) ( Notifications ) ;
136
359
137
360
ReactDOM . render (
138
361
< Provider store = { store } >
139
362
< TodoApp />
140
363
</ Provider > ,
141
364
document . getElementById ( 'root' )
142
- ) ;
365
+ ) ;
0 commit comments