
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
RimXA 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操作符实现更复杂的逻辑。
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 }的状态,因此会触发目标组件的两次渲染。
FAQs
A state management tool for React, based on RxJS and ImmutableJS.
The npm package rimx receives a total of 56 weekly downloads. As such, rimx popularity was classified as not popular.
We found that rimx demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.