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

rxdux

v1.2.0

Published

Yet another flux implementation based on redux with asynchronous reducer feature

Downloads

15

Readme

Rxdux

Build Status

Yet another flux implementation based on redux, extending reducer to allow asynchronous state change.

Abstract

Redux, one of the most popular flux implementation, only allows its state reducer with synchronous change. As this restriction, a redux application tends to yield fat action creators and very thin in reducers.

The Rxdux is based on Redux, but allows reducers to return asynchronous state changes. The reducer can return a promise of state change or a sequence of state changes. The first is done by Promise object, and the second is done by Observable in Rx world. Thunk function or generator function can also be used to yield the state changes.

Guide

Reducers

The reducer is a pure function, which accepts state and action and returns next state as in original Redux, except that it can return a wrapping type to represent the future states.

type Reducer<S> = (S, Action) => S | Promise<S> | Observable<S> | Thunk<S> | Generator<S>;

Promise Reducer

import { createStore } from 'rxdux';

// `fetchFruits` will return an array fruits ('apple','orange','banana')
// in node.js style callback function.
import { createStore } from 'rxdux';
import fetchFruits from '../utils/fetchFruits';
import wait from '../utils/wait';

const initialFruitsState = [];

// Reducer
function fruits(state = initialFruitsState, action) {
  switch (action.type) {
    case 'FETCH_FRUITS':
      // can return a Promise object for future change of state
      return new Promise((resolve, reject) => {
        fetchFruits((err, records) => {
          if (err) { return reject(err) }
          resolve(records);
        });
      });
    case 'CLEAR_FRUITS':
      // can also return synchronous state change, of course.
      return initialFruitsState;
    default:
      return state;
  }
}

const store = createStore(fruits);

store.subscribe((state) => {
  console.log(state);
});

// => []

store.dispatch({ type: 'FETCH_FRUITS' });

// => ['apple', 'orange', 'banana']

store.dispatch({ type: 'CLEAR_FRUITS' });

// => []

Observable Reducer

import { createStore } from 'rxdux';
import { Observable } from 'rx';
import fetchFruits from './fetchFruits';

const initialFruitsState = { records: [], loading: false };

// Reducer
function fruits(state = initialFruitsState, action) {
  switch (action.type) {
    case 'FETCH_FRUITS':
      // can return a Observable object to notify future change of state
      return Observable.create((o) => {
        o.onNext({ ...state, loading: true });
        fetchFruits((err, records) => {
          if (err) { return o.onError(err) }
          o.onNext({ records, loading: false });
          o.onCompleted();
        });
      });
    case 'CLEAR_FRUITS':
      // can also return synchronous state change, of course.
      return initialFruitsState;
    default:
      return state;
  }
}

const store = createStore(fruits);

store.subscribe((state) => {
  console.log(state);
});

// => { loading: false, records: [] }

store.dispatch({ type: 'FETCH_FRUITS' });

// => { loading: true, records: [] }
// => { loading: false, records: ['apple', 'orange', 'banana'] }

store.dispatch({ type: 'CLEAR_FRUITS' });

// => { loading: false, records: [] }

Thunk Function Reducer

import { createStore } from 'rxdux';
import { Observable } from 'rx';
import fetchFruits from './fetchFruits';

const initialFruitsState = { records: [], loading: false };

// Reducer
function fruits(state = initialFruitsState, action) {
  switch (action.type) {
    case 'FETCH_FRUITS':
      // can return a thunk function which accepts three callbacks to tell the state changes
      return (next, error, complete) => {
        next({ ...state, loading: true });
        fetchFruits((err, records) => {
          if (err) { return error(err); }
          next({ records, loading: false });
          complete();
        });
      };
    case 'CLEAR_FRUITS':
      // can also return synchronous state change, of course.
      return initialFruitsState;
    default:
      return state;
  }
}

const store = createStore(fruits);

store.subscribe((state) => {
  console.log(state);
});

// => { loading: false, records: [] }

store.dispatch({ type: 'FETCH_FRUITS' });

// => { loading: true, records: [] }
// => { loading: false, records: ['apple', 'orange', 'banana'] }

store.dispatch({ type: 'CLEAR_FRUITS' });

// => { loading: false, records: [] }

Generator Function Reducer

import { createStore } from 'rxdux';
import { Observable } from 'rx';
import fetchFruits from './fetchFruits';

const fetchFruitsPromise = () => {
  return new Promise((resolve, reject) => {
    fetchFruits((err, records) => {
      if (err) { return reject(err); }
      resolve(records);
    });
  });
};

const initialFruitsState = { records: [], loading: false };

// Reducer
function fruits(state = initialFruitsState, action) {
  switch (action.type) {
    case 'FETCH_FRUITS':
      // can return a generator function
      return function* () {
        yield { ...state, loading: true };
        // Nested generator function is ok but only top-level yield affects to the state changes.
        yield function* () {
          const records = yield fetchFruitsPromise();
          yield { records, loading: false };
        };
      };
    case 'CLEAR_FRUITS':
      // can also return synchronous state change, of course.
      return initialFruitsState;
    default:
      return state;
  }
}

const store = createStore(fruits);

store.subscribe((state) => {
  console.log(state);
});

// => { loading: false, records: [] }

store.dispatch({ type: 'FETCH_FRUITS' });

// => { loading: true, records: [] }
// => { loading: false, records: ['apple', 'orange', 'banana'] }

store.dispatch({ type: 'CLEAR_FRUITS' });

// => { loading: false, records: [] }

Combining Reducers

The idea of combining reducers is also the same with Redux, but Redux's combineReducers cannot be used because it is not assuming that the reducers will yield asynchronous state changes.

The Rxdux has its own combineReducers function to create a combined reducers. It can accept both simple synchronous and asynchronous reducers.

import { combineReducers, createStore } from 'rxdux';
import wait from './wait';

function num1(state = 0, action) {
  switch (action.type) {
    case 'APPLY':
      return wait(100).then(() => state + action.value);
    default:
      return state;
  }
}

function num2(state = 1, action) {
  switch (action.type) {
    case 'APPLY':
      return wait(200).then(() => state * action.value);
    default:
      return state;
  }
}

const reducer = combineReducers({ num1, num2 });

const store = createStore(reducer);

store.subscribe((state) => {
  console.log(state);
});

// => { num1: 0, num2: 1 }

store.dispatch({ type: 'APPLY', value: 2 });

// => { num1: 2, num2: 1 }
// => { num1: 2, num2: 2 }

store.dispatch({ type: 'APPLY', value: 4 });

// => { num1: 6, num2: 2 }
// => { num1: 6, num2: 8 }

Binding to React Components

Because the interface of dispatching action / notifying state change is same as the original Redux, you can utilize the works related to Redux even in Rxdux.

To binding the store information to React components, the react-redux package is the one and you can use it in Rxdux, too.

import 'babel-polyfill';

import React, { Component } from 'react';
import { render } from 'react-dom';
import { Provider, connect } from 'react-redux';
import { createStore } from 'rxdux';

import wait from './wait';

// simple reducer to count up gracefully
function counter(state = 0, action) {
  switch (action.type) {
    case 'ADD':
      return function* () {
        for (let i = 0; i < action.value; i++) {
          state = yield wait(100).then(() => state + 1);
        }
      };
    case 'RESET':
      return function* () {
        while (state > 0) {
          state = yield wait(100).then(() => state - 1);
        }
      };
    default:
      return state;
  }
}

const store = createStore(counter);

@connect(
  (state) => ({ counter: state })
)
class Root extends Component {
  render() {
    return (
      <div>
        <div>{ this.props.counter }</div>
        <button onClick={ () => this.props.dispatch({ type: 'ADD', value: 1 }) }>+1</button>
        <button onClick={ () => this.props.dispatch({ type: 'ADD', value: 5 }) }>+5</button>
        <button onClick={ () => this.props.dispatch({ type: 'RESET' }) }>Reset</button>
      </div>
    );
  }
}

class App extends Component {
  render() {
    return (
      <Provider store={ store }>
        <Root />
      </Provider>
    );
  }
}

render(<App />, document.getElementById('root'));

Avoid Blocking of State Changes by Preceding Actions

As the store's state changes will be serialized by the order of incoming actions, an application which accepts user's input simultaneously will not be responsive if the application's root store is built by one combined reducer function.

In Rxdux, instead of using one store and combined reducers, it is recommended to create multiple stores for each action serialization scope. Combining/merging stores is done by combineStores/mergeStores.

import { createStore, combineStores, mergeStores } from 'rxdux';
import wait from './wait';

function num(state = 0, action) {
  switch (action.type) {
    case 'ADD':
      return wait(150).then(() => state + action.value);
    default:
      return state;
  }
}

function string(state = 'abc', action) {
  switch (action.type) {
    case 'APPEND':
      return wait(50).then(() => state + action.value);
    default:
      return state;
  }
}

function obj(state = { checked: false }, action) {
  switch (action.type) {
    case 'FLICK':
      return function*() {
        yield { checked: true };
        yield wait(100).then(() => { checked: false });
      }
    default:
      return state;
  }
}

const numStore = createStore(num);
const stringStore = createStore(string);
const objStore = createStore(obj);
const store = mergeStores(
  combineStores({ num: numStore, string: stringStore }),
  objStore
);

store.subscribe((state) => {
  console.log(state);
});

// => { num: 0, string: 'abc', checked: false }

store.dispatch({ type: 'ADD', value: 1 });
store.dispatch({ type: 'APPEND', value: 'def' });
store.dispatch({ type: 'FLICK' });

// => { num: 0, string: 'abc', checked: true }
// => { num: 0, string: 'abcdef', checked: true }
// => { num: 0, string: 'abcdef', checked: false }
// => { num: 1, string: 'abcdef', checked: false }

Error Handling

TODO

Middlewares

Middleware support is not yet done in Rxdux. In fact, some middlewares in Redux might be safely used because its store interface is almost same, but some middlewares which require the changed state information after the action (e.g. redux-logger) will not work because the state change will not always come right after the reducer call.

License

MIT