1. What is Redux-Saga?
redux-saga
is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.
Saga is related to Redux as the name suggests. Redux-saga is a library in the form of Redux middleware designed to gracefully manage Side Effects in Redux applications, with more efficient execution, easier testing, and easier failure handling. Similarly, saga’s relationship with Redux can be seen from the logo.
Saga was developed from a Cornell university paper (link) to solve the problem of data consistency for long-running transactions (LLTS) in distributed systems.
2. What is SideEffects?
Side effects are the most common way that a program interacts with the outside world (people, filesystems, other computers on networks).
In Javascript program, Side Effects mainly refers to asynchronous network request, local reading localStorage/Cookie and other external operations:
Asynchronous things like data fetching and impure things like accessing the browser cache
In Web applications, the emphasis is on managing Side Effects elegantly rather than eliminating them.
What is the difference between saga and Thunk?
First, compare saga and Thunk’s package sizes, and the difference is as much as 10 times.
Both Redux-Thunk and Redux-Saga are middleware for Redux. Redux, as the main body, provides a uniform format for each middleware, issues getState and Dispatch, and calls Dispatch to collect actions.
//compose.js
function compose(..funcs) {
if (funcs.length === 0) {
retyrb arg => arg
}
if (funcs.length === 1) {
return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }//applyMiddleware.js
function applyMiddleware(. middlewares) {
return (createStore) = > (reducer, preloaderState, enhancer) = > {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
diapatch: (action) = > dispatch(action)
}
chain = middlewares.map(middleware= >middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
...store,
dispatch
}
}
}
Copy the code
Next, let’s take a look at the thunk function, which is introduced in ruan’s article:
function f(m){
return m * 2;
}
f(x + 5);
/ / is equivalent to
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
Copy the code
The compiler’s “call by name” implementation usually puts arguments in a temporary function that is passed into the function body. This temporary function is called the Thunk function. In the JavaScript language, the Thunk function replaces not an expression, but a multi-argument function, replacing it with a single-argument version that accepts only callback functions as arguments.
Then let’s look at the thunk source code
function createThunkMiddleware(extraArgument) {
//dispath, which can be used to dispatch new actions
GetState, which can be used to access the current state
return ({dispatch, getState}) = > (next) = > (action) = > {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
Copy the code
Redux-thunk is middleware that monitors every action coming into the system and calls that function, if it’s a function. That’s what Redux-Thunk does. Redux-thunk chooses to augment redux Store’s dispatch method in the form of Middleware (i.e. Supports Dispatch (function)), so that it can obtain data asynchronously while further separating the business logic related to data acquisition from the View layer.
Then take a look at Redux-Saga. Saga mode communicates with each saga in the form of command/reply, and the corresponding saga will be executed when the command is received, as shown in the figure:
The Saga mode isolates services and uses centrally distributed transaction orchestration to avoid cyclic dependencies between services and facilitate testing. It also reduces the complexity of the participants, as they only need to execute/reply to commands. However, saga produces a lot of useless action.types.
Redux-thunk and Redux-Saga are both middleware for Redux, but their design philosophy is different, so they are used in different ways.
// action.js
// ---------
ActionCreator (e.g. fetchData) returns function
// function contains the business data request code logic
// Handle request success and request failure separately as a callback
export function fetchData(someValue) {
return (dispatch, getState) = > {
myAjaxLib.post("/someEndpoint", { data: someValue })
.then(response= > dispatch({ type: "REQUEST_SUCCEEDED".payload: response })
.catch(error= > dispatch({ type: "REQUEST_FAILED".error: error });
};
}
// component.js
// ------------
// View layer Dispatch (fn) triggers asynchronous requests
// Omit some code here
this.props.dispatch(fetchData({ hello: 'saga' }));
Copy the code
Redux-saga: Redux-Saga
// saga.js
// -------
// worker saga
// It is a generator function
// fn also contains the business data request code logic
// But the execution logic of the code is synchronous.
function* fetchData(action) {
const { payload: { someValue } } = action;
try {
const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue });
yield put({ type: "REQUEST_SUCCEEDED".payload: response });
} catch (error) {
yield put({ type: "REQUEST_FAILED".error: error }); }}// watcher saga
// Listen to each dispatch(action)
// If action.type === 'REQUEST', execute fetchData
export function* watchFetchData() {
yield takeEvery('REQUEST', fetchData);
}
// component.js
// -------
// The View layer dispatch(action) triggers the asynchronous request
// The action can still be a plain object
this.props.dispatch({
type: 'REQUEST'.payload: {
someValue: { hello: 'saga'}}});Copy the code
In conclusion, redux-Saga is different from Redux-Thunk in the following points
1. The business logic related to data acquisition is moved to a separate saga. Js, no longer mixed in action.js or component.js.
2. Each saga is a generator function. The code is written synchronously to handle asynchronous logic, making the code easier to read.
4. Learn to use Saga
Saga provides two MIDDLEwareapis: createSagaMiddleware and middleware.run.
CreateSagaMiddleware (Options): Create a Redux Middleware and connect Sagas to the Redux Store. Options The following options are supported:
-
SagaMontior: Used to receive monitoring events passed by middleware.
-
Emmiter: Used to bring actions from Redux to Redux-saga
-
Logger: Custom logging method (by default, Middleware logs all errors and warnings to the console).
-
OnError: When this method is provided, Middleware will call it with uncaught errors in Sagas.
middleware.run(saga, … Args): Run saga dynamically. Can only be used to execute Saga after the applyMiddleware stage, where args is the argument provided to Saga.
After installing all dependencies, first associate store with Saga, and finally implement Rootsaga.
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas'
import rootReducer from './reducers'
const sgagMiddleware = createSagaMiddleware();
const enhancer = applyMiddleware(sagaMiddleware);
const store = createStore(rootReducer, enhancer);
// Performs rootSaga, usually the initialization of the program.
sagaMiddleWare.run(rootSaga);
Copy the code
Then, I will introduce some of the more important concepts in Saga: Task, Channel, Buffer, SagaMonitor.
1.Task
The Task interface specifies the results of running a Saga through fork, middleare.run, or runSaga, and provides the corresponding function methods.
2.Channel
A channel is an object used to send and receive messages between tasks. A message from a sender is put into a queue before it is requested by an interested receiver; Registered recipients will be queued until the information is available.
The Channel interface defines three methods: take, PUT, and close
Channel.take(callback): Used to register a taker.
Channel.put(message): Used to put messages on buffers.
Channel.flush(callback): Used to extract all cached messages from a Channel.
Channel.close(): Closes a Channel, meaning that putting operations are no longer allowed.
3.Buffer
Used to implement caching policies for channels. The Buffer interface defines three methods: isEmpty, PUT, and take
isEmpty()
: Returns if there is no message in the cache. This method is called by channel whenever a new Taker is registered.put(message)
: used to put new messages into the cache. Note that the cache can choose not to store messages. (For example, an dropping buffer can discard any new messages that exceed a given limit.)take()
: Used to retrieve any cached messages. Note that the behavior of this method must be consistent withisEmpty
Consistent.
4.SagaMonitor
Used to initiate monitor events by middleware. In fact, Middleware initiates five events:
- When an effect is triggered (through
yield someEffect
), the Middleware callsagaMonitor.effectTriggered
- If the effect is successfully resolved, middleware is called
sagaMonitor.effectResolved
- If the effect is rejected with an error, middleware is called
sagaMonitor.effectRejected
- If the effect is cancelled, middleware is called
sagaMonitor.effectCancelled
- Finally, middleware is called when a Redux action is initiated
sagaMonitor.actionDispatched
In redux-Saga, the Effect creator is mainly maintained by Effect. The description of Effect is as follows:
An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.
An effect is essentially a generic object that contains instructions that are interpreted and executed by Saga Middleware (essentially a publish-subscribe mode). Source code analysis can refer to the article (juejin.cn/post/688522…
Take, for example, is an Effect creator that creates an Effect.
Official explanation:
- Each of the following Effect creation functions returns a plain Javascript object and does nothing else.
- Execution is performed by middleware during the above iteration.
- Middleware checks the description for each Effect and acts accordingly
The following is a brief explanation of each Effect creator, Effect combinator, and helper function:
Take: Creates an Effect description that commands middleware to wait for the specified action on the Store. The Generator pauses until an action matching pattern is initiated.
Put: Creates an Effect description that commands middleware to initiate an action to the Store. This effect is non-blocking, and any errors thrown downstream (for example in reducer) do not bubble back into saga.
Call: Creates an Effect description that commands middleware to Call fn with args.
Apply: similar to Call.
Fork: Creates an Effect description that commands middleware to execute FN as a non-blocking call.
Spawn: Similar to fork, but creates separated tasks. The detached task remains independent from its parent task.
Join: Creates an Effect description that commands middleware to wait for the result of a previous fork task.
Cancel: Creates an Effect to Cancel the task.
Select: Creates an Effect that commands middleware to call a specified selector on the current Store state (return selector(getState()… Args).
ActionChannel: Creates an Effect that commands middleware to sort actions that match pattern through an event channel.
Flush: Creates an Effect that commands middleware to Flush all cached data from a channel. Flushed data is returned to Saga so it can be used again when needed.
Cancelled: Creates an Effect that commands middleware to return whether the generator has been Cancelled.
SetContext: Creates an effect that commands middleware to update its own context.
GetContext: Creates an effect that commands middleware to return a specific property from saga’s context.
Effect combiner
Race: Creates an Effect description that commands middleware to run races between effects (and promise.race ([…]). ).
All: Creates an Effect description that commands middleware to run multiple effects in parallel and wait for them All to complete. This is the equivalent of the standard Promise# All API.
Saga auxiliary function
TakeEvery: Derive a saga on every action that initiates (dispatchto Store) and matches the pattern.
TakeLatest: Derive a saga on every action that is initiated to the Store and matches the pattern. Automatically cancels all saga tasks that have been started but are still running.
TakeLeading: Derive a saga on every action initiated to the Store that matches the pattern. It blocks after deriving a task until the derived saga completes, and then starts listening again for the specified pattern.
Throttle: Derive a saga on an action that initiates to the Store and matches the pattern. After deriving a task, it still receives the incoming action into the underlying buffer, keeping at most one (recent). But at the same time, it pauses to derive new tasks in ms milliseconds — hence its name, Throttle. Its purpose is to ignore incoming actions for a given period of time while processing a task.
5. Story – Saga test
Because Redux-Saga breaks down each side effect into a smaller dimension, there is less coupling between services. This makes it very useful for unit testing, as follows:
function* callApi(url) {
const someValue = yield select(somethingFromState)
try {
const result = yield call(myApi, url, someValue)
yield put(success(result.json()));
return result.status;
} catch (e) {
yield put(error(e));
return -1; }}Copy the code
const dispatched = [];
const saga = runSaga({
dispatch: (action) = > dispatched.push(action),
getState: () = > ({ value: 'test' }),
}, callApi, 'http://url');
Copy the code
import sinon from 'sinon';
import * as api from './api';
test('callApi'.async (assert) => {
const dispatched = [];
sinon.stub(api, 'myApi').callsFake(() = > ({
json: () = > ({
some: 'value'})}));const url = 'http://url';
const result = await runSaga({
dispatch: (action) = > dispatched.push(action),
getState: () = > ({ state: 'test' }),
}, callApi, url).done;
assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' })));
assert.deepEqual(dispatched, [success({ some: 'value' })]);
});
Copy the code
Finally, I recommend two tips that I think are better after reading the official documents.
6. Redux-saga tips
1. Ajax retry
import { call, put, take, delay, delay } from 'redux-saga/effects'
function* updateApi(data) {
while (true) {
try {
const apiResponse = yield call (apiRequest, { data })
return apiResponse;
} catch(error) {
yield put({
type: 'UPDATE_RETRY',
error
})
yield delay(2000)}}}function* updateResource({ data }) {
const apiResponse = yield call(updateApi, data);
yield put({
type: 'UPDATE_SUCCESS'.payload: apiResponse.body,
});
}
export function* watchUpdateResource() {
yield takeLatest('UPDATE_START', updateResource);
}
Copy the code
2. Cancel
import { take, put, call, spawn, race, delay } from 'redux-saga/effects'
import { updateThreadApi, actions } from 'somewhere'
function* onArchive(action) {
const { threadId } = action
const undoId =`UNDO_ARCHIVE_${threadId}`
const thread = { id: threadId, archived: true}
yield put(actions.showUndo(undoId))
yield put(actions.updateThread(thread))
const { undo, archive } = yield race({
undo: take(action= > action.type === 'UNDO' && action.undoId === undoId),
archive: delay(5000)})yield put(actions.hideUndo(undoId))
if (undo) {
yield put(actions.updateThread({ id: threadId, archived: false}}))else if (archive) {
yield call(updateThreadApi,thread)
}
}
function* main() {
while (true) {
const action = yield take(`ARCHIVE_THREAD`)
yield spawn(onArchive, action)
}
}
Copy the code
Reference article:
1. The story – Saga to ramble
2.Saga Pattern
3. Redux-saga official documentation
4.Why saga
5. Handwritten source code for Redux-Saga