Redux-saga

From bibbleWiki
Jump to navigation Jump to search

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 });
}

And the test code

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)));

    })
})


And here is another example with the if statements tested

export function* handleIncreaseItemQuantity({id}) {
    yield put(setItemQuantityFetchStatus(FETCHING));
    const user = yield select(currentUserSelector);
    const response = yield call(fetch,`http://localhost:8081/cart/add/${user.get('id')}/${id}`);

    if (response.status !== 200) {
        yield put(decreaseItemQuantity(id, true));
        alert("Sorry, there weren't enough items in stock to complete your request.");
    }
    yield put(setItemQuantityFetchStatus(FETCHED));
}

And the test code

describe.only("the item quantity saga",()=>{
    let item;
    let user;
    beforeEach(()=>{
        item = {id:12345};
        user = fromJS({id:"ABCDE"});
    });
    describe("the saga root",()=>{
        test("the saga root should listen for the events",()=>{
            const gen = itemQuantitySaga();
            expect(gen.next().value).toEqual([
                takeLatest(DECREASE_ITEM_QUANTITY, handleDecreaseItemQuantity),
                takeLatest(INCREASE_ITEM_QUANTITY, handleIncreaseItemQuantity)
            ]);
        });
    });

    describe("handle decrease item quantity saga",()=>{
        test("decreasing the quantity of an item successfully",()=>{
            const gen = handleDecreaseItemQuantity(item);
            expect(gen.next().value).toEqual(put(setItemQuantityFetchStatus(FETCHING)));
            expect(gen.next().value).toEqual(select(currentUserSelector));
            expect(gen.next(user).value).toEqual(call(fetch,`http://localhost:8081/cart/remove/ABCDE/12345`));
            expect(gen.next({status:200}).value).toEqual(put(setItemQuantityFetchStatus(FETCHED)));
        });
    });

    describe("handle increase item quantity saga",()=>{
        let gen;
        beforeEach(()=>{
            gen = handleIncreaseItemQuantity(item);
            expect(gen.next().value).toEqual(put(setItemQuantityFetchStatus(FETCHING)));
            expect(gen.next().value).toEqual(select(currentUserSelector));
            expect(gen.next(user).value).toEqual(call(fetch,`http://localhost:8081/cart/add/ABCDE/12345`));
        });
        test("increasing the quantity of an item successfully",()=>{
            expect(gen.next({status:200}).value).toEqual(put(setItemQuantityFetchStatus(FETCHED)));
        });

        test("increasing the quantity of an item unsuccessfully",()=>{
            expect(gen.next({status:500}).value).toEqual(put(decreaseItemQuantity(item.id, true)));
            expect(gen.next().value).toEqual(put(setItemQuantityFetchStatus(FETCHED)));
        });
    });
});