Object-oriented state management for react
- Lightweight, based merely on React hooks:
useStateanduseEffect - Simple and intuitive API:
const { name, age } = user.use(['name', 'age']) - Subscription based, diff subscribed properties, no unnecessary rerender of components
https://github.com/ShenHongFei/react-object-model
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Model } from 'react-object-model'
function Example () {
const { name } = user.use(['name'])
const { value } = counter.use(['value'])
return <div className='example'>
<div className='user'>
<h3>User:</h3>
<div>
<button onClick={() => {
// set model properties by `model.set` method
// (`user.name = 'Tom'` is not allowed)
user.set({ name: 'Tom', age: 16 })
}}>login</button>
<button onClick={() => {
// will not trigger rerender as we don't subscribe to user's age in this component
// (except for the first `user.set` call as the model doesn't know the previous state)
user.set({ age: Math.trunc(100 * Math.random()) })
}}>set age</button>
<button onClick={() => {
// but we could get current value of model's property without subscription
alert(user.age)
console.log('user.age = ', user.age)
}}>get age</button>
<button onClick={async () => {
// call model method to change it's state
await user.logout()
alert('user logged out')
console.log('user logged out')
}}>logout (delay 2s)</button>
</div>
<div className='display'>user.name = {name}</div>
</div>
<div className='example-counter'>
<h3>Counter:</h3>
<div>
<button onClick={() => { counter.increase() }}>+1</button>
<button onClick={async () => {
await counter.increase_async()
console.log('counter.value', counter.value)
}}>+1 (delay 2s)</button>
<button onClick={() => { counter.reset() }}>reset</button>
</div>
<div className='display'>counter.value = {value}</div>
</div>
<div className='stats'>
<h3>Statistics:</h3>
<div className='detail'>
<div>{'<'}Example{' />'} component rendered</div>
<RenderCounter />
<div>times</div>
</div>
</div>
</div>
}
class User extends Model <User> {
name = ''
age = 0
async logout () {
await delay(2000)
this.set({ name: '', age: 0 })
}
}
let user = new User()
class Counter extends Model <Counter> {
value = 0
reset () {
this.set({ value: 0 })
}
increase () {
this.set({ value: this.value + 1 })
}
async increase_async () {
await delay(2000)
this.set({ value: this.value + 1 })
}
}
let counter = new Counter()
async function delay (milliseconds: number) {
return new Promise( resolve => {
setTimeout(resolve, milliseconds)
})
}
createRoot(
document.querySelector('.root')
).render(
<Example />
)export class Model <TModel> {
/** Map<rerender, selector> */
protected _selectors: Map<({ }) => void, (keyof TModel)[]>
/** 保存上次渲染的状态用于比较 last state */
protected _state: any
constructor () {
Object.defineProperty(this, '_selectors', {
configurable: true,
enumerable: false,
writable: true,
value: new Map()
})
Object.defineProperty(this, '_state', {
configurable: true,
enumerable: false,
writable: true,
value: { }
})
}
/** 像使用 react hooks 那样订阅模型属性 use and watch model's properties like react hooks
use 之后,通过 set 更新属性才会触发组件重新渲染 After `use`, properties updated by `set` will trigger rerender
@param selector 订阅属性名称组成的数组 selector array of properties to watch
@returns 模型本身 model self
@example ```ts
const { name, age } = user.use(['name', 'age'])
``` */
use <Key extends keyof TModel> (selector?: Key[]) {
// React guarantees that dispatch function identity is stable and won’t change on re-renders
const [, rerender] = useState({ })
useEffect(() => {
this._selectors.set(rerender, selector)
return () => { this._selectors.delete(rerender) }
}, [ ])
return this as any as Pick<TModel, Key>
}
/** 更新模型属性,diff, 重新渲染对应组件 assign properties to model then diff then rerender (when changed)
@param data 属性 data properties
@example ```ts
user.set({ name: 'Tom', age: 16 })
``` */
set (data: Partial<TModel>) {
Object.assign(this, data)
this.render()
}
render (diffs?: (keyof TModel)[]) {
const set_diffs = diffs ? new Set(diffs) : null
for (const [rerender, selector] of this._selectors)
if (
!selector ||
selector.find(key =>
set_diffs ?
set_diffs.has(key)
:
this[key as any] !== this._state[key]
)
)
rerender({ })
this._state = { ...this }
}
}