ts-lib-easy-peasy

TypeScript redux framework easy-peasy

Why

有一个知乎问题

你为什么不使用 TypeScript? - 绍毅的回答 - 知乎 https://www.zhihu.com/question/273619114/answer/1470574675

TS 没坑,框架没坑,TS+框架会有很多坑

redux with ts

redux + JavaScript 时可以使用 rematch, 用的比较舒服, 但切换到 ts 的时候要加上类型可太难了

在研究上面 rematch ts 的时候发现了 easy-peasy , 试用下来虽然有些小问题, 但都能解决, 能写起来比较舒服. 研究差不多了用 easy-peasy 重构了 clash-config-manager

@reduxjs/toolkit

  • 官方实现, 没有啥毛病
  • createAsyncThunk 极其繁琐, 个人不喜欢. (主要是繁琐…)

问题

models 不见了

https://github.com/ctrlplusb/easy-peasy/issues/616

  • 在使用 webpack + es module 的时候, webpack 会使用 defineProperty 会被 easy-peasy 认为是 computed, 然后就被过滤掉了, 详见 createStore 方法第一行.
  • 解决办法可以在传给 createStore 之前先 cloneDeep 一下
1
2
3
4
5
6
7
8
9
10
11
const store = createStore(_.cloneDeep(models))
export default store
export type StoreModel = typeof models

if (process.env.NODE_ENV === 'development') {
if ((module as any).hot) {
;(module as any).hot.accept('./models', () => {
store.reconfigure(_.cloneDeep(models)) // 👈 Here is the magic
})
}
}

model 的写法

分开写 type 和 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
import {Action, action} from 'easy-peasy'

interface TodosModel {
todos: string[]
addTodo: Action<TodosModel, string>
}

const todosModel: TodosModel = {
todos: [],
addTodo: action((state, payload) => {
state.todos.push(payload)
}),
}
  • 官方文档上的写法, 是没什么毛病
  • 但是 model 一旦大了, 相当费劲, 想新增一个 thunk, 得去 interface 新增类型, 然后在 todoModel 写实现

easy-peasy-decorators

https://github.com/easypeasy-community/decorators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {Model, Property, Action, createStore} from 'easy-peasy-decorators'

@Model('todos')
class TodoModel {
@Property()
public items = ['Create store', 'Wrap application', 'Use store']

@Action()
add(payload: string) {
this.items.push(payload)
}
}

interface IStoreModel {
todos: TodoModel
}

export const store = createStore<IStoreModel>()

优点

  • 写起来很爽, 避免了 1 中的问题.

缺点

  • decorators 因为标准未推进, 有废弃之势. (see mobx / clipanion / …) 但只要用的爽也不是大问题.
  • decorators 缺少类型约束, 如 https://github.com/easypeasy-community/decorators/issues/4
  • 仓库只有 9 个 commit, 而且很久没有更新了, 缺少维护, 用户群比较小.
  • 试用之后不想再这个用了.

export default new (class ModelClass implements IState{})

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
import _ from 'lodash'
import {message} from 'antd'
import {Action, action, Thunk, thunk, ThunkOn, thunkOn} from 'easy-peasy'
import {subscribeToClash} from '@util/fn/clash'
import {Subscribe} from '@define'
import {StoreModel} from '@store'
import storage from '@storage'
import {setStateFactory, SetStatePayload} from '@common/model/setState'

const SUBSCRIBE_LIST_STORAGE_KEY = 'subscribe_list'
const SUBSCRIBE_DETAIL_STORAGE_KEY = 'subscribe_detail'

interface IState {
inited: boolean
list: Subscribe[]
detail: any
}

export default new (class SubscribeModel implements IState {
/**
* state
*/

inited = false
list = []
detail = {}

/**
* helper
*/

setState: Action<SubscribeModel, SetStatePayload<IState>> = setStateFactory<SubscribeModel>()

load: Thunk<SubscribeModel> = thunk((actions) => {
const list = storage.get(SUBSCRIBE_LIST_STORAGE_KEY)
const detail = storage.get(SUBSCRIBE_DETAIL_STORAGE_KEY)
actions.setState({inited: true, list, detail})
})

init: Thunk<SubscribeModel> = thunk((actions, payload, {getState}) => {
const {inited} = getState()
if (inited) return
actions.load()
})

persist: Thunk<SubscribeModel> = thunk((actions, payliad, {getState}) => {
const {list, detail} = getState()
storage.set(SUBSCRIBE_LIST_STORAGE_KEY, list)
storage.set(SUBSCRIBE_DETAIL_STORAGE_KEY, detail)
})

add: Thunk<SubscribeModel, Subscribe> = thunk((actions, payload) => {
actions.setState(({list}) => {
list.push(payload)
})
actions.persist()
})

edit: Thunk<SubscribeModel, Subscribe & {editItemIndex: number}> = thunk((actions, payload) => {
const {url, name, id, editItemIndex} = payload
actions.setState(({list}) => {
list[editItemIndex] = {url, name, id}
})
actions.persist()
})

del: Thunk<SubscribeModel, number> = thunk((actions, index) => {
actions.setState(({list}) => {
list.splice(index, 1)
})
actions.persist()
})

update: Thunk<SubscribeModel, {url: string; silent?: boolean; forceUpdate?: boolean}> = thunk(
async (actions, payload) => {
const {url, silent = false, forceUpdate: forceUpdate = false} = payload
// TODO: ts
let servers: any[]
try {
servers = await subscribeToClash({url, forceUpdate})
} catch (e) {
message.error('更新订阅出错: \n' + e.stack || e)
throw e
}

if (!silent) {
message.success('更新订阅成功')
}

actions.setState(({detail}) => {
detail[url] = servers
})
actions.persist()
}
)

onInit: ThunkOn<SubscribeModel, any, StoreModel> = thunkOn(
(actions, storeActions) => storeActions.global.init,
(actions) => {
actions.init()
}
)

onReload: ThunkOn<SubscribeModel, any, StoreModel> = thunkOn(
(actions, storeActions) => storeActions.global.reload,
(actions) => {
actions.load()
}
)
})()
thunk 被当成 state 处理了的问题(actions 为空)

使用 new ModelClass 这种方式传进 easy-peasy.createStore 之后, actions 都为空, 都被当成 state 来处理了. 具体原因不详.

解决办法: 在 cloneDeep 的时候将 class instance 转成 plain object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import _ from 'lodash'
import * as models from './models'

const getModels = <T>(models: T) =>
_.cloneDeepWith(models, (val) => {
if (val && typeof val === 'object' && val.constructor !== Object) {
return {...val} // if it's a Model instance, make it plain object
}
}) as T

const store = createStore(getModels(models))
export default store
export type StoreModel = typeof models

if (process.env.NODE_ENV === 'development') {
if ((module as any).hot) {
;(module as any).hot.accept('./models', () => {
store.reconfigure(getModels(models)) // 👈 Here is the magic
})
}
}

Tips

useStoreDispatch().<model_namespace>.action()

可以像 rematch 一样直接使用 dispatch.xxx

useEasy / useEasyActions / useEasyState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const {useStore, useStoreActions, useStoreDispatch, useStoreState} = createTypedHooks<StoreModel>()
export {useStore, useStoreActions, useStoreDispatch, useStoreState}

export const useEasyState = <NSP extends keyof StoreModel>(nsp: NSP) => {
const state = useStoreState((state) => state[nsp], shallowEqual)
return state
}
export const useEasyActions = <NSP extends keyof StoreModel>(nsp: NSP) => {
const actions = useStoreActions((actions) => {
return actions[nsp]
})
return actions
}
export const useEasy = <NSP extends keyof StoreModel>(nsp: NSP) => {
const state = useEasyState(nsp)
const actions = useEasyActions(nsp)
return useMemo(() => {
return {
...state,
...actions,
}
}, [state, actions])
}

More

to be continued …