A state management tool for React, based on RxJS and ImmutableJS.
RimX
是一个类似redux
的状态管理工具,使用起来较为简单。你可以利用RxJS
强大的流处理能力来管理react
组件的状态变化,另一方面ImmutableJS
可以使状态更新变的更简单。
RimX
本身是个小巧的库,gzip
后仅3KB,本身提供了react
集成。
RxJS
>= 5.5.0ImmutableJS
>= 3.8.0
需要用户自行安装以上两个库。
npm i rimx --save
或者
yarn add rimx
RimX
会创建一个全局唯一的store
,所有的状态都存储在store
中。为保证模块之间的独立性,你需要在store
中创建不同的域scope
,然后需要用到该scope
的组件就可以通过connect
连接到该scope
,获得scope
中状态的实时响应。你既可以将scope
的状态注入到props
中,也可以手动订阅某个状态,利用RxJS
操作符实现更复杂的逻辑。
connect(options)(Component)
类似于redux
,rimx
提供一个connect
函数用于将组件加入状态管理当中。
connect({
scope: string;
initState: any,
connectScopes?: {
[scope: string]: any,
};
reducer?: Reducer;
cache?: boolean;
log?: boolean;
})(MyComponent)
属性 | 说明 | 默认值 |
---|---|---|
scope |
rimx 中的状态都在一个store 当中,但是你可以将其划分为多个scope ,举例来说一个业务模块中的多个组件可以属于同一个scope ,scope 之间不共享state 与reducer 。 |
-- |
initState |
指定scope 时表示创建scope ,然后就需要提供初始状态,这里需要注意的是,initState 会被转换为Immutable 结构,例如{ list: [] } 中的list 会被转成Immutable.List ,如果你希望list 是原生数组,那么需要用Immutable.Map({ list: [] }) 包装起来。 |
{} |
connectScopes |
创建了scope 之后,其他组件需要连接到这个scope 当中才能获取或者修改scope state ,传入connectScopes 的是一个对象,key 表示需要连接到的scope , value 有多种形式,后面有举例。 |
-- |
reducer |
类似于redux 的reducer ,写法基本相同。 |
-- |
cache |
是否缓存当前scope 的state ,当scope 被重新创建时会读取上一次的state |
false |
log |
是否在状态变化时打印日志,可以用于排查问题。 | false |
import React from 'react';
import { connect } from 'rimx';
class A extends React.Component {
...
}
const STATE = {
profile: {
name: 'tony',
age: 18,
},
role: 'admin',
};
export connect({
scope: 'global',
initState: STATE,
})
上面的代码创建了一个名为global
的scope
,然后我们在另一个组件中访问这个scope
。
import React from 'react';
import { connect } from 'rimx';
class B extends React.Component {
render() {
return (
<div>{this.props.profile.name}</div> //此时可以在props中获取scope state
);
}
}
export connect({
connectScopes: {
global: 'profile',
},
});
connectScopes
有多种写法,上面是简写,仅当propName
与path
相同时可以简写,其他写法如下:
connectScopes: {
global: [{
propName: 'age',
path: ['profile', 'age'],
}],
}
connectScopes: {
global: {
propName: 'age',
path: ['profile', 'age'],
},
}
connectScopes: {
global: {
propName: 'age',
path: 'profile.age',
},
}
如果要修改state
,有两种方法,一种是直接在scope
的控制对象controller
上修改,另一种是用reducer
。
// 直接修改
import React from 'react';
import { connect } from 'rimx';
class B extends React.Component {
handleChangeState = () => {
this.props.controller.next(() => ({
profile: {
age: 20,
},
}));
}
render() {
return (
<div onClick={this.handleChangeState}>{this.props.profile.name}</div> //此时可以在props中获取scope state
);
}
}
export connect({
connectScopes: {
global: 'profile',
},
});
每个被connect
包裹后的组件都会获得一个controller
对象,这个对象包含了对scope
的全部操作。
当
connect
的参数里只有scope
,或者connectScopes
只有连接了一个scope
时,或者connectScopes
只连接了一个scope
并且该scope
与scope
相同时,this.props.controller
指向scope controller
本身,如果连接到了多个scope
,需要提供scope name
来获取scope controller
,例如this.props.controllers['global']
。
controller
本身是一个RxJS Subject
对象,但是重载了next
和subscribe
这两个方法,其包含的数据为scope state
:
-
controller.next()
controller.next()
可以直接传入一个新的state
,或者传入一个函数,函数的参数为当前state
。调用next
之后可以同步修改state
。 -
controller.listen()
listen
接收一个路径,表示监听该路径指向数据的变化,listen
要和do
一起搭配使用,变化之后的数据会传入do
。listen
可以用于获取state
中的任何数据,而不局限于props
中提供的,不传入参数表示监听整个state
。 -
controller.listen().do()
do
接收一个observer
,用于响应数据变化,当state
发生变化时会触发do
的回调。import React from 'react'; import { connect } from 'rimx'; class B extends React.Component { componentDidMount() { this.props.controller.listen(['profile', 'age']).do(data => { console.log(data); // 18 -> 20; // 首次监听时会获取`profile.age`的初始值18,之后当触发`handleChangeState`时,会获得新值20。 // 其他字段例如profile.name的变化不会触发这里的回调。 }); } handleChangeState = () => { this.props.controller.next(() => ({ profile: { age: 20, }, })); } render() { return ( <div onClick={this.handleChangeState}>{this.props.profile.name}</div> ); } } export connect({ connectScopes: { global: 'profile', }, });
-
controller.listen().pipe().do()
pipe
用于对数据流进行提前处理,可以接入任何rxjs
的操作符,例如过滤低于20的值,只有当age
大于20时才会响应回调。import React from 'react'; import { connect } from 'rimx'; class B extends React.Component { componentDidMount() { this.props.controller .listen(['profile', 'age']) .pipe(ob => ob.filter(v => v 20)) .do(data => { console.log(data); // 21; // 第三次点击时才会触发回调。 }); } handleChangeState = () => { const nextAge = state.getIn(['profile', 'age']) + 1; this.props.controller.next(() => ({ profile: { age: nextAge, }, })); } render() { return ( <div onClick={this.handleChangeState}>{this.props.profile.name}</div> ); } } export connect({ connectScopes: { global: 'profile', }, });
-
controller.dispatch()
用于执行reducer
,接收一个action
作为参数,第二个参数用于在merge
和update
之间选择状态的更新方式。
为了简化使用,当
controller
指向scope controller
时,会将listen
和dispatch
直接注入props
。
不仅仅是B
组件,A
组件也可以完成上面的全部操作,只需像B
一样配置connectScopes
。
import React from 'react';
import { connect } from 'rimx';
class A extends React.Component {
componentDidMount() {
this.props.listen(['profile', 'age']).do(data => {
console.log(data); // 18 -> 20;
});
}
handleChangeState = () => {
this.props.controller.next(() => ({
profile: {
age: 20,
},
}));
}
render() {
return (
<div onClick={this.handleChangeState}>{this.props.profile.name}</div>
);
}
}
const STATE = {
profile: {
name: 'tony',
age: 18,
},
};
export connect({
scope: 'global',
initState: STATE,
connectScopes: {
global: 'profile',
},
})
基本用法:
// constants.js
export const CHANGE_AGE = 'CHANGE_AGE';
// actions.js
import { CHANGE_AGE } from './constants';
export function changeAge(age) {
return {
type: CHANGE_AGE,
payload: age,
};
}
// reducer.js
import { combineReducers } from 'rimx';
import { CHANGE_AGE } from './constants';
function changeAge(state, action) {
return {
profile: {
age: action.payload,
},
};
}
const reducers = combineReducers({
[CHANGE_AGE]: changeAge,
});
export default reducers;
// combineReducers用于将`action type`和`reducer`绑定在一起。
import React from 'react';
import { connect } from 'rimx';
import reducer from './reducer';
import { changeAge } from './actions';
class A extends React.Component {
handleChangeState = () => {
this.props.dispatch(changeAge(20));
}
render() {
return (
<div onClick={this.handleChangeState}>{this.props.profile.name}</div>
);
}
}
const STATE = {
profile: {
name: 'tony',
age: 18,
},
};
export connect({
scope: 'global',
initState: STATE,
reducer,
})
以上代码只要用过redux
基本都看得懂,这里需要特别指出的是关于reducer
的返回值。默认情况下,rimx
使用ImmutableJS
的merge
来合并前后两个状态,注意merge
是浅合并,因此修改一个基本类型的值时,只需提供包含修改部分的对象(或者是Immutable
结构)即可。
// 修改前
state = {
profile: {
name: 'tony',
age: 18,
},
role: 'admin',
};
↓
reducer(state, action) {
return {
profile: {
age: action.payload,
},
};
或者
return Immutable.fromJS({
profile: {
age: action.payload,
},
});
但是不能这样
return Immutable.Map({
profile: {
age: action.payload,
},
});
下面这种在merge策略下尽量不要这么做,因为一方面会提高合并成本,另一方面会导致异步reducer之后状态发生异常。
return state.setIn(['profile', 'age']);
}
↓
// 修改后
state = {
profile: {
name: 'tony',
age: 20,
},
role: 'admin',
};
为什么说上面只能用Immutable.fromJS
而不能用Immutable.Map
呢?因为不论是返回原生对象还是Immutable.fromJS
,最终结果都是被转换为完完全全的Immutable
结构,但是Immutable.Map
只会转换第一层,也就是说profile
不是Immutable
的,当调用profile.get('age')
时就会报错,因为profile
是原生的对象。
那么什么情况下应该使用Immutable.Map
呢,例如前面说过,想要在某个字段上创建一个原生的数组或者对象时,需要用Immutable.Map
包裹起来,道理同上,此时为了修改状态后的字段依然为原生,就需要在reducer
里将返回值用Immutable.Map
包裹起来。
merge
策略会导致一个问题,就是Immutable.List
对象会合并,例如:
export connect({
scope: 'global',
initState: {
list: [],
},
reducer,
})
此时list
被转换为Immutable.List
,当重新发起http
请求来获取最新的list
数据时,前后list
会被合并,旧数据被保留了下来。此时有两种解决办法,一是用原生数组保存list
:
export connect({
scope: 'global',
initState: Immutable.Map({
list: [],
}),
reducer,
})
二是改用update
策略,将dispatch()
的第二个参数设置为false
可以切换到update
,新的state
会替换旧的state
:
this.props.dispatch(loadData(), false);
此时reducer
需要用到state
这个参数来返回新的state
,不然就丢失了其他字段。
loadData(state, action) {
return state.set('list', Immutable.List(action.payload));
}
利用rxjs
可以轻松实现异步reducer
,基本用法如下:
// reducer.js
import DataService from 'data.service';
function loadRole(state, action) {
return DataService.getData(action.payload).map(data => ({
role: data.role,
}));
}
DataService.getData
返回了一个用Observable
包装后的Http
请求,然后使用map
操作符返回需要修改的state
。
只要返回值是
Observable
,rimx
就可以从中获取数据。某个库只有Promise
?可以用Observable.fromPromise(promise)
来将Promise
转换为Observable
。
一个稍微复杂点的例子:
// reducer.js
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/merge';
import DataService from 'data.service';
function loadRole(state, action) {
return Observable.of({
loading: true,
}).merge(
DataService.getData(action.payload).map(data => ({
role: data.role,
loading: false,
}))
);
}
上面实现的是发起Http
请求之前将loading
设为true
,完成后再设为false
,这个reducer
首先会返回一个{ loading: true }
的状态,完成Http
请求之后再返回另一个{ loading: false, role: data.role }
的状态,因此会触发目标组件的两次渲染。