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的三个基本原则:
保存应用当前的状态: getState()方法;
能够分发action: dispatch()方法;
创建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;const store = createStore(counter);console .log(store.getState());store.dispatch({ type : 'INCREMENT' }); console .log(store.getState());store.dispatch({ type : 'DECREMENT' }); console .log(store.getState());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 = []; const getState = () => state; const dispatch = (action ) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; const subscribe = (listener ) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l!== listener); }; }; dispatch({}); 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); unsubscribeFoo();
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; } }; 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 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> ) }; } const render = () => { ReactDOM.render( <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; } }; 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 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>) 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; } }; 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> ); }; 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> ) } } const Footer = () => ( <p> Show: {' ' } <FilterLink filter="SHOW_ALL" > All </FilterLink> {' '} <FilterLink filter="SHOW_ACTIVE"x > Active </ FilterLink> {' ' } <FilterLink filter="SHOW_COMPLETED" > Completed </FilterLink> </ p>) 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 .unsubscribe = store.subscribe(() => this .forceUpdate() ); } 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' ) )
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.getState(); componentDidMount() { this .unsubscribe = store.subscribe(() => this .forceUpdate() ); } componentWillUnmount() { this .unsubscribe(); } 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; } }; 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> ); }; 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> ) } } 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>) 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() { const { store } = this .props; this .unsubscribe = store.subscribe(() => this .forceUpdate() ); } 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 ;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; } }; 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 }; 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 }; const Footer = () => ( <p> Show: {' ' } <FilterLink filter="SHOW_ALL" > All </FilterLink> {' '} <FilterLink filter="SHOW_ACTIVE" > Active </ FilterLink> {' ' } <FilterLink filter="SHOW_COMPLETED" > Completed </FilterLink> </ p>) 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() { const { store } = this .context; this .unsubscribe = store.subscribe(() => this .forceUpdate() ); } 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 = { store: React.PropTypes.object }; 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 { getChildContext() { return { store: this .props.store }; } render() { return this .props.children; } } Provider.childContextTypes = { 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; } }; 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); 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') )
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; } }; 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); 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') )
参考