├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── pictures ├── section-redux-async-1.png ├── section-redux-async-2.png ├── section-redux-async-3.png ├── section-redux-async-4.png └── section-redux-async-5.png ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── index.js ├── pages │ ├── Detail │ │ ├── index.js │ │ ├── index.jsx │ │ └── reducer.js │ ├── Home │ │ ├── index.js │ │ ├── index.jsx │ │ └── reducer.js │ ├── List │ │ ├── index.js │ │ ├── index.jsx │ │ └── reducer.js │ ├── Root.jsx │ └── rootReducer.js └── store │ ├── createStore.js │ └── reducerUtils.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Qiutc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux store 的动态注入 2 | 3 | --- 4 | 5 | # 前言 6 | 7 | 在 React + Redux + React-Router 的单页应用架构中,我们将 UI 层( React 组件)和数据层( Redux store )分离开来,以做到更好地管理应用的。 8 | Redux store 既是存储整个应用的数据状态,它的 state 是一个树的数据结构,可以看到如图的例子: 9 | 10 | ![state tree](./pictures/section-redux-async-1.png) 11 | 12 | 而随着应用和业务逻辑的增大,项目中的业务组件和数据状态也会越来越多;在 Router 层面可以使用 React-Router 结合 webpack [做按需加载](https://reacttraining.com/react-router/web/guides/static-routes) 以减少单个 js 包的大小。 13 | 而在 store 层面,随着应用增大,整个结构可能会变的非常的大,应用加载初始化的时候就会去初始化定义整个应用的 store state 和 actions ,这对与内存和资源的大小都是一个比较大的占用和消耗。 14 | 15 | 因此如何做到像 Router 一样地在需要某一块业务组件的时候再去添加这部分的 Redux 相关的数据呢? 16 | 17 | **Redux store 动态注入** 的方案则是用以解决以上的问题。 18 | 19 | 在阅读本文的时候建议了解以下一些概念: 20 | - [Redux 的数据流概念与文档](http://redux.js.org/) 21 | - [React-Router v4 文档](https://reacttraining.com/react-router/web/guides/philosophy) 22 | - [redux 大法好 —— 入门实例 TodoList 23 | ](https://qiutc.me/post/redux-%E5%A4%A7%E6%B3%95%E5%A5%BD-%E2%80%94%E2%80%94-%E5%85%A5%E9%97%A8%E5%AE%9E%E4%BE%8B-TodoList.html) 24 | 25 | --- 26 | 27 | # 方案实践 28 | 29 | ## 原理 30 | 31 | 在 Redux 中,对于 store state 的定义是通过组合 reducer 函数来得到的,也就是说 reducer 决定了最后的整个状态的数据结构。在生成的 store 中有一个 [replaceReducer(nextReducer)](http://redux.js.org/docs/api/Store.html#replaceReducer) 方法,它是 Redux 中的一个高阶 API ,该函数接收一个 `nextReducer` 参数,用于替换 store 中原原有的 reducer ,以此可以改变 store 中原有的状态的数据结构。 32 | 33 | 因此,在初始化 store 的时候,我们可以只定义一些默认公用 reducer(登录状态、全局信息等等),也就是在 `createStore` 函数中只传入这部分相关的 reducer ,这时候其状态的数据结构如下: 34 | 35 | ![state tree](./pictures/section-redux-async-4.png) 36 | 37 | 当我们加载到某一个业务逻辑对应的页面时,比如 `/home`,这部分的业务代码经过 Router 中的处理是按需加载的,在其初始化该部分的组件之前,我们可以在 store 中注入该模块对应的 reducer ,这时候其整体状态的数据结构应该如下: 38 | 39 | ![state tree](./pictures/section-redux-async-5.png) 40 | 41 | 在这里需要做的就是将新增的 reducer 与原有的 reducer 组合,然后通过 `store.replaceReducer` 函数更新其 reducer 来做到在 store 中的动态注入。 42 | 43 | ## 代码 44 | 45 | 话不多说,直接上代码 [https://github.com/TongchengQiu/react-redux-dynamic-injection](https://github.com/TongchengQiu/react-redux-dynamic-injection) ,示例项目的目录结构如下: 46 | 47 | ``` 48 | . 49 | ├── src 50 | | ├── pages 51 | | | ├── Detail 52 | | | | ├── index.js 53 | | | | ├── index.jsx 54 | | | | └── reducer.jsx 55 | | | ├── Home 56 | | | | ├── index.js 57 | | | | ├── index.jsx 58 | | | | └── reducer.jsx 59 | | | ├── List 60 | | | | ├── index.js 61 | | | | ├── index.jsx 62 | | | | └── reducer.jsx 63 | | | ├── Root.js 64 | | | └── rootReducer.js 65 | | ├── store 66 | | | ├── createStore.js 67 | | | ├── location.js 68 | | | └── reducerUtil.js 69 | | └── index.js 70 | └── package.json 71 | ``` 72 | 73 | ### 入口 74 | 75 | 首先来看整个应用的入口文件 `./src/index.js` : 76 | 77 | ```js 78 | import React from 'react'; 79 | import ReactDOM from 'react-dom'; 80 | import Root from './pages/Root'; 81 | 82 | ReactDOM.render(, document.getElementById('root')); 83 | ``` 84 | 85 | 这里所做的就是在 `#root` DOM 元素上挂载渲染 `Root` 组件; 86 | 87 | ### Root 根组件 88 | 89 | 在 `./src/pages/Root.jsx` 中: 90 | 91 | ```js 92 | import React, { Component} from 'react'; 93 | import { Provider } from 'react-redux'; 94 | import { Link, Switch, Route, Router as BrowserRouter } from 'react-router'; 95 | 96 | import createStore from '../store/createStore'; 97 | import { injectReducer } from '../store/reducerUtils'; 98 | import reducer, { key } from './rootReducer'; 99 | 100 | export const store = createStore({} , { 101 | [key]: reducer 102 | }); 103 | 104 | const lazyLoader = (importComponent) => ( 105 | class AsyncComponent extends Component { 106 | state = { C: null } 107 | 108 | async componentDidMount () { 109 | const { default: C } = await importComponent(); 110 | this.setState({ C }); 111 | } 112 | 113 | render () { 114 | const { C } = this.state; 115 | return C ? : null; 116 | } 117 | } 118 | ); 119 | 120 | export default class Root extends Component { 121 | render () { 122 | return ( 123 |
124 | 125 | 126 |
127 | Home 128 |
129 | List 130 |
131 | Detail 132 | 133 | import('./Home'))} 135 | /> 136 | import('./List'))} 138 | /> 139 | import('./Detail'))} 141 | /> 142 | 143 |
144 |
145 |
146 |
147 | ); 148 | } 149 | } 150 | ``` 151 | 152 | 首先是创建了一个 Redux 的 store ,这里的 `createStore` 函数并并没有用 Redux 中原生提供的,而是重新封装了一层来改造它; 153 | 它接收两个参数,第一个是初始化的状态数据,第二个是初始化的 reducer,这里传入的是一个名称为 `key` 的 reducer ,这里的 `key` 和 `reducer` 是在 `./src/pages/rootReducer.js` 中定义的,它用来存储一些通用和全局的状态数据和处理函数的; 154 | `lazyLoader` 函数是用来异步加载组件的,也就是通过不同的 route 来分割代码做按需加载,具体可参考 [code-splitting](https://reacttraining.com/react-router/web/guides/code-splitting) ; 155 | 他的用法就是在 `Route` 组件中传入的 `component` 使用 `lazyLoader(() => import('./List'))` 的方式来导入; 156 | 接下来就是定义了一个 `Root` 组件并暴露,其中 `Provider` 是用来连接 Redux store 和 React 组件,这里需要传入 `store` 对象。 157 | 158 | ### 创建 STORE 159 | 160 | 前面提到,创建 store 的函数是重新封装 Redux 提供的 `createStore` 函数,那么这里面做了什么处理的? 161 | 看 `./src/store/createStore.js` 文件: 162 | 163 | ```js 164 | import { applyMiddleware, compose, createStore } from 'redux'; 165 | import thunk from 'redux-thunk'; 166 | 167 | import { makeAllReducer } from './reducerUtils'; 168 | 169 | export default (initialState = {}, initialReducer = {}) => { 170 | const middlewares = [thunk]; 171 | 172 | const enhancers = []; 173 | 174 | if (process.env.NODE_ENV === 'development') { 175 | const devToolsExtension = window.devToolsExtension; 176 | if (typeof devToolsExtension === 'function') { 177 | enhancers.push(devToolsExtension()); 178 | } 179 | } 180 | 181 | const store = createStore( 182 | makeAllReducer(initialReducer), 183 | initialState, 184 | compose( 185 | applyMiddleware(...middlewares), 186 | ...enhancers 187 | ) 188 | ); 189 | 190 | store.asyncReducers = { 191 | ...initialReducer 192 | }; 193 | 194 | return store; 195 | } 196 | ``` 197 | 198 | 首先在暴露出的 `createStore` 函数中,先是定义了 Redux 中我们需要的一些 `middlewares` 和 `enhancers` : 199 | - [`redux-thunk`](https://github.com/gaearon/redux-thunk) 是用来在 Redux 中更好的处理异步操作的; 200 | - `devToolsExtension` 是在开发环境下可以在 chrome 的 redux devtool 中观察数据变化; 201 | 202 | 之后就是生成了 store ,其中传入的 reducer 是由 `makeAllReducer` 函数生成的; 203 | 最后返回 store ,在这之前给 `store` 增加了一个 `asyncReducers` 的属性对象,它的作用就是用来缓存旧的 reducers 然后与新的 reducer 合并,其具体的操作是在 `injectReducer` 中; 204 | 205 | ### 生成 REDUCER 206 | 207 | 在 `./src/store/reducerUtils.js` 中: 208 | 209 | ```js 210 | import { combineReducers } from 'redux'; 211 | 212 | export const makeAllReducer = (asyncReducers) => combineReducers({ 213 | ...asyncReducers 214 | }); 215 | 216 | export const injectReducer = (store, { key, reducer }) => { 217 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return; 218 | 219 | store.asyncReducers[key] = reducer; 220 | store.replaceReducer(makeAllReducer(store.asyncReducers)); 221 | } 222 | 223 | export const createReducer = (initialState, ACTION_HANDLES) => ( 224 | (state = initialState, action) => { 225 | const handler = ACTION_HANDLES[action.type]; 226 | return handler ? handler(state, action) : state; 227 | } 228 | ); 229 | ``` 230 | 231 | 在初始化创建 store 的时候,其中的 reducer 是由 `makeAllReducer` 函数来生成的,这里接收一个 `asyncReducers` 参数,它是一个包含 `key` 和 `reducer` 函数的对象; 232 | 233 | `injectReducer` 函数是用来在 store 中动态注入 reducer 的,首先判断当前 store 中的 `asyncReducers` 是否存在该 reducer ,如果存在则不需要做处理,而这里的 `asyncReducers` 则是存储当前已有的 reducers ; 234 | 如果需要新增 reducer ,则在 `asyncReducers` 对象中加入新增的 reducer ,然后通过 `makeAllReducer` 函数返回原有的 reducer 和新的 reducer 的合并,并通过 `store.replaceReducer` 函数替换 `store` 中的 reducer。 235 | 236 | `createReducer` 函数则是用来生成一个新的 reducer 。 237 | 238 | ### 定义 ACTION 与 REDUCER 239 | 240 | 关于如何定义一个 action 与 reducer 这里以 rootReducer 的定义来示例 `./src/pages/rootReducer.js` : 241 | 242 | ```js 243 | import { createReducer } from '../store/reducerUtils'; 244 | 245 | export const key = 'root'; 246 | 247 | export const ROOT_AUTH = `${key}/ROOT_AUTH`; 248 | 249 | export const auth = () => ( 250 | (dispatch, getState) => ( 251 | new Promise((resolve) => { 252 | setTimeout(() => { 253 | dispatch({ 254 | type: ROOT_AUTH, 255 | payload: true 256 | }); 257 | resolve(); 258 | }, 300); 259 | }) 260 | ) 261 | ); 262 | 263 | export const actions = { 264 | auth 265 | }; 266 | 267 | const ACTION_HANLDERS = { 268 | [ROOT_AUTH]: (state, action) => ({ 269 | ...state, 270 | auth: action.payload 271 | }) 272 | }; 273 | 274 | const initalState = { 275 | auth: false 276 | }; 277 | 278 | export default createReducer(initalState, ACTION_HANLDERS); 279 | ``` 280 | 281 | 这一步其实比较简单,主要是结合 `redux-thunk` 的异步操作做了一个模拟 auth 验证的函数; 282 | 283 | 首先是定义了这个 reducer 对应的 state 在根节点中的 key ; 284 | 然后定义了 actions ; 285 | 之后定义了操作函数 auth ,其实就是触发一个 `ROOT_AUTH` 的 action; 286 | 之后定义 actions 对应的处理函数,存储在 `ACTION_HANLDERS` 对象中; 287 | 最后通过 `createReducer` 函数生成一个 reducer 并暴露出去; 288 | 289 | 对于在业务组件中需要动态注入的 reducer 的定义也是按照这套模式,具体可以观察每个业务组件中的 `reducer.js` 文件; 290 | 291 | ### 动态注入 REDUCER 292 | 293 | 在前面,我们生成了一个 store 并赋予其初始化的 state 和 reducer ,当我们加载到某一块业务组件的时候,则需要动态注入该组件对应的一些 state 和 reducer。 294 | 295 | 以 Home 组件为示例,当加载到该组件的时候,首先执行 `index.js` 文件: 296 | 297 | ```js 298 | import { injectReducer } from '../../store/reducerUtils'; 299 | import { store } from '../Root'; 300 | import Home from './index.jsx'; 301 | import reducer, { key } from './reducer'; 302 | 303 | injectReducer(store, { key, reducer }); 304 | 305 | export default Home; 306 | ``` 307 | 308 | 首先是在 store 中插入其业务模块对于的 reducer: `injectReducer(store, { key, reducer })` ,之后直接暴露该组件; 309 | 因此在该组件初始化之前,在 store 中就注入了其对应的 state 和 reducer; 310 | 311 | 而在 `index.jsx` 中对于 Redux 的使用和其标准的用法并无区别;感兴趣可以阅读该部分的代码。 312 | 313 | ### 运行示例 314 | 315 | clone 仓库: 316 | 317 | ```bash 318 | git clone https://github.com/TongchengQiu/react-redux-dynamic-injection.git 319 | ``` 320 | 321 | 初始化: 322 | 323 | ```bash 324 | npm i -d 325 | ``` 326 | 327 | 运行: 328 | 329 | ```bash 330 | npm start 331 | ``` 332 | 333 | 可以看到启动了项目 `http://localhost:3000/`; 334 | 335 | 通过 Redux Devtool ,可以看到这里的初始状态为: 336 | 337 | ![state tree](./pictures/section-redux-async-2.png) 338 | 339 | 点击 **List** 到 List 对应的页面,可以看到原来的状态变为了: 340 | 341 | ![state tree](./pictures/section-redux-async-3.png) 342 | 343 | 也就是说在加载到 List 组件的时候,动态插入了这部分对应的 state 和 reducer。 344 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-dynamic-injection", 3 | "version": "0.1.0", 4 | "author": "qiutc", 5 | "email": "tongchengqiu@gmail.com", 6 | "private": true, 7 | "dependencies": { 8 | "react": "^15.6.1", 9 | "react-dom": "^15.6.1", 10 | "react-redux": "^5.0.6", 11 | "react-router-dom": "^4.2.2", 12 | "react-scripts": "1.0.12", 13 | "redux": "^3.7.2", 14 | "redux-thunk": "^2.2.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pictures/section-redux-async-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TongchengQiu/react-redux-dynamic-injection/21b2f30e37170a03eedb0d2969747623cca65b87/pictures/section-redux-async-1.png -------------------------------------------------------------------------------- /pictures/section-redux-async-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TongchengQiu/react-redux-dynamic-injection/21b2f30e37170a03eedb0d2969747623cca65b87/pictures/section-redux-async-2.png -------------------------------------------------------------------------------- /pictures/section-redux-async-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TongchengQiu/react-redux-dynamic-injection/21b2f30e37170a03eedb0d2969747623cca65b87/pictures/section-redux-async-3.png -------------------------------------------------------------------------------- /pictures/section-redux-async-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TongchengQiu/react-redux-dynamic-injection/21b2f30e37170a03eedb0d2969747623cca65b87/pictures/section-redux-async-4.png -------------------------------------------------------------------------------- /pictures/section-redux-async-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TongchengQiu/react-redux-dynamic-injection/21b2f30e37170a03eedb0d2969747623cca65b87/pictures/section-redux-async-5.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TongchengQiu/react-redux-dynamic-injection/21b2f30e37170a03eedb0d2969747623cca65b87/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './pages/Root'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/pages/Detail/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducerUtils'; 2 | import { store } from '../Root'; 3 | import Detail from './index.jsx'; 4 | import reducer, { key } from './reducer'; 5 | 6 | injectReducer(store, { key, reducer }); 7 | 8 | export default Detail; 9 | -------------------------------------------------------------------------------- /src/pages/Detail/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { increment, doubleAsync, key } from './reducer'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | count: state[key].count 8 | }); 9 | 10 | const mapDispatchTpProps = { 11 | increment: () => increment(4), 12 | doubleAsync 13 | }; 14 | 15 | class Detail extends Component { 16 | render() { 17 | const { count, increment, doubleAsync } = this.props; 18 | 19 | return ( 20 |
21 |

Detail Counter: { count }

22 | 23 |
24 | 25 |
26 | ); 27 | } 28 | }; 29 | 30 | export default connect(mapStateToProps, mapDispatchTpProps)(Detail); 31 | -------------------------------------------------------------------------------- /src/pages/Detail/reducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../../store/reducerUtils'; 2 | 3 | export const key = 'detail'; 4 | 5 | export const DETAIL_INCREMENT = `${key}/DETAIL_INCREMENT`; 6 | export const DETAIL_DOUBLE_ASYNC = `${key}/DETAIL_DOUBLE_ASYNC`; 7 | 8 | export const increment = (value = 1) => ( 9 | { 10 | type: DETAIL_INCREMENT, 11 | payload: value 12 | } 13 | ); 14 | 15 | export const doubleAsync = () => ( 16 | (dispatch, getState) => ( 17 | new Promise((resolve) => { 18 | setTimeout(() => { 19 | dispatch({ 20 | type: DETAIL_DOUBLE_ASYNC, 21 | payload: null 22 | }); 23 | resolve(); 24 | }, 200); 25 | }) 26 | ) 27 | ); 28 | 29 | export const actions = { 30 | increment, 31 | doubleAsync 32 | }; 33 | 34 | const ACTION_HANLDERS = { 35 | [DETAIL_INCREMENT]: (state, action) => ({ 36 | ...state, 37 | count: state.count + action.payload 38 | }), 39 | [DETAIL_DOUBLE_ASYNC]: (state, action) => ({ 40 | ...state, 41 | count: state.count * 2 42 | }), 43 | }; 44 | 45 | const initalState = { 46 | count: 0 47 | }; 48 | 49 | export default createReducer(initalState, ACTION_HANLDERS); 50 | -------------------------------------------------------------------------------- /src/pages/Home/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducerUtils'; 2 | import { store } from '../Root'; 3 | import Home from './index.jsx'; 4 | import reducer, { key } from './reducer'; 5 | 6 | injectReducer(store, { key, reducer }); 7 | 8 | export default Home; 9 | -------------------------------------------------------------------------------- /src/pages/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { increment, doubleAsync, key } from './reducer'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | count: state[key].count 8 | }); 9 | 10 | const mapDispatchTpProps = { 11 | increment: () => increment(1), 12 | doubleAsync 13 | }; 14 | 15 | class Home extends Component { 16 | render() { 17 | const { count, increment, doubleAsync } = this.props; 18 | 19 | return ( 20 |
21 |

Counter: { count }

22 | 23 |
24 | 25 |
26 | ); 27 | } 28 | }; 29 | 30 | export default connect(mapStateToProps, mapDispatchTpProps)(Home); 31 | -------------------------------------------------------------------------------- /src/pages/Home/reducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../../store/reducerUtils'; 2 | 3 | export const key = 'home'; 4 | 5 | export const HOME_INCREMENT = `${key}/HOME_INCREMENT`; 6 | export const HOME_DOUBLE_ASYNC = `${key}/HOME_DOUBLE_ASYNC`; 7 | 8 | export const increment = (value = 1) => ( 9 | { 10 | type: HOME_INCREMENT, 11 | payload: value 12 | } 13 | ); 14 | 15 | export const doubleAsync = () => ( 16 | (dispatch, getState) => ( 17 | new Promise((resolve) => { 18 | setTimeout(() => { 19 | dispatch({ 20 | type: HOME_DOUBLE_ASYNC, 21 | payload: null 22 | }); 23 | resolve(); 24 | }, 200); 25 | }) 26 | ) 27 | ); 28 | 29 | export const actions = { 30 | increment, 31 | doubleAsync 32 | }; 33 | 34 | const ACTION_HANLDERS = { 35 | [HOME_INCREMENT]: (state, action) => ({ 36 | ...state, 37 | count: state.count + action.payload 38 | }), 39 | [HOME_DOUBLE_ASYNC]: (state, action) => ({ 40 | ...state, 41 | count: state.count * 2 42 | }), 43 | }; 44 | 45 | const initalState = { 46 | count: 0 47 | }; 48 | 49 | export default createReducer(initalState, ACTION_HANLDERS); 50 | -------------------------------------------------------------------------------- /src/pages/List/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducerUtils'; 2 | import { store } from '../Root'; 3 | import List from './index.jsx'; 4 | import reducer, { key } from './reducer'; 5 | 6 | injectReducer(store, { key, reducer }); 7 | 8 | export default List; 9 | -------------------------------------------------------------------------------- /src/pages/List/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { increment, doubleAsync, key } from './reducer'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | count: state[key].count 8 | }); 9 | 10 | const mapDispatchTpProps = { 11 | increment: () => increment(2), 12 | doubleAsync 13 | }; 14 | 15 | class List extends Component { 16 | render() { 17 | const { count, increment, doubleAsync } = this.props; 18 | 19 | return ( 20 |
21 |

List Counter: { count }

22 | 23 |
24 | 25 |
26 | ); 27 | } 28 | }; 29 | 30 | export default connect(mapStateToProps, mapDispatchTpProps)(List); 31 | -------------------------------------------------------------------------------- /src/pages/List/reducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../../store/reducerUtils'; 2 | 3 | export const key = 'list'; 4 | 5 | export const LIST_INCREMENT = `${key}/LIST_INCREMENT`; 6 | export const LIST_DOUBLE_ASYNC = `${key}/LIST_DOUBLE_ASYNC`; 7 | 8 | export const increment = (value = 1) => ( 9 | { 10 | type: LIST_INCREMENT, 11 | payload: value 12 | } 13 | ); 14 | 15 | export const doubleAsync = () => ( 16 | (dispatch, getState) => ( 17 | new Promise((resolve) => { 18 | setTimeout(() => { 19 | dispatch({ 20 | type: LIST_DOUBLE_ASYNC, 21 | payload: null 22 | }); 23 | resolve(); 24 | }, 200); 25 | }) 26 | ) 27 | ); 28 | 29 | export const actions = { 30 | increment, 31 | doubleAsync 32 | }; 33 | 34 | const ACTION_HANLDERS = { 35 | [LIST_INCREMENT]: (state, action) => ({ 36 | ...state, 37 | count: state.count + action.payload 38 | }), 39 | [LIST_DOUBLE_ASYNC]: (state, action) => ({ 40 | ...state, 41 | count: state.count * 2 42 | }), 43 | }; 44 | 45 | const initalState = { 46 | count: 0 47 | }; 48 | 49 | export default createReducer(initalState, ACTION_HANLDERS); 50 | -------------------------------------------------------------------------------- /src/pages/Root.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Link, Switch, Route, BrowserRouter as Router } from 'react-router-dom'; 4 | 5 | import createStore from '../store/createStore'; 6 | import reducer, { key } from './rootReducer'; 7 | 8 | export const store = createStore({} , { 9 | [key]: reducer 10 | }); 11 | 12 | const lazyLoader = (importComponent) => ( 13 | class AsyncComponent extends Component { 14 | state = { C: null } 15 | 16 | async componentDidMount () { 17 | const { default: C } = await importComponent(); 18 | this.setState({ C }); 19 | } 20 | 21 | render () { 22 | const { C } = this.state; 23 | return C ? : null; 24 | } 25 | } 26 | ); 27 | 28 | export default class Root extends Component { 29 | render () { 30 | return ( 31 |
32 | 33 | 34 |
35 | Home 36 |
37 | List 38 |
39 | Detail 40 | 41 | import('./Home'))} 43 | /> 44 | import('./List'))} 46 | /> 47 | import('./Detail'))} 49 | /> 50 | 51 |
52 |
53 |
54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../store/reducerUtils'; 2 | 3 | export const key = 'root'; 4 | 5 | export const ROOT_AUTH = `${key}/ROOT_AUTH`; 6 | 7 | export const auth = () => ( 8 | (dispatch, getState) => ( 9 | new Promise((resolve) => { 10 | setTimeout(() => { 11 | dispatch({ 12 | type: ROOT_AUTH, 13 | payload: true 14 | }); 15 | resolve(); 16 | }, 300); 17 | }) 18 | ) 19 | ); 20 | 21 | export const actions = { 22 | auth 23 | }; 24 | 25 | const ACTION_HANLDERS = { 26 | [ROOT_AUTH]: (state, action) => ({ 27 | ...state, 28 | auth: action.payload 29 | }) 30 | }; 31 | 32 | const initalState = { 33 | auth: false 34 | }; 35 | 36 | export default createReducer(initalState, ACTION_HANLDERS); 37 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import { makeAllReducer } from './reducerUtils'; 5 | 6 | export default (initialState = {}, initialReducer = {}) => { 7 | const middlewares = [thunk]; 8 | 9 | const enhancers = []; 10 | 11 | if (process.env.NODE_ENV === 'development') { 12 | const devToolsExtension = window.devToolsExtension; 13 | if (typeof devToolsExtension === 'function') { 14 | enhancers.push(devToolsExtension()); 15 | } 16 | } 17 | 18 | const store = createStore( 19 | makeAllReducer(initialReducer), 20 | initialState, 21 | compose( 22 | applyMiddleware(...middlewares), 23 | ...enhancers 24 | ) 25 | ); 26 | 27 | store.asyncReducers = { 28 | ...initialReducer 29 | }; 30 | 31 | return store; 32 | } 33 | -------------------------------------------------------------------------------- /src/store/reducerUtils.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | export const makeAllReducer = (asyncReducers) => combineReducers({ 4 | ...asyncReducers 5 | }); 6 | 7 | export const injectReducer = (store, { key, reducer }) => { 8 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return; 9 | 10 | store.asyncReducers[key] = reducer; 11 | store.replaceReducer(makeAllReducer(store.asyncReducers)); 12 | } 13 | 14 | export const createReducer = (initialState, ACTION_HANDLES) => ( 15 | (state = initialState, action) => { 16 | const handler = ACTION_HANDLES[action.type]; 17 | return handler ? handler(state, action) : state; 18 | } 19 | ); 20 | --------------------------------------------------------------------------------