Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/xiaojundebug/zhuangtai
一个固执己见的状态管理工具,基于 RxJS,暂只支持 React
https://github.com/xiaojundebug/zhuangtai
react rxjs state store
Last synced: 21 days ago
JSON representation
一个固执己见的状态管理工具,基于 RxJS,暂只支持 React
- Host: GitHub
- URL: https://github.com/xiaojundebug/zhuangtai
- Owner: xiaojundebug
- License: mit
- Created: 2022-11-27T08:42:28.000Z (about 2 years ago)
- Default Branch: dev
- Last Pushed: 2024-11-08T08:55:20.000Z (about 2 months ago)
- Last Synced: 2024-11-08T09:37:30.349Z (about 2 months ago)
- Topics: react, rxjs, state, store
- Language: TypeScript
- Homepage:
- Size: 354 KB
- Stars: 5
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
> [!WARNING]
> _该库处于开发构想状态,还未发布到 NPM,不要尝试用于生产环境_# zhuangtai
一个固执己见的状态管理工具,基于 RxJS,暂只支持 React
## 特点
- 简单易用,TypeScript 类型声明完善,没有啰嗦的样板代码
- 内置多个常用的功能插件,采用不可变状态模型,状态可追溯
- 数据处理逻辑(Store)可以和 React 组件完全分离,便于移植
- 基于 RxJS,便于有能力者扩展使用
- React 状态原子化,没有重复的渲染
- 兼容 React 18 并发模式## 快速上手
### 安装依赖
```bash
npm i zhuangtai rxjs
```### 简单示例
```tsx
// counter.store.ts
import { Store } from 'zhuangtai'export interface CounterState {
count: number
}class Counter extends Store {
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 (
count is: {count}
counter.increase()}>increase
counter.decrease()}>decrease
)
}
```## Store
Store 作为一个 class,其作用是管理数据的存放,处理数据的增删改查,原则上它是可以脱离 React 的
### 简单示例
```ts
import { Store } from 'zhuangtai'export interface CounterState {
count: number
}class Counter extends Store {
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()
```### Getters
#### `state`
> Type: `S`
一个 getter,等同于 [store.getState()](#getState)
### 实例方法
#### `setState`
> Type: `(state: Partial, replace?: boolean) => void`
设置 state,默认通过 `Object.assign` 和原属性进行合并,你可以通过 `replace` 参数跳过该行为
#### `getState`
> Type: `() => S`
获取 Store 最新的 state
#### `select`
> Type: `(selector?: Selector | null, comparer?: Comparer) => Observable`
根据 selector 创建一个 RxJS Observable,一般用来监听某些属性的变动,selector 传空表示监听任意属性变动,
你还可以通过自定义 `comparer` 来决定 observable 的值是否发出,默认是 `Object.is````ts
// 监听 count 变动
const count$ = store.select(state => state.count)
count$.subscribe(val => {
console.log(`count is: ${val}`)
})
```_`select` 方法其实是 RxJS 中 `observable.pipe(map(selector), distinctUntilChanged(comparer))` 的简单封装_
## Plugins
可以通过插件机制对 Store 进行功能扩展,目前内置了 `immer`、`persist` 与 `history` 插件
### Immer Plugin
首先你需要安装 [immer](https://github.com/immerjs/immer)
```bash
npm i immer
``````ts
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 })
}
}
```### Persist Plugin
用于将 state 持久化到本地
```ts
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',
})
}
// ...
}
```#### Options
##### `name`(必选)
> Type: `string`
存到 storage 中的唯一的 key 值
##### `partialize`(可选)
> Type: `(state: S) => Partial`
> Default: `state => state`
只保存需要的字段,默认保存所有
##### `getStorage`(可选)
> Type: `() => StateStorage`
> Default: `localStorage`
自定义 storage
##### `serialize`(可选)
> Type: `Serizlizer`
> Default: `JSON.stringify`
自定义序列化器
##### `deserialize`(可选)
> Type: `Deserializer`
> Default: `JSON.parse`
自定义反序列化器
##### `version`(可选)
> Type: `number`
> Default: `0`
如果持久化的 state 版本与此处指定的版本不匹配,则跳过状态合并
### History Plugin
一个方便的插件,用于实现 `undo`、`redo` 功能
```ts
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` 插件
#### Options
##### `limit`(可选)
> Type: `number`
最大保存历史数量限制
> Type: `number`
> Default: `Infinite`
#### APIs
##### `store.history.undo`
> Type: `() => boolean`
状态撤销,不能的话 return `false`
##### `store.history.redo`
> Type: `() => boolean`
状态重做,不能的话 return `false`
##### `store.history.go`
> Type: `(step: number) => boolean`
`step` 为负数代表撤销次数,为正数代表重做次数,如果传入一个超出历史记录范围的数字,则取开头或者末尾的那条记录,传 `0` 会 return `false`
##### `store.history.getPast()`
> Type: `() => S[]`
获取可以撤销的历史记录
##### `store.history.getFuture()`
> Type: `() => S[]`
获取可以重做的历史记录,如果你设置了一个新的 state,该记录会被清空
### 自定义插件
你也可以根据业务需求开发自己的插件,让我们以 log 插件为例
```ts
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')
}
// ...
}
```---
## 与 React 一起使用
Store 只是一个普通 class,要想它在 React 中使用,必须用一种方法使两者关联起来
### `useStore`
React 自定义 Hook,用于将 Store 中的 state 绑定到 React 组件
```tsx
import { useStore } from 'zhuangtai/react'
```它支持多种状态选择方式
1. 使用 Selector(可以自由选择 & 组合状态)
```ts
const count = useStore(counter, state => state.count)
const { doubleCount } = useStore(counter, state => ({ doubleCount: state.count * 2 }))
```2. 使用 Keys(写法更便捷,满足大多数使用场景)
```ts
const { count } = useStore(counter, ['count'])
```3. 不传第二个参数(不推荐,代表选择所有状态,会导致多余渲染)
```ts
const state = useStore(counter)
```推荐使用方式 1 和 2,不会导致多余渲染(默认通过浅比对来判断数据变动,你可以通过第三个参数传入自定义比对函数)
**一个使用自定义比对函数的例子**
```ts
import _ from 'lodash'function deepEqual(a, b) {
return _.isEqual(a, b)
}const { foo, bar } = useStore(store, ['foo', 'bar'], deepEqual)
```## FAQ
为什么选择用 class 作为 Store 而不是函数风格?
- 个人感觉 OOP 风格代码更容易维护
- 业务都写在函数中我感觉很乱,而且会有暂时性死区问题而 class 没有这种困扰
- 我可以忽略 this 带来的困扰怎样在本地运行此项目?
此项目没有使用 monorepo 进行管理,你可以通过 `npm link` 方式进行本地开发 & 预览
1. 进入项目根目录,执行 `npm link`,代码修改后执行 `npm run build`
2. 进入 `examples/counter` 文件夹,先执行 `npm link zhuangtai`,然后执行 `npm run dev`怎样在 SSR 框架中使用?
正常来说 `zhuangtai` 可以在 SSR 框架中使用,但是 `persist` 插件会有些问题,因为 `persist` 插件会在服务端渲染期间访问浏览器 storage,这将导致服务端渲染失败,你需要自定义 `getStorage` 字段来修复此问题,参考以下代码
```ts
const dummyStorage = {
getItem: (name: string) => null,
setItem: (name: string, value: string) => {},
removeItem: (name: string) => {},
}
const isBrowser = typeof window !== 'undefined'
const myStorage = isBrowser ? localStorage : dummyStorageconst persistPlugin = persist({
// ...
getStorage: () => myStorage,
})
```除此之外还有一些其它问题,服务端和客户端渲染时由于状态不一致可能会导致“水合错误”(客户端拿到的是持久化后的状态,但服务端渲染时拿不到),在 `nextjs` 中我们可以通过[动态组件](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading)的方式解决此问题
怎样配合 react-tracked 使用?
如果你觉得通过 Selector 或 Keys 选择状态的方式太过繁琐,但是不传又会产生多余渲染,那么 [react-tracked](https://github.com/dai-shi/react-tracked) 是一个不错的选择,它会跟踪你真正使用的 state,未使用的 state 变动时不会触发组件渲染
首先你需要先安装它
```bash
npm i react-tracked
```在 `zhuangtai` 中使用 `react-tracked` 需要做一些适配操作,参考下方代码
```tsx
import { State, Store } from 'zhuangtai'
import { useStore } from 'zhuangtai/react'
import { createTrackedSelector } from 'react-tracked'// 你可以把这个函数抽离到公共模块中,该函数时是为了适配 createTrackedSelector
function createUseSelector(store: Store) {
return useStore.bind(null, store as any) as unknown as (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 (
foo: {state.foo}
)
}
```