npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rimx

v3.8.3

Published

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

Downloads

80

Readme

RimX

Travis (.org) Coverage Status NPM

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.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使用ImmutableJSmergeDeepIn来合并前后两个状态,因此修改一个基本类型的值时,只需提供包含修改部分的对象(或者是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 }的状态,因此会触发目标组件的两次渲染。