redux-saga-routines
A smart action creator for Redux. Useful for any kind of async actions like fetching data.
Also fully compatible with Redux Saga and Redux Form.
Why do I need this?
Reduce boilerplate from your source code when making requests to API or validating forms build on top of Redux Form.
Installation
yarn add redux-saga-routines
or
npm install --save redux-saga-routines
What is routine?
Routine is a smart action creator that encapsulates 5 action types and 5 action creators to make standard actions lifecycle easy-to-use:
TRIGGER
-> REQUEST
-> SUCCESS
/ FAILURE
-> FULFILL
So, with redux-saga-routines
you don't need to create all these action type constants and action creators manually, just use createRoutine
:
import { createRoutine } from 'redux-saga-routines';
const routine = createRoutine('ACTION_TYPE_PREFIX');
'ACTION_TYPE_PREFIX'
passed to createRoutine
is a name of routine (and a prefix for all it's action types).
You can access all action types using TRIGGER
, REQUEST
, SUCCESS
, FAILURE
, FULFILL
attributes of routine
object:
console.log(routine.TRIGGER);
console.log(routine.REQUEST);
console.log(routine.SUCCESS);
console.log(routine.FAILURE);
console.log(routine.FULFILL);
You also have 5 action creators: trigger
, request
, success
, failure
, fulfill
:
console.log(routine.trigger(payload));
console.log(routine.request(payload));
console.log(routine.success(payload));
console.log(routine.failure(payload));
console.log(routine.fulfill(payload));
Routine by itself is a trigger action creator function:
console.log(routine(payload));
console.log(routine.trigger(payload));
Every routine's action creator is a Flux Standard Action
Payload and meta creators for routines
redux-saga-routines
based on redux-actions, so createRoutine
actually accepts 3 parameters: (actionTypePrefix, payloadCreator, metaCreator) => function
.
Changing action payload with payloadCreator
You may pass a function as a second argument to createRoutine
and it will be used as a payload creator:
const routine = createRoutine('PREFIX', (value) => value * 2);
console.log(routine.trigger(1));
console.log(routine.request(2));
console.log(routine.success(3));
console.log(routine.failure(4));
console.log(routine.fulfill(5));
You may also pass object as a second argument to define unique payload creator for each action:
const payloadCreator = {
trigger: (payload) => ({ ...payload, trigger: true }),
request: ({ id }) => ({ id }),
success: (payload) => ({ ...payload, data: parseData(payload.data) }),
failure: (payload) => ({ errorMessage: parseError(payload.error), error: true }),
fulfill: () => ({}),
};
const routine = createRoutine('PREFIX', payloadCreator);
console.log(routine.trigger({ id: 42 }));
console.log(routine.request({ id: 42, foo: 'bar' }));
console.log(routine.success({ id: 42, data: 'something' }));
console.log(routine.failure({ id: 42, error: 'oops...' }));
console.log(routine.fulfill({ id: 42, foo: 'bar', baz: 'zab' }));
You may use lower or uppercase for payloadCreator
-object keys:
const payloadCreator = {
trigger: () => {},
REQUEST: () => {},
};
Adding or changing action meta with metaCreator
createRoutine
accept third parameter and treat it as metaCreator
. It works almost the same as payloadCreator
(function or object is accepted)
the only difference is it works with action.meta
instead of action.payload
parameter:
const simpleMetaCreator = () => ({ foo: 'bar' });
const routineWithSimpleMeta = createRoutine('PREFIX', null, simpleMetaCreator);
console.log(routineWithSimpleMeta.trigger());
const complexMetaCreator = {
trigger: () => ({ trigger: true }),
request: () => ({ ignoreCache: true }),
success: () => ({ saveToCache: true }),
failure: () => ({ logSomewhere: true }),
fulfill: () => ({ yo: 'bro!' }),
};
const routineWithComplexMeta = createRoutine('PREFIX', null, complexMetaCreator);
console.log(routineWithSimpleMeta.trigger());
console.log(routineWithSimpleMeta.request());
console.log(routineWithSimpleMeta.success());
console.log(routineWithSimpleMeta.failure());
console.log(routineWithSimpleMeta.fulfill());
Creating your own routines
Sometimes you may need custom routines, so now you are able to create your own routine creator to get them!
import { createRoutineCreator } from 'redux-saga-routines';
const createToggleRoutine = createRoutineCreator(['SHOW', 'HIDE', 'TOGGLE']);
console.log(createToggleRoutine.STAGES);
const myToggler = createToggleRoutine('PREFIX');
console.log(myToggler._STAGES);
console.log(myToggler._PREFIX);
console.log(myToggler.SHOW);
console.log(myToggler.HIDE);
console.log(myToggler.TOGGLE);
console.log(myToggler.show(payload));
console.log(myToggler.hide(payload));
console.log(myToggler.toggle(payload));
So, now you are able to group any actions into custom routine and use it as you want!
createRoutineCreator
also accepts custom separator as a second parameter, so you are able to change slash /
with anything you want:
import { createRoutineCreator, defaultRoutineStages } from 'redux-saga-routines';
const createUnderscoreRoutine = createRoutineCreator(defaultRoutineStages, '_');
const routine = createUnderscoreRoutine('ACTION_TYPE_PREFIX');
console.log(routine.TRIGGER);
console.log(routine.REQUEST);
console.log(routine.SUCCESS);
console.log(routine.FAILURE);
console.log(routine.FULFILL);
In example above you may notice, that default routine stages are also exported from the package, so you may use them to create your own extended routine.
Now all the power is in your hands, use it as you want!
Usage examples
Example: fetching data from server
Let's start with creating routine for fetching some data from server:
import { createRoutine } from 'redux-saga-routines';
export const fetchData = createRoutine('FETCH_DATA');
Then, let's create some component, that triggers data fetching:
import { connect } from 'react-redux';
import { fetchData } from './routines';
class FetchButton extends React.Component {
static mapStateToProps = (state) => {
return {...};
}
static mapDispatchToProps = {
fetchData,
};
onClick() {
this.props.fetchData();
}
render() {
return (
<button onClick={() => this.onClick()}>
Fetch data from server
</button>
);
}
}
export default connect(FetchButton.mapStateToProps, FetchButton.mapDispatchToProps)(FetchButton);
Now, let's take a look at reducer example:
import { fetchData } from './routines';
const initialState = {
data: null,
loading: false,
error: null,
};
export default function exampleReducer(state = initialState, action) {
switch (action.type) {
case fetchData.TRIGGER:
return {
...state,
loading: true,
};
case fetchData.SUCCESS:
return {
...state,
data: action.payload,
};
case fetchData.FAILURE:
return {
...state,
error: action.payload,
};
case fetchData.FULFILL:
return {
...state,
loading: false,
};
default:
return state;
}
}
And, saga (but you can use any other middleware, like redux-thunk
):
import { fetchData } from './routines';
function* requestWatcherSaga() {
yield takeEvery(fetchData.TRIGGER, fetchDataFromServer)
}
function* fetchDataFromServer() {
try {
yield put(fetchData.request());
const response = yield call(apiClient.request, '/some_url');
yield put(fetchData.success(response.data));
} catch (error) {
yield put(fetchData.failure(error.message));
} finally {
yield put(fetchData.fulfill());
}
}
Filtering actions
It is a common case to ignore some triggered actions and not to perform API request every time.
For example, let's make a saga, that perform API request only on odd button clicks (1st, 3rd, 5th, ...):
import { fetchData } from './routines';
function* requestWatcherSaga() {
yield takeEvery(fetchData.TRIGGER, handleTriggerAction)
}
let counter = 0;
function* handleTriggerAction() {
if (counter++ % 2 === 0) {
yield call(fetchDataFromServer);
}
yield put(fetchData.fulfill());
}
function* fetchDataFromServer() {
try {
yield put(fetchData.request());
const response = yield call(apiClient.request, '/some_url');
yield put(fetchData.success(response.data));
} catch (error) {
yield put(fetchData.failure(error.message));
}
}
Wrap routine into promise
Sometimes it is useful to use promises (especially with 3rd-party components). With redux-saga-routines
you are able to wrap your routine into promise and handle it in your saga!
To achive this just add routinePromiseWatcherSaga
in your sagaMiddleware.run()
, for example like this:
import { routinePromiseWatcherSaga } from 'redux-saga-routines';
const sagas = [
yourFirstSaga,
yourOtherSaga,
routinePromiseWatcherSaga,
];
sagas.forEach((saga) => sagaMiddleware.run(saga));
Now we are ready. There is special promisifyRoutine
helper, that wraps your routine in function with signature: (payload, dispatch) => Promise
.
See example below:
First, create routine:
import { createRoutine, promisifyRoutine } from 'redux-saga-routines';
export const myRoutine = createRoutine('MY_ROUTINE');
export const myRoutinePromiseCreator = promisifyRoutine(myRoutine);
Then, use it in your form component:
import { bindPromiseCreators } from 'redux-saga-routines';
import { myRoutine, myRoutinePromiseCreator } from './routines';
class MyComponent extends React.Component {
static mapStateToProps(state) {
}
static mapDispatchToProps(dispatch) {
return {
...bindPromiseCreators({
myRoutinePromiseCreator,
}, dispatch),
dispatch,
};
}
handleClick() {
const promise = this.props.myRoutinePromiseCreator(somePayload);
promise.then(
(successPayload) => console.log('success :)', successPayload),
(failurePayload) => console.log('failure :(', failurePayload),
);
setTimeout(
() => this.props.dispatch(myRoutine.success('voila!')),
5000,
);
}
render() {
return (
<button onClick={() => this.handleClick()}>
{/* your form fields here... */}
</form>
);
}
}
export default connect(MyComponent.mapStateToProps, MyComponent.mapDispatchToProps)(MyComponent);
You are able to resolve/reject given promise in your saga:
import { myRoutine } from './routines';
function* myRoutineTriggerWatcher() {
yield takeEvery(myRoutine.TRIGGER, handleTriggerAction)
}
function* handleTriggerAction(action) {
const { payload } = action;
const isDataCorrect = verifyData(payload);
if (isDataCorrect) {
yield call(sendFormDataToServer, payload);
} else {
yield put(myRoutine.failure('something went wrong'));
}
yield put(myRoutine.fulfill());
}
function* sendFormDataToServer(data) {
try {
yield put(myRoutine.request());
const response = yield call(apiClient.request, '/endpoint', data);
yield put(myRoutine.success(response.data));
} catch (error) {
yield put(myRoutine.failure(error.message);
}
}
redux-saga
, redux-form
, redux-saga-routines
combo
You are also allowed to use combo of redux-saga
, redux-form
and redux-saga-routines
!
Since redux-form
validation based on promises, you are able to handle redux-form
validation in your saga.
To achive this just add routinePromiseWatcherSaga
in your sagaMiddleware.run()
, like in example above.
There is a special bindRoutineToReduxForm
helper, that wraps your routine in function with redux-form
compatible signature: (values, dispatch, props) => Promise
(it works just like promisifyRoutine
but more specific to be compatible with full redux-form
functionality)
First, create routine and it's wrapper for redux-form
:
import { createRoutine, bindRoutineToReduxForm } from 'redux-saga-routines';
export const submitFormRoutine = createRoutine('SUBMIT_MY_FORM');
export const submitFormHandler = bindRoutineToReduxForm(submitFormRoutine);
Then, use it in your form component:
import { reduxForm } from 'redux-form';
import { submitFormHandler } from './routines';
class MyForm extends React.Component {
render() {
return (
<form onSubmit={this.props.handleSubmit(submitFormHandler)}>
{/* your form fields here... */}
</form>
);
}
}
export default reduxForm()(MyForm);
Now you are able to handle form submission in your saga:
import { SubmissionError } from 'redux-form';
import { submitFormRoutine } from './routines';
function* validateFormWatcherSaga() {
yield takeEvery(submitFormRoutine.TRIGGER, validate)
}
function* validate(action) {
const { values, props } = action.payload;
if (!isValid(values, props)) {
const errors = getFormErrors(values, props);
yield put(submitFormRoutine.failure(new SubmissionError(errors)));
} else {
yield call(sendFormDataToServer, values);
}
yield put(submitFormRoutine.fulfill());
}
function* sendFormDataToServer(formData) {
try {
yield put(submitFormRoutine.request());
const response = yield call(apiClient.request, '/submit', formData);
yield put(submitFormRoutine.success(response.data));
} catch (error) {
yield put(submitFormRoutine.failure(new SubmissionError({ _error: error.message })));
}
}
Version 2
Module was totally reworked since version 2.0.0. If you still using version 1.* see version 1 docs
License
MIT