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.
Version 2
Module was totally reworked since version 2.0.0. If you still using version 1.* see version 1 docs
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
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:
routine.TRIGGER === 'ACTION_TYPE_PREFIX/TRIGGER';
routine.REQUEST === 'ACTION_TYPE_PREFIX/REQUEST';
routine.SUCCESS === 'ACTION_TYPE_PREFIX/SUCCESS';
routine.FAILURE === 'ACTION_TYPE_PREFIX/FAILURE';
routine.FULFILL === 'ACTION_TYPE_PREFIX/FULFILL';
You also have 5 action creators: trigger
, request
, success
, failure
, fulfill
:
routine.trigger(payload) === { type: 'ACTION_TYPE_PREFIX/TRIGGER', payload };
routine.request(payload) === { type: 'ACTION_TYPE_PREFIX/REQUEST', payload };
routine.success(payload) === { type: 'ACTION_TYPE_PREFIX/SUCCESS', payload };
routine.failure(payload) === { type: 'ACTION_TYPE_PREFIX/FAILURE', payload };
routine.fulfill(payload) === { type: 'ACTION_TYPE_PREFIX/FULFILL', payload };
Routine by itself is a trigger action creator function:
expect(routine(payload)).to.deep.equal(routine.trigger(payload));
redux-saga-routines
based on redux-actions, so createRoutine
actually accepts 3 parameters: (actionTypePrefix, payloadCreator, metaCreator) => function
.
Every routine action creator is a redux-actions
FSA, so you can use them with handleAction(s)
or combineActions
from redux-actions
Usage
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(sagaMiddleware.run);
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 are 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 })));
}
}
License
MIT