README
structured-react-hook
structured-react-hook 已整合到 mencius 库内部 结构化的, 基于响应式的 React 应用状态管理框架
快速上手
安装
yarn add reactx
创建 Hello World 组件
/**
* defaultShowCode: true
*/
import React from 'react'
import { createComponent } from 'mencius'
const HelloWorld = createComponent({
name: 'HelloWorld',
state: {
text: 'Hello World',
},
view: {
render() {
return <div>{this.state.text}</div>
},
},
})
修改组件状态
/**
* defaultShowCode: true
*/
import React, { useEffect } from 'react'
import { createComponent } from 'mencius'
export const HelloWorld = createComponent({
name: 'HelloWorld',
state: {
text: 'Hello World',
},
controller: {
onMount() {
this.setter.text('Welcome!!')
},
},
view: {
render() {
useEffect(() => {
this.controller.onMount()
}, [])
return <div>{this.state.text}</div>
},
},
})
使用 Props
传给 HelloWorld
组件的 Props
会被挂载到 this(HelloWorld 组件实例)
上.
/**
* defaultShowCode: true
*/
import React, { useEffect } from 'react'
import { createComponent } from 'mencius'
const HelloWorld = createComponent({
name: 'HelloWorld',
state: {
text: 'Hello World',
},
controller: {
onMount() {
this.setter.text('Welcome!!!')
},
},
view: {
render() {
useEffect(() => {
this.controller.onMount()
}, [])
return (
<div>
{this.state.text} and {this.props.text}
</div>
)
},
},
})
export default function Wrapper() {
return <HelloWorld text="I love JavaScript" />
}
循环渲染 HelloWorld 组件
循环渲染 SRH
创建出来的组件时要注意, 和 React
要求的 key
一样为组件注入唯一的 sid
,
这样才能确保每个 HelloWorld
组件获得属于自己的 Props
export default function Wrapper() {
return [1, 2, 3, 4].map((number) => {
return <HelloWorld sid={number} key={number} text="I love JavaScript" />
})
}
异步响应其他组件的变化
示例中展示了多个场景
User
组件登录异步获取username
,username
初始状态为null
Welcome
组件初始化的时候通过this.getState
读取了User.state.username
的初始值- 点击
Login
按钮User
组件2000ms
后读取username
Welcome
组件订阅了User
的onClick
触发和state
变更
SRH 基于 Rx.js 实现的响应式机制确保开发者不需要关心变更是异步的还是同步的, SRH 会妥善处理这一点.
/**
* defaultShowCode: true
*/
import React, { useEffect } from 'react'
import { createComponent } from 'mencius'
const User = createComponent({
name: 'User',
state: {
username: null,
},
controller: {
onClick() {
setTimeout(() => {
this.setter.username('jacky')
}, 2000)
},
},
view: {
render() {
return (
<>
<button onClick={this.controller.onClick}> Login </button>
</>
)
},
},
})
const Welcome = createComponent({
name: 'Welcome',
state: {
text: 'Welcome',
},
subscribe: {
User: {
state({ nextState }) {
this.setter.text((text) => {
return `Welcome ~ ${nextState.username}`
})
},
controller: {
onClick({ state }) {
this.setter.text('Welcome loading...')
},
},
},
},
controller: {
async onMount() {
const UserState = await this.getState('User')
this.setter.text(`Welcome ~ ${UserState.username}`)
},
},
view: {
render() {
useEffect(() => {
this.controller.onMount()
}, [])
return <div>{this.state.text}</div>
},
},
})
export default function Wrapper() {
return (
<>
<User />
<Welcome />
</>
)
}
响应式状态
什么是响应式状态?
组件是如何通过响应式状态来实现互相影响的? 下图 👇🏻 可以很好的解释这一过程
A 组件
通过在视图中调用 controller
来处理 UI
交互, 同时修改自身状态, 这一过程对订阅了 A 组件
的 B 组件
是可观察的. 但对于 A 组件而言, 它如何影响其他组件并不需要它自身来感知这一件事, 通过这种方式, 实现了组件间能够互相影响但又完全解耦的模式.
而对于 B 组件
而言 subscribe 是独立于组件自身之外的一段逻辑, 当 A 组件
不存在的时候, B 组件完全不受任何影响. 直接删除这些订阅逻辑也是可行的.
const ComponentB = createComponent({
name:"ComponentB",
// 完全移除 subscribe 不会对 Component B 产生任何影响
// subscribe:{
// ComponentA:{
// state(){
// // do any thing
// }
// }
// }
...
})
subscribe
使用 SRH 提供的 subscribe 声明式 api, 你可以很方便的编写响应式代码
响应 state 的变化
state
变化对象包含两个值
- key: 变化的 state key
- prevState: 变化前的 state
- nextState: 变化后的 state
/**
* defaultShowCode: true
*/
import React, { useEffect } from 'react'
import { createComponent } from 'mencius'
const Count1 = createComponent({
name: 'Count1',
state: {
count: 0,
},
controller: {
onAddClick() {
this.setter.count(this.state.count + 1)
},
},
view: {
render() {
return (
<>
<span>
{this.state.count}{' '}
<button onClick={this.controller.onAddClick}> + </button>
</span>
</>
)
},
},
})
const Count2 = createComponent({
name: 'Count2',
state: {
count: 0,
},
subscribe: {
Count1: {
state({ nextState }) {
this.setter.count(nextState.count)
},
},
},
view: {
render() {
return (
<>
<span>{this.state.count}</span>
</>
)
},
},
})
export default function Wrapper() {
return (
<>
Count1: <Count1 />
<div />
Count2: <Count2 />
</>
)
}
响应 controller 交互事件
controller
变化对象包含三个值
- key: 触发的 controller 函数名, 以 on 开头
- args: 触发的 controller 函数参数
- state: 触发 controller 函数后最新的 state
/**
* defaultShowCode: true
*/
import React, { useEffect } from 'react'
import { createComponent } from 'mencius'
const Count1a = createComponent({
name: 'Count1a',
state: {
count: 0,
},
controller: {
onAddClick(prevCount) {
this.setter.count(prevCount + 1)
},
},
view: {
render() {
return (
<>
<span>
{this.state.count}{' '}
<button
onClick={() => this.controller.onAddClick(this.state.count)}
>
{' '}
+{' '}
</button>
</span>
</>
)
},
},
})
const Count2a = createComponent({
name: 'Count2a',
state: {
subscribeInfo: '未获取到任何交互信息',
},
subscribe: {
Count1a: {
controller: {
onAddClick({ key, args, state }) {
this.setter.subscribeInfo(
`Count1a 触发了 ${key}, 参数是 ${[
...args,
]},最后的 state : ${JSON.stringify(state)}`
)
},
},
},
},
view: {
render() {
return (
<>
<span>{this.state.subscribeInfo}</span>
</>
)
},
},
})
export default function Wrapper() {
return (
<>
Count1a: <Count1a />
<div />
Count2a: <Count2a />
</>
)
}
声明式 API
SRH
采用声明式 API
来创建组件, 这里介绍了主要的可声明的 API
name
对于 SRH
组件来说 name
并不是必须的, 你可以使用匿名组件, 但建议为每个组件声明一个唯一的 ID
注意匿名组件是不可订阅的, 也无法和其他组件产生响应
name: '唯一 ID'
state
state
是一个组件的核心, 声明了组件所拥有的的状态, 然后你可以通过 this.setter[stateKey]
来操作这些状态,
要注意的是, SRH
只会为 state
声明的第一层 key 建立 setter
函数, 这样做是为了尽可能让 state
声明的结构不那么复杂
state: {
foo: {
bar: '1'
}
}
// this.setter.foo({bar:'2'}) right!
// this.setter.foo.bar('2') wrong!!
derived
derived
意指派生状态, derived
下的派生函数通过 state
中声明的状态派生出一个新值, 通过 this.dervied[derivedKey]
可以直接获取到派生出来的值, 和 state
一样, 派生值发生变化也会触发 render
, 详见下面的例子
import React from 'react'
import { createComponent } from 'mencius'
export default createComponent({
name: 'DerivedExample',
state: {
count: 0,
},
derived: {
countIsNotZero() {
if (this.state.count !== 0) {
return 'true'
}
return 'false'
},
},
controller: {
onAddClick() {
this.setter.count(this.state.count + 1)
},
onSubClick() {
this.setter.count(this.state.count - 1)
},
},
view: {
render() {
return (
<>
<div>
{' '}
count:
<span>
{this.state.count}
<button
style={{ margin: '0 8px' }}
onClick={this.controller.onAddClick}
>
{' '}
加 1{' '}
</button>
<button onClick={this.controller.onSubClick}>减 1</button>
</span>{' '}
</div>
<div>count changed:{this.derived.countIsNotZero}</div>
</>
)
},
},
})
ref
ref
等价于 useRef
, 但和直接使用 useRef
Hook 不同的是, 在 SRH
组件内声明的 ref
可以像普通的字段那样直接使用, 在这点上, ref
更像是 classComponent
下直接挂载到 this
上的 key
ref:{
count:0
},
controller:{
onClick(){
console.log(this.ref.count) // 0
this.ref.count++
console.log(this.ref.count) // 1
}
}
使用 ref 存储 React dom 实例
和 useRef 不同点在于, 使用 ref 声明字段绑定到 React ref 属性上时需要做一些处理
import React from 'react'
import { createComponent } from 'mencius'
export default createComponent({
name: 'DomRefExample',
state: {
value: 0,
inputValue: 0,
},
ref: {
inputEl: React.createRef(),
},
controller: {
onChange(e) {
this.setter.value(e.target.value)
},
onClick() {
this.setter.inputValue(this.ref.inputEl.current.value)
},
},
view: {
render() {
return (
<>
输入:{' '}
<input
ref={this.ref.inputEl}
onChange={this.controller.onChange}
value={this.state.value}
/>
<button style={{ margin: '0 8px' }} onClick={this.controller.onClick}>
{' '}
通过 Ref 展示输入框的值{' '}
</button>
当前输入框的值: {this.state.inputValue}
</>
)
},
},
})
view
view
用来声明组件的视图, 通常是 React JSX. 在 view
声明的 render
函数中, 你可以使用 React
原生的 Hook API, 但要注意的是 Hook
的限制你同样也要遵守
view:{
render(){
return <>hello world</>
}
}
注意!! view.render 是必须实现的函数
controller
controller
的函数签名必须以 on
开头, 以 click
, change
等动词结尾, controller
对 view
来说是可见的, 但是对自身是不可见的, 简单讲, 你可以在 view
的 render
函数里调用 controller
来实现交互逻辑, 但是你无法在 controller
函数内调用另一个 controller
函数, 这是不被允许的
controller:{
onClick(){
//this.controller.onChange() wrong!!
},
onChange(){
}
}
view:{
render(){
return(
<button onClick={this.controller.onClick}></button> // right!!
)
}
}
service
service 函数对自身和 controller 可见, 但是对 view 不可见, 这是为了弥补 controller 函数无法调用其他 controller 函数导致逻辑无法被共享的问题
service:{
doA(){
// this.service.doB() right!!
},
doB(){}
}
扩展组件
SRH
提出了 Membrane
概念来解决如何让 扩展 SRH
组件
如果说通常我们所说的通过 Props
来控制的 React
组件是一种封闭型组件
, 那么基于 Membrane
模式下的 SRH
组件, 就是一种开放型组件
首先 Membrane 是一种概念, 并不涉及到具体的 API , 通常将被扩展对象称为 Core, Membrane 就是覆盖在 Core 上的一层膜, 具有和 Core 一样或者更多的结构, 通过包裹 Core, Membrane 和 Core 共同组成了新的组件实体.
因此 Membrane 模式有两个必要条件
- Core 和 Membrane 在技术上必须可以实现 Merge, 例如 JS 对象
- Membrane 不能被其他 Membrane 再覆盖
/**
* defaultShowCode: true
*/
import React from 'react'
import { createComponent } from 'mencius'
import deepmerge from 'deepmerge'
const core = () => {
return {
name: 'CoreButton',
state: {
text: 'Core Button',
},
view: {
render() {
return <button> {this.state.text} </button>
},
},
}
}
export default createComponent(
deepmerge(core(), {
name: 'MembraneButton',
state: {
text: 'Membrane Button',
},
})
)