Package Exports
- react-object-model
- react-object-model/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (react-object-model) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
react-object-model
react-object-model
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
GitHub
https://github.com/ShenHongFei/react-object-model
1. Model
Usage
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 />
)Implementation
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 }
}
}2. FormModel
Usage
import React from 'react'
import ReactDOM from 'react-dom'
import { FormModel, FormField } from 'react-object-model/form.js'
class UserForm extends FormModel <UserFormValues> {
name = new FormField(this.form, 'name', '')
age = new FormField(this.form, 'age', '0')
override async submit (values: UserFormValues) {
await delay(3000)
console.log('submit', values)
}
override validate ({ name, age }: UserFormValues) {
return {
name: name ? undefined : 'name cannot be empty',
age: Number(age) < 18 ? 'age is less than 18' : undefined,
}
}
}
interface UserFormValues {
name: string
age: string
}
let fuser = new UserForm()
// re-render only when form state (hasValidationErrors, submitting) change
function UserFormExample () {
const { form: { hasValidationErrors, submitting, submit } } = fuser.use({ hasValidationErrors: true, submitting: true })
return <>
<Form className='form-test'>
<NameInput />
<AgeInput />
</Form>
<Form.Action>
<Button type='primary' loading={submitting} onClick={submit}>提交 ({String(hasValidationErrors)})</Button>
</Form.Action>
<Counter />
</>
}
// re-render only when name change
function NameInput () {
const { name } = fuser
name.use()
return <Form.Item /* auto inject status and message */ {...name.item} /* label='custom label' */>
<Input {...name.input} autoComplete='off' />
<Form.Text>touched: {String(name.meta.touched)}, error: {name.meta.error}</Form.Text>
<Counter />
</Form.Item>
}
// re-render only when age change
function AgeInput () {
const { age } = fuser
age.use()
return <Form.Item {...age.item}>
<Input {...age.input} autoComplete='off' />
<Form.Text>touched: {String(age.meta.touched)}, error: {age.meta.error}</Form.Text>
<Counter />
</Form.Item>
}
/** counter for rendered times */
function Counter () {
const rcounter = useRef(0)
return <div className='counter'>{++rcounter.current}</div>
}
ReactDOM.render(<UserFormExample/>, document.querySelector('.root'))