Redux-saga
About
- Manages side-effects
- Depends on ES6 and Yield
- Consumes and emits actions
- Works without redux
Generator functions
The functions does not execute. It waits until .next() is called and progresses through the function breaking after each yield
var delayGenerator = function* () {
let data1 = yield delay(1000,1);
console.info("Step 1");
let data2 = yield delay(1000,2);
console.info("Step 2");
let data3 = yield delay(1000,3);
console.info("Step 3");
}
var obj = delayGenerator()
obj.next()
// Console Step 1
obj.next()
// Console Step 2
obj.next()
// Console Step 3
Using co
co runs the generator and the then holds the result.
var wrapped = co.wrap(delayGenerator);
wrapped().then( v=>console.log("Got a value:", v));
// Console Step 1
// Console Step 2
// Console Step 3
// Got a value: 6
Effects
Take
Take waits for an event to occur in the generator
var mySaga = function* () {
console.info("Start");
const state = yield effect.take("SET_STATE");
console.info("Got State", state);
}
So dispatch can make it conclude
dispatch({state:"SET_STATE"});
Put
Put puts an action in the store
var putSaga = function* () {
yield effect.put({type:"SET_STATE", value:42});
}
So
run(putSaga)
Will result in mySaga completing
fork
fork forks a process, i.e. it is the child of the original thread. e.g.
export function* loadItemDetails(item) {
console.info("item?",item);
const { id } = item;
const response = yield fetch(`http://localhost:8081/items/${id}`)
const data = yield response.json();
const info = data[0];
yield put(setItemDetails(info));
}
export function* itemDetailsSaga() {
const { items } = yield take("SET_CART_ITEMS");
yield all (items.map( item=>fork(loadItemDetails,item)));
}
Each item is updated on a new child thread. Fork means than the effect cancel can be used to stop the thread.
spawn
spawn is like fork but also like the unix function spawn. It starts a new unrelated thread to perform its work can cannot be cancelled.
TakeEvery
TakeEvery waits on an event and forks a new process.
let process = function* () {
let timesLooped = 0;
while(true) {
console.info(`Looped ${timesLooped++} times`);
yield delay(500);
}
}
let saga = function*() {
yield effects.takeLatest("START_PROCESS", process);
}
run(saga)
dispatch( {type:"START PROCESS"});
// Looped 1 times
// Looped 2 times
// Looped 3 times
dispatch( {type:"START PROCESS"});
// Looped 1 times
// Looped 2 times
TakeLatest
TakeLatest waits on an event and forks until a new event. The current is cancelled and a new fork is started.
let process = function* () {
let timesLooped = 0;
while(true) {
console.info(`Looped ${timesLooped++} times`);
yield delay(500);
}
}
let saga = function*() {
yield effects.takeLatest("START_PROCESS", process);
}
run(saga)
dispatch( {type:"START PROCESS"});
// Looped 1 times
// Looped 2 times
// Looped 3 times
dispatch( {type:"START PROCESS"});
// Looped 1 times
// Looped 2 times
All
This allows you to wait on a group of things e.g.
export function* itemPriceSaga() {
const [{user},{items}] = yield all([
take(SET_CURRENT_USER),
take(SET_CART_ITEMS)
]);
yield all (items.map(item=>call(fetchItemPrice, item.id, user.country)));
}
Channels
Action Channel
Buffer actions to be processed one at a time
function* updateSaga() {
let chan = yield actionChannel("UPDATE");
while(true)
{
yield effects.take(chan);
conole.info("Update logged.");
yield delay(1000);
}
}
With an action channel no events are lost. Whereas if the take was yield.take("UPDATE"), only events outside of the delay would be processed.
General Channel
Communicate between to sagas
function* saga() {
let chan = yield channel();
function* handleRequest(chan) {
while(true)
{
let payload = yield effects.take(chan);
console.info("Got payload.");
yield delay(1000);
}
}
yield effects.fork(handleRequest, chan);
yield effects.fork(handleRequest, chan);
yield effects.put(chan, {payload:42} );
yield effects.put(chan, {payload:42} );
yield effects.put(chan, {payload:42} );
}
The first two request as logged and then the third on looping around.
Event Channel
Connects app to outside event sources e.g. web sockets
Test Sagas
Example of test the currentUserSaga
export function* currentUserSaga() {
const { id } = yield take(GET_CURRENT_USER_INFO);
const response = yield call(fetch, `http://localhost:8081/user/${id}`);
const data = yield apply(response,response.json);
// yield put(setCurrentUser(data));
yield put({type: SET_CURRENT_USER, user: data });
}
describe("The current user saga", (()=> {
test("It fetches and puts the current users data", ()=> {
const id = "NCC1701"
const user = "Jean-Luc"
const json = ()=>{}
const response = { json}
const gen = currentUserSaga()
expect(gen.next().value).toEqual(take(GET_CURRENT_USER_INFO));
expect(gen.next({id}).value).toEqual(call(fetch,`http://localhost:8081/user/${id}`));
expect(gen.next(response).value).toEqual(apply(response,json));
expect(gen.next(user).value).toEqual(put(setCurrentUser(user)));
})
})