({ initialState, reducers });
203 | const actions = { home };
204 |
205 | expect(() => { home.dispatch.show(); }).toThrowError("call");
206 | expect(() => home.query).toThrowError("call");
207 |
208 | const { redux } = createStore(actions);
209 | expect(home.query).toBe(1);
210 |
211 | let flag = 0;
212 | redux.subscribe(() => flag = 1);
213 | home.dispatch.increase({ inc: 1 });
214 | expect(home.query).toBe(2);
215 | expect(flag).toBe(1);
216 |
217 | redux.subscribe(() => flag = 2);
218 | home.dispatch.decrease(2);
219 | expect(home.query).toBe(0);
220 | expect(flag).toBe(2);
221 |
222 | redux.subscribe(() => flag = 3);
223 | home.dispatch.replace({ count: 3 });
224 | expect(home.query).toBe(3);
225 | expect(flag).toBe(3);
226 |
227 | redux.subscribe(() => flag = 4);
228 | home.dispatch.show();
229 | expect(home.query).toBe(10);
230 | expect(flag).toBe(4);
231 |
232 | expect(() => { home.query = 0; }).toThrowError("modify");
233 | });
234 | });
235 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | Imdux
3 | 🌈 A redux helper for react & hooks & typescript developers.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ### 特点
14 |
15 | - 🚀简单高效:完全去除了redux冗余低效的样板代码,提供一把全自动的生产力工具。
16 | - :shaved_ice: 类型安全:面向typescript用户,100%类型安全,同时摒弃interface类型预定义,改用infer,实现了state和dispatch类型推导,极大地减少了类型定义代码。
17 | - ✈️目前未来:拥抱react hooks,便于typescript的类型检查和代码复用。
18 | - :cocktail:最佳实践:Imdux的目的不仅仅是提供一个库,更希望的是提供一个解决方案,探索一种react hooks的最佳实践。
19 |
20 | #### 开始
21 |
22 | 首先,创建一个react项目:
23 |
24 | ```shell
25 | npx create-react-app imdux-demo
26 | ```
27 |
28 | 安装imdux,imdux依赖于 immer,redux,react-redux :
29 |
30 | ```shell
31 | yarn add imdux immer redux react-redux
32 | ```
33 |
34 | 创建一个简单的项目结构:
35 |
36 | ```js
37 | ├── package.json
38 | ├── public
39 | │ ├── favicon.ico
40 | │ ├── index.html
41 | │ └── robots.txt
42 | ├── src
43 | │ ├── App.js
44 | │ ├── index.js
45 | │ └── store
46 | │ ├── counter.reducers.js
47 | │ └── index.js
48 | └── yarn.lock
49 | ```
50 |
51 |
52 | 打开`src/store/counter.reducers.js`,输入代码:
53 |
54 |
55 | ```js
56 | import { createAction } from "imdux";
57 |
58 | const initialState = {
59 | value: 0
60 | };
61 |
62 | const reducers = {
63 | increase(draft, payload) {
64 | draft.value += payload;
65 | },
66 | decrease(draft, payload) {
67 | draft.value -= payload;
68 | }
69 | };
70 |
71 | export const counter = createAction({ initialState, reducers });
72 | ```
73 |
74 |
75 | 打开`src/store/index.js`,创建一个store:
76 |
77 |
78 | ```js
79 | import { createStore } from "imdux";
80 |
81 | import { counter } from "./counter.reducers";
82 |
83 | export const store = createStore({ counter }, { devtool: true });
84 | export const { Dispatch, Query } = store;
85 | ```
86 |
87 |
88 | 打开`src/App.js`,创建一个`App`:
89 |
90 |
91 | ```js
92 | import React from "react";
93 | import { useSelector } from "react-redux";
94 |
95 | import { Dispatch } from "./store";
96 |
97 | export function App() {
98 | // 取出counter中的值,如果这个值改变,那么组件会自动更新
99 | const value = useSelector(store => store.counter.value);
100 | return (
101 |
102 |
{value}
103 | {/* 通过Dispatch触发状态更新 */}
104 |
105 |
106 | );
107 | }
108 | ```
109 |
110 |
111 | 最后,打开`src/index.js`,注入redux的store:
112 |
113 |
114 | ```js
115 | import React from "react";
116 | import ReactDOM from "react-dom";
117 | import { Provider } from "react-redux";
118 |
119 | import { App } from "./App";
120 | import { store } from "./store";
121 |
122 | ReactDOM.render(
123 | {/* 注入 */}
124 |
125 | ,
126 | document.getElementById("root")
127 | );
128 | ```
129 |
130 | enjoy it~ 很简单,对不对?
131 |
132 | 你可以在浏览器中打开这个例子: [javascript](https://codesandbox.io/s/imdux-start-javascript-3049f?fontsize=14&hidenavigation=1&theme=dark) [typescript](https://codesandbox.io/s/imdux-start-typescript-7wz5u?fontsize=14&hidenavigation=1&theme=dark)
133 |
134 | 打开redux的devtool,通过点击`increase`和`decrease`button,我们可以看到状态变更的历史记录:
135 |
136 | 
137 |
138 | ### 命名空间
139 |
140 | 上面的例子中,如果有多个counter,可以在`reducers`中用命名空间隔离:
141 |
142 | ```js
143 | import { createAction } from "imdux";
144 |
145 | const initialState = {
146 | first: 0,
147 | last: 0,
148 | };
149 |
150 | const reducers = {
151 | first: {
152 | increase(draft, payload) {
153 | draft.first += payload;
154 | },
155 | decrease(draft, payload) {
156 | draft.first -= payload;
157 | }
158 | },
159 | last: {
160 | increase(draft, payload) {
161 | draft.last += payload;
162 | },
163 | decrease(draft, payload) {
164 | draft.last -= payload;
165 | }
166 | }
167 | };
168 |
169 | export const counter = createAction({ initialState, reducers });
170 |
171 | ```
172 |
173 | ```js
174 | export function App() {
175 | const first = useSelector(store => store.counter.first);
176 | const last = useSelector(store => store.counter.last);
177 | return (
178 |
179 |
{first}
180 |
181 |
182 |
183 | {last}
184 |
185 |
186 |
187 | );
188 | }
189 | ```
190 |
191 | 
192 |
193 | 命名空间是可以多级嵌套,应当根据项目情况自由组织,推荐把相关的状态变更放在一个命名空间下。
194 |
195 | ### getState()
196 |
197 | 在redux中,某些情况下需要`同步`获得状态的最新值,[redux提供了getState()接口来实现](https://redux.js.org/basics/store)。
198 |
199 | 在imdux中,`createStore`导出的`Query`内置了getter,可以达到和`getState()`一样的效果。
200 |
201 | 例如:
202 |
203 | ```js
204 | import { createStore } from "imdux";
205 |
206 | import { counter } from "./counter.reducers";
207 |
208 | export const store = createStore({ counter }, { devtool: true });
209 | export const { Dispatch, Query } = store;
210 |
211 | console.log(store.redux.getState().counter); // { value: 0 }
212 | console.log(Query.counter); // { value: 0 }
213 |
214 | console.log(store.redux.getState().counter === Query.counter); // true
215 |
216 | ```
217 |
218 | ### Typescript
219 |
220 | 在redux中实现100%的类型检查是imdux的初衷。对于typescript用户,推荐在`counter.reducers.ts`中带上类型:
221 |
222 | ```ts
223 | import { createAction } from "imdux";
224 |
225 | type State = typeof initialState; // 获得类型
226 | type Reducers = typeof reducers; // 获得类型
227 |
228 | const initialState = {
229 | value: 0
230 | };
231 |
232 | const reducers = {
233 | increase(draft: State, payload: number) { // draft的类型为State
234 | draft.value += payload;
235 | },
236 | decrease(draft: State, payload: number) { // draft的类型为State
237 | draft.value -= payload;
238 | }
239 | };
240 |
241 | export const counter = createAction({ initialState, reducers }); // 注入类型
242 | ```
243 |
244 | 在`src/store/index.ts`中,导出`Query`的类型,改写`useSelector`的函数定义:
245 |
246 | ```ts
247 | import { createStore } from "imdux";
248 | import { useSelector as useReduxSelector } from "react-redux";
249 |
250 | import { counter } from "./counter.reducers";
251 |
252 | export const store = createStore({ counter }, { devtool: true });
253 | export const { Dispatch, Query } = store;
254 |
255 | export type Store = typeof Query;
256 |
257 | export function useSelector(
258 | selector: (state: Store) => TSelected,
259 | equalityFn?: (left: TSelected, right: TSelected) => boolean
260 | ) {
261 | return useReduxSelector(selector, equalityFn);
262 | }
263 | ```
264 |
265 | 在`src/App.tsx`中,使用改写后的`useSelector`,这样就可以很轻松地获得typescript的类型检查和代码提示:
266 |
267 | 
268 |
269 | 你可以在浏览器中打开这个例子: [javascript](https://codesandbox.io/s/imdux-start-javascript-3049f?fontsize=14&hidenavigation=1&theme=dark) [typescript](https://codesandbox.io/s/imdux-start-typescript-7wz5u?fontsize=14&hidenavigation=1&theme=dark)
270 |
271 | 按住`ctrl`键,鼠标左键点击`increase`,可以准确跳转到`reducers.increase`:
272 |
273 | 
274 |
275 |
276 | ### 和immer的关系
277 |
278 | immer是一个强大的immutable库,它可以非常直观、高效地创建immutable数据:
279 |
280 | ```ts
281 | import produce from "immer";
282 |
283 | const user = {
284 | name: "Jack",
285 | friends: [{ name: "Tom" }, { name: "Jerry" }]
286 | };
287 |
288 | const user2 = produce(user, draft => {
289 | draft.name = "James";
290 | });
291 |
292 | console.log(user2.friends === user.friends); // true
293 |
294 | const user3 = produce(user, draft => {
295 | draft.friends.push({ name: "Vesper" });
296 | });
297 |
298 | console.log(user3.friends === user.friends); // false
299 | console.log(user3.friends[0] === user.friends[0]); // true
300 | ```
301 |
302 | 他的原理如图:
303 |
304 | 
305 |
306 | 相对于通过扩展运算符...,Array.slice等方式来创建immutable对象,immer通过一个参数为draft的函数来修改原对象,然后将修改的过程打包生成一个新对象,原对象不变,符合人的思维直觉。
307 |
308 | 详情请参考immer文档:https://immerjs.github.io/immer/docs/introduction
309 |
310 | 其实,从名字你就可以看出端倪:imdux = im + dux = immer + redux
311 |
312 | imdux做的事情其实很简单,就是将redux中的reducer,和immer中的draft函数合二为一:
313 |
314 | 1. 利用修改draft不会影响原来对象的特性,在reducer内直接读取和修改draft
315 | 2. 利用immer中的produce函数,来生成下一个immutable状态,然后提交给redux,触发状态更新
316 |
317 | 基于以上原理,imdux中的reducer必须是**同步的**。
318 |
319 | ### 异步请求
320 |
321 | imdux推荐两种异步操作解决办法:
322 | 1. 基于hooks的异步方案
323 | 2. 基于全局函数的异步方案
324 |
325 | #### 基于hooks的异步方案
326 |
327 | 一个常见的滚动翻页代码如下:
328 |
329 | ```js
330 | // 定义一个名称为news的action,并使用createStore初始化
331 |
332 | import { createAction } from "imdux";
333 |
334 | const initialState = {
335 | list: [],
336 | page: 0,
337 | isLoading: false,
338 | isEndOfList: false
339 | };
340 |
341 | const reducers = {
342 | addNews(draft, list) {
343 | draft.list.push(...list);
344 | },
345 | addPage(draft) {
346 | if (draft.isEndOfList || draft.isLoading) return;
347 | draft.page++;
348 | },
349 | startLoading(draft) {
350 | draft.isLoading = true;
351 | },
352 | stopLoading(draft) {
353 | draft.isLoading = false;
354 | },
355 | reachEndOfList(draft) {
356 | draft.isEndOfList = true;
357 | }
358 | };
359 |
360 | export const news = createAction({ initialState, reducers });
361 |
362 | ```
363 |
364 | ```js
365 | // 使用Dispatch.news.addPage更新news.page,触发request异步操作
366 |
367 | import * as React from "react";
368 | import { useSelector } from "react-redux";
369 |
370 | import { Dispatch, Store } from "./store";
371 |
372 | export default function App() {
373 | const news = useSelector(p => p.news);
374 |
375 | React.useEffect(() => {
376 | request(news.page);
377 | }, [news.page]);
378 |
379 | const request = async (page) => {
380 | Dispatch.news.startLoading();
381 | try {
382 | const response = await Api.getNewsList(page);
383 | if (!Api.isError(response)) {
384 | if (response.data.list.length === 0) {
385 | Dispatch.news.reachEndOfList();
386 | } else {
387 | Dispatch.news.addNews(response.data.list);
388 | }
389 | } else {
390 | alert(response.message);
391 | }
392 | } catch (e) {
393 | alert(e.message);
394 | } finally {
395 | Dispatch.news.stopLoading();
396 | }
397 | };
398 |
399 | return (
400 |
401 | {news.list.map(item =>
402 |
{item.title}
403 | )}
404 | {news.isLoading ? "加载中" : news.isEndOfList ? "加载完毕" : ""}
405 |
406 | );
407 | }
408 | ```
409 |
410 | #### 基于全局函数的异步方案
411 | 当一个异步方法需要在多个component中复用的时候,可以定义一个全局函数,在函数内使用`Dispatch`触发状态更新,使用`Query`获得状态的最新值,然后在需要的component中import这个函数即可。
412 |
413 | 需要注意的是,这种方案非常简单,但是会造成全局变量污染问题,请酌情使用。
414 |
415 | ### 最佳实践
416 |
417 | // TODO
418 |
419 | ### License
420 |
421 | MIT
422 |
--------------------------------------------------------------------------------