Redux ~ Saga - rohit120582sharma/Documentation GitHub Wiki

Redux-saga is a redux middleware library, that is designed to make handling side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in your redux app nice and simple.

Redux-Saga is an immensely powerful tool that comes packed with a lot of useful helper functions. E.g. it already has built in ways to handle advanced tasks like throttling, debouncing, race conditions and cancellation.

It achieves this by leveraging an ES6 feature called Generators, allowing us to write asynchronous code that looks synchronous, and is very easy to test.

Redux-Saga forms a wrapper around generators that simplify managing promises.

A saga is like a separate thread in your application that is solely responsible for side effects and it can handle all the normal Redux actions and it can dispatch Redux actions as well.


Watchers & Workers

The main saga file is usually split into two different classes of sagas: workers & watchers.

Watcher saga's see every action that is dispatched to the redux store, if it matches the action they are told to handle, they assign it to their worker saga.


Workflow steps

  • An event takes place — e.g. user does something (clicks a button) or an update occurs (like componentDidMount)
  • Based on the event, an action is dispatched, likely through a function declared in mapDispatchToProps
  • A watcherSaga sees the action and triggers a workerSaga. Use saga helpers to watch for actions differently.
  • While the saga is starting, the action also hits a reducer and updates some piece of state to indicate that the saga has begun and is in process
  • The workerSaga performs some side-effect operation
  • Based on the result of the workerSaga‘s operation, it dispatches an action to indicate that result. If successful (API_CALL_SUCCESS), you might include a payload in the action. If an error (API_CALL_FAILURE), you might send along an error object for more details on what went wrong.
  • The reducer handles the success or failure action from the workerSaga and updates the Store accordingly with any new data, as well as sets the “in process” indicator to false.

Throughout this process, you can use the updates to Redux state flowing through to props to keep your user informed of the process and progress thereof.


References



Install and Configure

If you want to include redux-saga as part of your project’s, you can do so using npm:

npm install --save redux-saga

Then add the middleware to your Redux store:

/*-----------------------------
index.js
-----------------------------*/
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import watchFetchUser from './sagas';

// create the saga middleware
const sagaMiddleware = createSagaMiddleware();

// mount it on the Store
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

// then run the saga
sagaMiddleware.run(watchFetchUser);

// render the application

Then create a saga that watches for actions and triggers an API call:

/*-----------------------------
sagas.js
-----------------------------*/
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import Api from '...';


// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* workerFetchUser(action) {
	try{
		const user = yield call(Api.fetchUser, action.payload.userId);
		yield put({type: "USER_FETCH_SUCCEEDED", user: user});
	}catch(e){
		yield put({type: "USER_FETCH_FAILED", message: e.message});
	}
}


/**
 * Starts workerFetchUser on each dispatched "USER_FETCH_REQUESTED" action.
 * Allows concurrent fetches of user.
 */
function* watchFetchUser() {
	yield takeEvery('USER_FETCH_REQUESTED', workerFetchUser);
}

/**
 * Alternatively you may use takeLatest.
 * 
 * Does not allow concurrent fetches of user.
 * If "USER_FETCH_REQUESTED" gets dispatched while a fetch is already pending, that pending fetch is cancelled and only the latest one will be run.
 */
function* watchFetchUser() {
	yield takeLatest('USER_FETCH_REQUESTED', workerFetchUser);
}


export default watchFetchUser;


Effects

Effect objects causes side-effects when yielded inside a Redux-Saga, but have no effect outside that context. These side-effects can include dispatching an action to the application or even making an outside API call using AJAX.

Redux-saga provides us with a few different "effects" and helper methods that scoot things along.


Thread Management

call

It simply calls the specified method.

If it returns a promise, pauses the saga until the promise is resolved. It is used when we need to get data asynchronously, and might need to do some stuff in between (like when you use fetch to make an API call). It takes two arguments - a callback, and an optional spread of additional arguments

It is very similar to invoking the method directly. Since effects don't actually do anything outside of Redux-Saga, if we call a method directly instead of yielding to it, we can't test whether that method is really called.

function* process(){
	let result = yield call(fnToRun, optionalArgsToPassToFn);
	console.log(result);
	yield put(actionCreators.actionToDispatch(result));
}

fork

It works a lot like call. When you use call the code stops untill the called function or generator completes, but with fork your existing thread of code just keeps right on running.

Every time a fork effect is triggered, a new child process is created. Each child process run independently, but every child process is dependent on the parent. If parent makes an error or if it is cancelled, then all the forked processes are cancelled as well.

Forked methods can use a try/finally block, similar to try/catch block, to tell if they have been cancelled or if their parent has error.

spawn

It creates a new process similar to fork. The caller is not interrupted in its processing and goes down to the next line.

The new process is not a child process of the caller. So if the caller errors or is cancelled, the new process is not effected. This is the difference between spawn and fork.


Action creation

put

It dispatches an action to the rest of the app. It is functionally very similar to just calling dispatch in a different part of your application. This does not pause the code execution, it just dispatches an action like calling dispatch in Redux-Thunk or React-Redux.


Data seeding

select

It returns a copy of the application's state when yield to this effect.

You can pass a selector to select and it will just return the properties from state you want.


Flow control

take

It pauses code execution until action is called.

It pauses at a particular line of code through yield keyword and when the specified action is dispatched, the code resumes. Since we use the yield keyword to do this, whatever the properties of the action are, are actually passed to a running saga when the action occurs.

takeEvery

It works as a combination of take and fork.

Every time the specified action is dispatched, it forks a new child process.

So when the specified action is dispatched, the takeEvery is triggered and a child process is forked. The child process runs continuously in its own thread. Then the same action is dispatched again, it forks another child process right away.

function* watchSaga(){
	yield takeEvery('ACTION', workerSagaToRun);
}

takeLatest

It works as a combination of fork, takeEvery, and cancel.

Every time the specified action is dispatched, it forks a new child process, but it keeps exactly one instance of the process running at anytime.

So when the specified action is dispatched, the takeLatest is triggered and a child process is forked. The child process runs continuously in its own thread. Then the same action is dispatched again, instead of forking another process right away like with takeEvery, first the previous child process is cancelled then the new child process forked.

import { delay, takeLatest } from 'redux-saga/effects';

...
function* process(){
	let loopTimes = 0;
	while(true){
		console.info(`Loop is running ${loopTimes} times`);
		loopTimes++;
		yield delay(500);
	}
}
function* saga(){
	yield takeLatest('CALL_PROCESS', process);
}

...
dispatch({
	type: 'CALL_PROCESS'
});


Testing

Saga effects are largely related to testing as they can be easily tested against expected values.

Unit tests verify the value of yielded effects.

Sagas are advanced manually during tests by calling next, and therefore a saga can be tested instantly.

Sagas must be written to use call instead of directly invoking API methods to use unit tests effectively.

Dependencies don't need to be injected for unit tests.


⚠️ **GitHub.com Fallback** ⚠️