Skip to content
/ rimx Public

A state management tool for React, based on RxJS and ImmutableJS.

License

Notifications You must be signed in to change notification settings

tonypig93/rimx

Repository files navigation

RimX

Travis (.org) Coverage Status NPM

A state management tool for React, based on RxJS and ImmutableJS.

RimX是一个类似redux的状态管理工具,使用起来较为简单。你可以利用RxJS强大的流处理能力来管理react组件的状态变化,另一方面ImmutableJS可以使状态更新变的更简单。 RimX本身是个小巧的库,gzip后仅3KB,本身提供了react集成。

依赖

  • RxJS >= 5.5.0
  • ImmutableJS >= 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)

类似于reduxrimx提供一个connect函数用于将组件加入状态管理当中。

connect({
  scope: string;
  initState: any,
  connectScopes?: {
    [scope: string]: any,
  };
  reducer?: Reducer;
  cache?: boolean;
  log?: boolean;
})(MyComponent)
属性 说明 默认值
scope rimx中的状态都在一个store当中,但是你可以将其划分为多个scope,举例来说一个业务模块中的多个组件可以属于同一个scopescope之间不共享statereducer --
initState 指定scope时表示创建scope,然后就需要提供初始状态,这里需要注意的是,initState会被转换为Immutable结构,例如{ list: [] }中的list会被转成Immutable.List,如果你希望list是原生数组,那么需要用Immutable.Map({ list: [] })包装起来。 {}
connectScopes 创建了scope之后,其他组件需要连接到这个scope当中才能获取或者修改scope state,传入connectScopes的是一个对象,key表示需要连接到的scopevalue有多种形式,后面有举例。 --
reducer 类似于reduxreducer,写法基本相同。 --
cache 是否缓存当前scopestate,当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,
})

上面的代码创建了一个名为globalscope,然后我们在另一个组件中访问这个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有多种写法,上面是简写,仅当propNamepath相同时可以简写,其他写法如下:

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并且该scopescope相同时,this.props.controller指向scope controller本身,如果连接到了多个scope,需要提供scope name来获取scope controller,例如this.props.controllers['global']

controller本身是一个RxJS Subject对象,但是重载了nextsubscribe这两个方法,其包含的数据为scope state

  • controller.next() controller.next()可以直接传入一个新的state,或者传入一个函数,函数的参数为当前state。调用next之后可以同步修改state

  • controller.listen() listen接收一个路径,表示监听该路径指向数据的变化,listen要和do一起搭配使用,变化之后的数据会传入dolisten可以用于获取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作为参数,第二个参数用于在mergeupdate之间选择状态的更新方式。

为了简化使用,当controller指向scope controller时,会将listendispatch直接注入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使用ImmutableJSmerge来合并前后两个状态,注意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));
}

异步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

只要返回值是Observablerimx就可以从中获取数据。某个库只有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 }的状态,因此会触发目标组件的两次渲染。

About

A state management tool for React, based on RxJS and ImmutableJS.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published