Skip to content

Latest commit

 

History

History

redux-cube

redux-cube

< Back to Project WebCube

NPM Version

Nodei

Redux Cube is an app state manager. It is part of my effort to simplify the usage of all 'mainstream' tools and best practices mentioned in the Spellbook of Modern Web Dev. It can reduce boilerplate and support many patterns (like Sub App, Reducer Bundle, ...)

Slides: Introduction to Redux Cube

npm install --save redux-cube

New docs based on the new createCube API and webcube's SSR feature

Coming soon!

Examples


Old docs

Overview

Action Type

// sampleApp/hub.js
import { createHub } from 'redux-cube';

export default createHub();

createHub returns a Hub instance which is an namespace manager of action types and a set of helper functions used to generate standard action object (follow Flux Standard Action and other best practices) and action creators.

// sampleApp/actions/sample.js
import hub from '../hub';

export const { actions, types, typeDict } = hub.add('NAMESPACE/MORE_NAMESPACE/MY_TYPE');
export const { actions, types, typeDict } = hub.add('namespace.moreNamespace.myType');
export const { actions, types, typeDict } = hub.add({
  NAMESPACE: {
    MORE_NAMESPACE: {
      MY_TYPE: true
    },
  },
});
export const { actions, types, typeDict } = hub.add({
  namespace: {
    moreNamespace: {
      myType: true
    },
  },
});

The above codes are equivalent.

console.log(typeDict)
// {
//   'NAMESPACE/MORE_NAMESPACE/MY_TYPE': defaultActionCreator,
// }

console.log(types)
// {
//   namespace: {
//     moreNamespace: {
//       myType: 'NAMESPACE/MORE_NAMESPACE/MY_TYPE',
//     },
//   },
// }

console.log(actions)
// {
//   namespace: {
//     moreNamespace: {
//       myType: defaultActionCreator,
//     },
//   },
// }

The defaultActionCreator is equivalent to:

() => ({
  type: 'NAMESPACE/MORE_NAMESPACE/MY_TYPE',
  payload: defaultPayloadCreator,
  meta: undefined,
})

The defaultPayloadCreator is equivalent to:

a => a

Action Creators

export const { actions, types, typeDict } = hub.add('NAMESPACE/MORE_NAMESPACE/MY_TYPE', payloadCreator, metaCreator);
export const { actions, types, typeDict } = hub.add({
  namespace: {
    moreNamespace: {
      myType: true,
      myType2: payloadCreator,
      myType3: [payloadCreator, metaCreator],
      myType4: [
        (a, b) => ({ data: a + b }),
        (a, b) => ({ a, b }),
      ],
      myType5: {
        [hub.ACTION_CREATOR]: actionCreator,
      },
    },
  },
});
actions.namespace.moreNamespace.myType(10);
// or
typeDict['NAMESPACE/MORE_NAMESPACE/MY_TYPE'](10);
// results:
// {
//   "type": "NAMESPACE/MORE_NAMESPACE/MY_TYPE",
//   "payload": 10
// }
actions.namespace.moreNamespace.myType4(1, 10);
// or
typeDict['NAMESPACE/MORE_NAMESPACE/MY_TYPE_4'](1, 10);
// result:
// {
//   "type": "NAMESPACE/MORE_NAMESPACE/MY_TYPE_4",
//   "payload": { data: 11 },
//   "meta": { "a": 1, "b": 10 }
// }

Reducers

// sampleApp/reducers/sample.js
import hub from '../hub';

export const { reducer } = hub.handle({
  namespace: {
    moreNamespace: {
      myType: (state, { payload, meta }) => newState,
    },
  },
  anotherType: (state, { payload, meta }) => newState,
}, initialStateForASliceOfStore);

Async Action Creators / Side Effects

For common needs:

For API-related needs:

For complex needs:

// sampleApp/actions/users.js
import hub from '../hub';
// https://www.npmjs.com/package/hifetch
import hifetch from 'hifetch';
import { reset } from 'redux-form';

export const { actions, types, typeDict } = hub.add({
  users: {
    fetchAll: () =>
      // handle by redux-promise-middleware
      hifetch({
        url: '/v1/users/',
      }).send(),

    add: [
      (userId, userData, opt) =>
        // handled by Thunk Payload Middleware
        dispatch =>
          // handle by redux-promise-middleware
          hifetch({
            url: `/v1/users/${userId}`,
            method: 'put',
            data: userData,
            ...opt,
          }).send().then(response => {
            dispatch(reset('userInfos'));
            return response;
          }),
      userId => ({
        userId,
      }),
    ],

    delete: [
      (userId, userData) =>
        // handle by redux-promise-middleware
        hifetch({
          url: `/v1/users/${userId}`,
          method: 'delete',
        }).send(),
      userId => ({
        userId,
      }),
    ],

  },
});

// handle by redux-observable
export const epics = [
  action$ =>
    action$.pipe(
      ofType('USERS/DELETE_FULFILLED'),
      map(action => ({
        type: 'NOTIFY',
        payload: { text: 'DELETED!' },
      }))
    ),
];
// sampleApp/reducers/users.js
import hub from '../hub';
import Immutable from 'immutable';

export const { reducer, actions, types, typeDict } = hub.handle(
  {
    users: {
      fetchAllPending: state => state.set('isLoading', true),
      fetchAllFulfilled: (state, { payload }) =>
        state.mergeDeep({
          users: Immutable.fromJS(payload.data),
          isLoading: false,
        }),
      fetchAllRejected: state => state.set('isLoading', false),
      addPending: state => state.set('isLoading', true),
      // ...
      deleteFulfilled: (state, { payload }) =>
        state.set(
          'users',
          state.get('users').filter(user => user.get('id') !== payload.userId),
        ),
    },
  },
  Immutable.fromJS({
    users: [],
    isLoading: false,
  }),
);

How to use redux-cube with redux-source:

See webcube-examples

Ducks Modular / Reducer Bundle

Original Ducks Modular:

// widgets.js

// Action Types

// Action Creators

// Side Effects
// e.g. thunks, epics, etc

// Reducer

For reference:

Redux Cube's Reducer Bundle:

// sampleApp/ducks/actions/sample.js
import hub from '../../hub';

export const { actions, types, typeDict } = hub.add({
  myType1: asyncPayloadCreator1,
  myType2: asyncPayloadCreator2,
});
// sampleApp/ducks/sample.js
import hub from '../hub';
import { typeDict as existTypeDict } from './actions/sample';

export const { reducer, actions, types, typeDict } = hub.handle(
  // declared action type
  myType1: (state, { payload, meta }) => newState,
  // undeclared action type
  myType3: (state, { payload, meta }) => newState,
  // undeclared action type
  myType4: (state, { payload, meta }) => newState,
}, initialStateForASliceOfStore).mergeActions(existTypeDict);

export const epics = [
  action$ =>
    action$.pipe(/* ... */)
];
import { actions, types, typeDict } from '../reducers/sample';

console.log(actions);
// {
//   myType1: asyncActionCreator1,
//   myType2: asyncActionCreator2,
//   myType3: defaultActionCreator,
//   myType4: defaultActionCreator,
// }

console.log(typeDict);
// {
//   MY_TYPE_1: asyncActionCreator1,
//   MY_TYPE_2: asyncActionCreator2,
//   MY_TYPE_3: defaultActionCreator,
//   MY_TYPE_4: defaultActionCreator,
// }

It is highly recommended to use "duck" files as the only authoritative sources of action types and action creators.

Action files should be only used by "duck" files. They should be totally transparent to all other code.

Because hub.handle can automatically add actions for undeclared action types, you only need to manually call hub.add (and maybe write them in a separate action file) when these actions have side effects

Connect to React Components

// sampleApp/containers/Sample.jsx
import { connect } from 'redux-cube';
import { Bind } from 'lodash-decorators';
import { actions as todoActions } from '../ducks/todo';

@connect({
  selectors: [
    state => state.todo.input,
    state => state.todo.items,
  ],
  transform: (input, items) => ({
    input,
    items,
    count: items.filter(item => !item.isCompleted).length,
  }),
 actions: todoActions,
})
export default class Main extends PureComponent {
  @Bind
  handleInputChange(content) {
   this.props.actions.todo.changeInput(content);
  }
  render() {
    const { input, items, count } = this.props;

Te above code is equal to

// ...
import { createSelector } from 'reselect';

@connect({
  mapStateToProps: createSelector(
    [
      state => state.todo.input,
      state => state.todo.items,
    ],
    transform: (input, items) => ({
      input,
      items,
      count: items.filter(item => !item.isCompleted).length,
    }),
  ),
  mapDispatchToProps: dispatch => ({
    actions: {
      todo: {
        changeInput: (...args) => dispatch(
          todoActions.todo.changeInput(...args)
        ),
      },
    },
  }),
})
export default class Main extends PureComponent {

mapDispatchToProps option can be used together with actions option.

mapStateToProps option can be used together with selectors option.

Sub-Apps

// multipleTodoApp/todo/index.jsx
import React, { Component } from 'react';
import withPersist from 'redux-cube-with-persist';
import localforage from 'localforage';
import { createApp } from 'redux-cube';

import { reducer as sampleReducer, epics } from './ducks/sample';
import { reducer as sample2Reducer, epics } from './ducks/sample2';
import Sample from './containers/Sample';

@createApp(withPersist({
  reducers: {
    items: sampleReducer,
    sample2: {
      data: sample2Reducer,
    },
  },
  epics,
  preloadedState: typeof window !== 'undefined' && window._preloadTodoData,
  devToolsOptions: { name: 'TodoApp' },
  persistStorage: localforage,
  persistKey: 'todoRoot',
}))
class TodoApp extends Component {
  render() {
    return <Sample />;
  }
}

export const App = TodoApp;
// multipleTodoApp/index.jsx
import React, { Component } from 'react';
import { Route, Redirect, Switch } from 'react-router-dom';
import { createApp } from 'redux-cube';
import withRouter from 'redux-cube-with-router';
import { App as TodoApp } from './todo';

const JediTodoApp = () => (
  <TodoApp
    title="Jedi Todo"
    routePath="/jedi-todo"
    appConfig={{
      persistKey: 'jediTodoRoot',
      devToolsOptions: { name: 'JediTodoApp' },
      preloadedState:
        typeof window !== 'undefined' && window._preloadJediTodoData,
    }}
  />
);
const SithTodoApp = () => (
  <TodoApp
    title="Sith Todo"
    routePath="/sith-todo"
    appConfig={{
      persistKey: 'sithTodoRoot',
      devToolsOptions: { name: 'SithTodoApp' },
      preloadedState:
        typeof window !== 'undefined' && window._preloadSithTodoData,
    }}
  />
);

@createApp(withRouter({
  supportHtml5History: isDynamicUrl(),
  devToolsOptions: { name: 'EntryApp' },
}))
class EntryApp extends Component {
  render() {
    const TodoApps = () => (
      <div>
       <JediTodoApp />
       <SithTodoApp />
      </div>
    );
    const JumpToDefault = () => <Redirect to="jedi-todo/" />;
    return (
      <Switch>
        <Route path="/" exact={true} render={JumpToDefault} />
        <Route path="/" render={TodoApps} />
      </Switch>
    );
  }
}

export const App = EntryApp;

Immutable

Frozen plain object + immutability-helper / icepick / seamless-immutable / dot-prop-immutable / object-path-immutable / timm / updeep

@createApp(withPersist(withRouter({
  reducers,
  disableFreezeState: false, // default
  // ...
})))
import update from 'immutability-helper';
import hub from '../hub';

export const { reducer, actions, types, typeDict } = hub.handle({
  changeInput: (state, { payload: content }) =>
    update(state, {
     input: { $set: content },
    }),
  todo: {
    clearCompleted: state =>
      update(state, {
        items: {
         $apply: items =>
            items.map(item =>
              update(item, {
               isCompleted: { $set: false },
              }),
            ),
        },
      }),
  },
}, {
  items: [],
  input: '',
});

ImmutableJS object + redux-immutable

@createApp(withImmutable(withRouter({
  reducers, //
  // ...
})))

API

redux-cube

import { createApp, createHub, connect } from 'redux-cube'
createApp

It's mainly a wrapper of redux API and some must-have action middlewares, store enhancers, high-order reducers and high-order components.

It provides the support for Sub-App pattern (React component with its own isolated Redux store)

Options

createHub

An superset and enhanced implement (almost a rewrite) of redux-actions.

It provides the support for namespace management and Reducer-Bundle-or-Ducks-Modular-like pattern

Options:

  • delimiter
connect

It's mainly a wrapper of react-redux and reselect

Options:

  • selectors
  • transform
  • mapStateToProps
  • actions
  • actionsProp
  • mapDispatchToProps

Plugins