redux-define
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']);
console.log('' + CREATE_TODO);
console.log('' + CREATE_TODO.ERROR);
console.log('' + 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');
console.log('' + CREATE_TODO);
console.log('' + CREATE_TODO.ERROR);
console.log('' + 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);
console.log('' + CREATE_TODO);
console.log('' + CREATE_TODO.ERROR);
console.log('' + 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']);
console.log('' + CREATE_TODO);
console.log('' + CREATE_TODO.ACTION);
console.log('' + CREATE_TODO.ERROR);
console.log('' + 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);
console.log('' + todos);
console.log('' + todos.LOADING);
console.log('' + todos.SUCCESS);
console.log('' + CREATE);
console.log('' + CREATE.ERROR);
console.log('' + CREATE.SUCCESS);
Best practice
Extract general state constants into a separate file so they can easily be
imported and shared across different modules:
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS';
export const myApp = defineAction('my-app');
In the module; we can import the stateConstants
and optionally parent modules
to construct a namespace.
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);
console.log('' + myApp);
console.log('' + todos);
console.log('' + todos.LOADING);
console.log('' + todos.SUCCESS);
console.log('' + CREATE);
console.log('' + CREATE.ACTION);
console.log('' + CREATE.ERROR);
console.log('' + 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.