Skip to content

Latest commit

 

History

History
327 lines (247 loc) · 9.93 KB

README.md

File metadata and controls

327 lines (247 loc) · 9.93 KB

redux-define

Join the chat at https://gitter.im/smeijer/redux-define

build status

NPM

Installation

with npm:

npm install --save redux-define

or yarn:

yarn add redux-define

If you don’t use npm, you may grab the latest UMD build from unpkg (either a development or a production build). The UMD build exports a global called window.ReduxDefine if you add it to your page via a <script> tag.

We don’t recommend UMD builds for any serious application, as most of the libraries complementary to Redux are only available on npm.

Usage

defineAction(type, ?[subactions], ?namespace)

import { defineAction } from 'redux-define';

Create a redux action type with one or more subactions:

const CREATE_TODO = defineAction('CREATE_TODO', ['ERROR', 'SUCCESS']);

// result:
console.log('' + CREATE_TODO);            // CREATE_TODO
console.log('' + CREATE_TODO.ERROR);      // CREATE_TODO_ERROR
console.log('' + CREATE_TODO.SUCCESS);    // CREATE_TODO_SUCCESS;

Namespaces can be used to separate actions through out modules and apps.

const CREATE_TODO = defineAction('CREATE_TODO', ['ERROR', 'SUCCESS'], 'my-app');

// result:
console.log('' + CREATE_TODO);            // my-app/CREATE_TODO
console.log('' + CREATE_TODO.ERROR);      // my-app/CREATE_TODO_ERROR
console.log('' + CREATE_TODO.SUCCESS);    // my-app/CREATE_TODO_SUCCESS;

It's also possible to give in another constant as namespace for the new one.

const todos = defineAction('todos', ['LOADING', 'SUCCESS'], 'my-app');
const CREATE_TODO = defineAction('CREATE_TODO', ['ERROR', 'SUCCESS'], todos);

// result:
console.log('' + CREATE_TODO);            // my-app/todos/CREATE_TODO
console.log('' + CREATE_TODO.ERROR);      // my-app/todos/CREATE_TODO_ERROR
console.log('' + CREATE_TODO.SUCCESS);    // my-app/todos/CREATE_TODO_SUCCESS;

To integrate better with other redux libraries, a special ACTION property is added to the constant. redux-actions and redux-saga for example treat actionTypes other than string specially.

Extra benefit of this little feature, is that it makes the separation between user actions and status updates more clear. Read more about this under best practice and integrations

const CREATE_TODO = defineAction('CREATE_TODO', ['ERROR', 'SUCCESS']);

// result:
console.log('' + CREATE_TODO);            // CREATE_TODO
console.log('' + CREATE_TODO.ACTION);     // CREATE_TODO
console.log('' + CREATE_TODO.ERROR);      // CREATE_TODO_ERROR
console.log('' + CREATE_TODO.SUCCESS);    // CREATE_TODO_SUCCESS;

actionType.defineAction(type, ?[subactions])

As alternative syntax, we can use the defineAction method on defined constants. Constants defined in this way inherit their namespace. Making the namespace argument obsolete.

const myApp = defineAction('my-app');
const todos = myApp.defineAction('todos', ['LOADING', 'SUCCESS']);
const CREATE = todos.defineAction('CREATE', ['ERROR', 'SUCCESS']);

This is the same as writing:

const myApp = defineAction('my-app');
const todos = defineAction('todos', ['LOADING', 'SUCCESS'], 'my-app');
const CREATE = todos.defineAction('CREATE', ['ERROR', 'SUCCESS'], todos);

Or if you only need the CREATE constant:

const CREATE = todos.defineAction('CREATE', ['ERROR', 'SUCCESS'], 'my-app/todos');

Result in these cases is the same. Except in the third case, where we only defined the CREATE constant:

console.log('' + myApp);                  // my-app

console.log('' + todos);                  // my-app/todos
console.log('' + todos.LOADING);          // my-app/todos_LOADING
console.log('' + todos.SUCCESS);          // my-app/todos_SUCCESS

console.log('' + CREATE);                 // my-app/todos/CREATE
console.log('' + CREATE.ERROR);           // my-app/todos/CREATE_ERROR;
console.log('' + CREATE.SUCCESS);         // my-app/todos/CREATE_SUCCESS;

Best practice

Extract general state constants into a separate file so they can easily be imported and shared across different modules:

// stateConstants.js
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS';
// app.js
export const myApp = defineAction('my-app');

In the module; we can import the stateConstants and optionally parent modules to construct a namespace.

// todos.js
import { defineAction } from 'redux-define';
import { LOADING, ERROR, SUCCESS } from './stateConstants';
import { myApp } from './app';

const todos = defineAction('todos', [LOADING, SUCCESS], myApp);
const CREATE = defineAction('CREATE', [ERROR, SUCCESS], todos);

// result:
console.log('' + myApp);                  // my-app

console.log('' + todos);                  // my-app/todos
console.log('' + todos.LOADING);          // my-app/todos_LOADING
console.log('' + todos.SUCCESS);          // my-app/todos_SUCCESS

console.log('' + CREATE);                 // my-app/todos/CREATE
console.log('' + CREATE.ACTION);          // my-app/todos/CREATE
console.log('' + CREATE.ERROR);           // my-app/todos/CREATE_ERROR
console.log('' + CREATE.SUCCESS);         // my-app/todos/CREATE_SUCCESS

Use the ACTION constant in dispatch and in saga watchers. This makes it clear that an user or system ACTION is being handled. All other subtypes should be status updates. They should be handled trough thunks or sagas, but never dispatched by a user. Although it is possible to handle user actions in the reducer directly, the advice is to not do this. Keep clear separation between user actions and reducer actions.

Implementation example

stateConstants.js
export const CANCELLED = 'CANCELLED';
export const ERROR     = 'ERROR';
export const PENDING   = 'PENDING';
export const SUCCESS   = 'SUCCESS';
actionTypes.js
import { defineAction } from 'redux-define';
import { CANCELLED, ERROR, PENDING, SUCCESS } from './stateConstants';

export const DELETE_COMMENT = defineAction('DELETE_COMMENT',
	[CANCELLED, ERROR, PENDING, SUCCESS], 'comments');
actions.js
import { createAction } from 'redux-actions';
import { DELETE_COMMENT } from './actionTypes';

export const deleteComment = createAction(DELETE_COMMENT.ACTION);
reducer.js
import { handleActions, combineActions } from 'redux-actions';
import { DELETE_COMMENT } from './actionTypes';

const initialState = {
  isDeleting: false,
};

const reducer = handleActions({
  [DELETE_COMMENT.PENDING]: state => ({
    ...state,
    isDeleting: true,
  }),

  [combineActions(
    DELETE_COMMENT.CANCELLED,
    DELETE_COMMENT.SUCCESS,
    DELETE_COMMENT.ERROR,
  )]: state => ({
    ...state,
    isDeleting: false,
  }),
}, initialState);
sagas.js
import { call, put, take } from 'redux-saga/effects';
import deleteAPI from 'somewhere-out-of-this-scope';
import { DELETE_COMMENT } from './actionTypes';

export function* deleteComment({ payload }) {
  try {
    yield put({ type: DELETE_COMMENT.PENDING });
    const { data } = yield call(deleteAPI, payload);
    yield put({ type: DELETE_COMMENT.SUCCESS, payload: data });
  }
  catch (error) {
    yield put({ type: DELETE_COMMENT.ERROR, payload: { error: error.message } });
  }
}
watchers.js
import { takeEvery } from 'redux-saga';
import { fork } from 'redux-saga/effects';

import { DELETE_COMMENT } from './actionTypes';
import * as s from './sagas';

function* deleteCommentWatcher() {
  yield* takeEvery(DELETE_COMMENT.ACTION, s.deleteComment);
}

export default function* () {
  yield [
    fork(deleteCommentWatcher),
  ];
}

Why use redux-define?

This library reduces a lot of the boilerplate that comes with defining redux action types. This library is created as solution to organizing large ducks Let's show the difference here. See above for a full implementation example. When using ducks, some of the files in the example above should be joined into a single duck file.

Without using redux-define

const CREATE_TODO = 'CREATE_TODO';
const CREATE_TODO_PENDING = 'CREATE_TODO_PENDING';
const CREATE_TODO_ERROR = 'CREATE_TODO_ERROR';
const CREATE_TODO_SUCCESS = 'CREATE_TODO_SUCCESS';

const DELETE_TODO = 'DELETE_TODO';
const DELETE_TODO_PENDING = 'DELETE_TODO_PENDING';
const DELETE_TODO_CANCELLED = 'DELETE_TODO_CANCELLED';
const DELETE_TODO_ERROR = 'DELETE_TODO_ERROR';
const DELETE_TODO_SUCCESS = 'DELETE_TODO_SUCCESS';

With redux-define

import { defineAction } from 'redux-define';
import { PENDING,  CANCELLED, ERROR, SUCCESS } from '/lib/stateConstants.js';

const CREATE_TODO = defineAction('CREATE_TODO', [PENDING, ERROR, SUCCESS]);
const DELETE_TODO = defineAction('DELETE_TODO', [PENDING, CANCELLED, ERROR, SUCCESS]);

Integrations

Created constants can be directly used in sagas reducers, or together with redux-actions.

See implementation example in this readme for implementation details. We handle redux-actions in actions.js and reducer.js and redux-saga in watchers.js and sagas.js.