├── .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 | 
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 | 
36 |
37 | 当我们加载到某一个业务逻辑对应的页面时,比如 `/home`,这部分的业务代码经过 Router 中的处理是按需加载的,在其初始化该部分的组件之前,我们可以在 store 中注入该模块对应的 reducer ,这时候其整体状态的数据结构应该如下:
38 |
39 | 
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 | 
338 |
339 | 点击 **List** 到 List 对应的页面,可以看到原来的状态变为了:
340 |
341 | 
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 |
--------------------------------------------------------------------------------