rimx v3.8.3
RimX
A state management tool for React, based on RxJS and ImmutableJS.
RimX是一个类似redux的状态管理工具,不同的是RimX没有action reducer等概念,使用起来较为简单。你可以利用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操作符实现更复杂的逻辑。
API
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',
},
})如何使用reducer
基本用法:
// 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的mergeDeepIn来合并前后两个状态,因此修改一个基本类型的值时,只需提供包含修改部分的对象(或者是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));
}异步reducer
利用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 }的状态,因此会触发目标组件的两次渲染。
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago