Warning
该库处于开发构想状态,还未发布到 NPM,不要尝试用于生产环境
一个固执己见的状态管理工具,基于 RxJS,暂只支持 React
- 简单易用,TypeScript 类型声明完善,没有啰嗦的样板代码
- 内置多个常用的功能插件,采用不可变状态模型,状态可追溯
- 数据处理逻辑(Store)可以和 React 组件完全分离,便于移植
- 基于 RxJS,便于有能力者扩展使用
- React 状态原子化,没有重复的渲染
- 兼容 React 18 并发模式
npm i zhuangtai rxjs
// counter.store.ts
import { Store } from 'zhuangtai'
export interface CounterState {
count: number
}
class Counter extends Store<CounterState> {
constructor() {
// initial state
super({ count: 0 })
}
increase() {
this.setState({ count: this.state.count + 1 })
}
decrease() {
this.setState({ count: this.state.count - 1 })
}
}
export const counter = new Counter()
// App.tsx
import { useStore } from 'zhuangtai/react'
function App() {
// 不用担心其他 state 变动会触发多余渲染,内部已经处理
const count = useStore(counter, s => s.count)
return (
<div className="App">
<p>count is: {count}</p>
<div className="actions">
<button onClick={() => counter.increase()}>increase</button>
<button onClick={() => counter.decrease()}>decrease</button>
</div>
</div>
)
}
Store 作为一个 class,其作用是管理数据的存放,处理数据的增删改查,原则上它是可以脱离 React 的
import { Store } from 'zhuangtai'
export interface CounterState {
count: number
}
class Counter extends Store<CounterState> {
constructor() {
// initial state
super({ count: 0 })
}
increase() {
this.setState({ count: this.state.count + 1 })
}
decrease() {
this.setState({ count: this.state.count - 1 })
}
}
export const counter = new Counter()
Type:
S
一个 getter,等同于 store.getState()
Type:
(state: Partial<S>, replace?: boolean) => void
设置 state,默认通过 Object.assign
和原属性进行合并,你可以通过 replace
参数跳过该行为
Type:
() => S
获取 Store 最新的 state
Type:
(selector?: Selector<S, V> | null, comparer?: Comparer<V>) => Observable<V>
根据 selector 创建一个 RxJS Observable,一般用来监听某些属性的变动,selector 传空表示监听任意属性变动,
你还可以通过自定义 comparer
来决定 observable 的值是否发出,默认是 Object.is
// 监听 count 变动
const count$ = store.select(state => state.count)
count$.subscribe(val => {
console.log(`count is: ${val}`)
})
select
方法其实是 RxJS 中 observable.pipe(map(selector), distinctUntilChanged(comparer))
的简单封装
可以通过插件机制对 Store 进行功能扩展,目前内置了 immer
、persist
与 history
插件
首先你需要安装 immer
npm i immer
import { Store, State } from 'zhuangtai'
import immer from 'zhuangtai/plugins/immer'
class Counter extends Store<{ count: number }> {
constructor() {
super({ count: 0 })
immer(this)
}
increase() {
// immer 写法
this.setState(s => {
s.count++
})
}
decrease() {
// 普通写法
this.setState({ count: this.state.count - 1 })
}
}
用于将 state 持久化到本地
import { Store, State } from 'zhuangtai'
import persist from 'zhuangtai/plugins/persist'
class Counter extends Store<{ count: number }> {
constructor() {
super({ count: 0 })
persist(this, {
name: 'COUNTER_STATE',
})
}
// ...
}
Type:
string
存到 storage 中的唯一的 key 值
Type:
(state: S) => Partial<S>
Default:
state => state
只保存需要的字段,默认保存所有
Type:
() => StateStorage
Default:
localStorage
自定义 storage
Type:
Serizlizer
Default:
JSON.stringify
自定义序列化器
Type:
Deserializer
Default:
JSON.parse
自定义反序列化器
Type:
number
Default:
0
如果持久化的 state 版本与此处指定的版本不匹配,则跳过状态合并
一个方便的插件,用于实现 undo
、redo
功能
import { Store, State } from 'zhuangtai'
import history from 'zhuangtai/plugins/history'
class Counter extends Store<{ count: number }> {
constructor() {
super({ count: 0 })
// ...Other plugins
history(this, { limit: 10 })
}
// ...
}
如果你需要使用多个插件,你应该确保最后再应用 history
插件
Type:
number
最大保存历史数量限制
Type:
number
Default:
Infinite
Type:
() => boolean
状态撤销,不能的话 return false
Type:
() => boolean
状态重做,不能的话 return false
Type:
(step: number) => boolean
step
为负数代表撤销次数,为正数代表重做次数,如果传入一个超出历史记录范围的数字,则取开头或者末尾的那条记录,传 0
会 return false
Type:
() => S[]
获取可以撤销的历史记录
Type:
() => S[]
获取可以重做的历史记录,如果你设置了一个新的 state,该记录会被清空
你也可以根据业务需求开发自己的插件,让我们以 log 插件为例
import { Store, Plugin } from 'zhuangtai'
import { pairwise } from 'rxjs/operators'
function logger(store: Store, scope: string) {
store
.select()
.pipe(pairwise())
.subscribe(([prev, next]) => {
console.log(
`${scope}:
%cprev state: %o
%cnext state: %o
`,
'color: #999',
prev,
'color: #22c55e',
next
)
})
}
class Counter extends Store<{ count: number }> {
constructor() {
super({ count: 0 })
logger(this, 'Counter')
}
// ...
}
Store 只是一个普通 class,要想它在 React 中使用,必须用一种方法使两者关联起来
React 自定义 Hook,用于将 Store 中的 state 绑定到 React 组件
import { useStore } from 'zhuangtai/react'
它支持多种状态选择方式
- 使用 Selector(可以自由选择 & 组合状态)
const count = useStore(counter, state => state.count)
const { doubleCount } = useStore(counter, state => ({ doubleCount: state.count * 2 }))
- 使用 Keys(写法更便捷,满足大多数使用场景)
const { count } = useStore(counter, ['count'])
- 不传第二个参数(不推荐,代表选择所有状态,会导致多余渲染)
const state = useStore(counter)
推荐使用方式 1 和 2,不会导致多余渲染(默认通过浅比对来判断数据变动,你可以通过第三个参数传入自定义比对函数)
一个使用自定义比对函数的例子
import _ from 'lodash'
function deepEqual(a, b) {
return _.isEqual(a, b)
}
const { foo, bar } = useStore(store, ['foo', 'bar'], deepEqual)
为什么选择用 class 作为 Store 而不是函数风格?
- 个人感觉 OOP 风格代码更容易维护
- 业务都写在函数中我感觉很乱,而且会有暂时性死区问题而 class 没有这种困扰
- 我可以忽略 this 带来的困扰
怎样在本地运行此项目?
此项目没有使用 monorepo 进行管理,你可以通过 npm link
方式进行本地开发 & 预览
- 进入项目根目录,执行
npm link
,代码修改后执行npm run build
- 进入
examples/counter
文件夹,先执行npm link zhuangtai
,然后执行npm run dev
怎样在 SSR 框架中使用?
正常来说 zhuangtai
可以在 SSR 框架中使用,但是 persist
插件会有些问题,因为 persist
插件会在服务端渲染期间访问浏览器 storage,这将导致服务端渲染失败,你需要自定义 getStorage
字段来修复此问题,参考以下代码
const dummyStorage = {
getItem: (name: string) => null,
setItem: (name: string, value: string) => {},
removeItem: (name: string) => {},
}
const isBrowser = typeof window !== 'undefined'
const myStorage = isBrowser ? localStorage : dummyStorage
const persistPlugin = persist<Counter>({
// ...
getStorage: () => myStorage,
})
除此之外还有一些其它问题,服务端和客户端渲染时由于状态不一致可能会导致“水合错误”(客户端拿到的是持久化后的状态,但服务端渲染时拿不到),在 nextjs
中我们可以通过动态组件的方式解决此问题
怎样配合 react-tracked 使用?
如果你觉得通过 Selector 或 Keys 选择状态的方式太过繁琐,但是不传又会产生多余渲染,那么 react-tracked 是一个不错的选择,它会跟踪你真正使用的 state,未使用的 state 变动时不会触发组件渲染
首先你需要先安装它
npm i react-tracked
在 zhuangtai
中使用 react-tracked
需要做一些适配操作,参考下方代码
import { State, Store } from 'zhuangtai'
import { useStore } from 'zhuangtai/react'
import { createTrackedSelector } from 'react-tracked'
// 你可以把这个函数抽离到公共模块中,该函数时是为了适配 createTrackedSelector
function createUseSelector<S extends State>(store: Store<S>) {
return useStore.bind(null, store as any) as unknown as <V>(selector: (state: S) => V) => V
}
class MyStore extends Store<{ foo: string; bar: number }> {
constructor() {
super({ foo: 'abc', bar: 123 })
}
// ...
}
const myStore = new MyStore()
const useMyStore = createTrackedSelector(createUseSelector(myStore))
const App = () => {
const state = useMyStore()
// 这里只使用了 foo,所以 bar 变动时候时不会触发渲染的
return (
<div>
<p>foo: {state.foo}</p>
</div>
)
}