Redux nowadays : From actions creators to sagas

There's this expression I say each time I stumble on something (some technology) that's going to change my habits in development .... Oh, ça déchire sa mère :P a not so conventional french alternative to wow that's great !!!.

And you know what, It just happened again. I discovered redux-saga, And I'm like: "Wow this is great, that's exactly what I was looking for"

If you don't know much about Redux, I encourage you read some about it here or here before going further.

The missing part to frontend applications ?

A modern frontend application architecture using Redux can be described like so:

1- A store holding an immutable application's state.
2- The state is rendered to components (html or anything else). It's a simple and testable (not always :P) pure function.

const render = (state) => components  

3- Components can dispatch actions to the store.
4- The state is updated (actually, a new state is generated) using reducers which are very simple pure functions.

const reducer = (oldState, action) => newState  

5- Go back to 1.

While this looks pretty straightforward and easy to reason about, things get more complicated when we need to handle async actions (often called side effects in functional programming).

To address this problem, Redux suggests using middlewares (especially thunk middleware). Basically the idea is, if you need to trigger side effects, use an action creator: a function that returns a function that can do any async call needed and dispatch whatever action you want.

Using this approach can quickly lead to complex and very difficult to test actions creators. That's where redux-saga comes in. It defines the concept of a Saga, a declarative and well organized way to express the side-effects (Timeouts, API calls ...). So instead of writing an action creator using thunk middleware, we continue dispatching synchronous actions, but instead of having a reducer handling those actions, we will have a Saga taking this action and yielding effects (simple javascript objets defining the async actions to perform).

Isn't this too complex ? Action creators seems simpler no ?

No it's not, even if it seems to.

Let's see how to write an action creator that fetches some data from a server and dispatches actions to the redux store.

function loadTodos() {  
    return dispatch => {
        dispatch({ type: 'FETCHING_TODOS' });
        fetch('/todos')
            .then(res => res.json())
            .then(todos => {
            dispatch({ type: 'FETCHED_TODOS', payload: todos });
        });
    }
}

This is one of the simplest thunk action creator we could write with redux, and as you can see, the only way to test this code, is by using some kind of mock to the fetch method.

Let's use Sagas instead of action creators to load todos:

import { call, put } from 'redux-saga';  
const fetchTodos = () =>  
    fetch('/todos').then(res => res.json())
function* loadTodos() {  
    yield put({ type: 'FETCHING_TODOS' });
    const todos = yield call(fetchTodos);
    yield put({ type: 'FETCHED_TODOS', payload: todos });
}

As you can see a saga is a generator that yield effects (I like to call it a pure generator, because it doesn't actually executes the side effect but just yields a description of effects to be executed). In the example above, I used two kind of effects:

  • a put effect is an effect that will dispatch an action to the Redux Store.
  • a call effect which is an effect that will run an async function (promise, cps or another saga).

Now testing this saga is quite straightforward:

import { call, put } from 'redux-saga';  
const mySaga = loadTodos();  
const myTodos = [{ message: 'text', done: false }];  
mySaga.next();  
expect(mySaga.next().value).toEqual(put({ type: 'FETCHING_TODOS' }));  
expect(mySaga.next().value).toEqual(call(fetchTodos));  
expect(mySaga.next(myTodos).value).toEqual(put({ type: 'FETCHED_TODOS', payload: myTodos }));  

Trigger a saga

Triggering an action creator happens when dispatching the function returned by the thunk action creator. Sagas are different, "they are like daemon tasks that run in the background and choose their own logic of progression" (cf Yassine Elouafi creator of redux-saga).

So let's see now how we can bind our saga to the Redux application workflow.

import { createStore, applyMiddleware } from 'redux';  
import sagaMiddleware from 'redux-saga';

const createStoreWithSaga = applyMiddleware(  
  sagaMiddleware([loadTodos])
)(createStore);

let store = createStoreWithSaga(reducer, initialState);  

Combining sagas

A saga is an effect itself, so like redux reducers, combining and composing sagas is quite easy (btw a good understanding of ES6 generators is recommended).

In the previous exemple, the loadTodos saga was triggered at startup but what if we would like to trigger this saga each time a special action is dispatched to the store. In this case, our code would look like:

import { fork, take } from 'redux-saga';

// The same loadTodos saga declared above
function* loadTodos() {  
    yield put({ type: 'FETCHING_TODOS' });
    const todos = yield call(fetchTodos);
    yield put({ type: 'FETCHED_TODOS', payload: todos });
}

function* watchTodos() {  
     while (yield take('FETCH_TODOS')) {
         yield fork(loadTodos);
     }
}

// We need to update our root saga
const createStoreWithSaga = applyMiddleware(  
  sagaMiddleware([watchTodos])
)(createStore);

Here I used two special effects provided by redux-saga:

  • a take effect which waits for a redux action to be dispatched
  • a fork effect which triggers another effect but doesn't wait for the end of the sub effect to continue.

Conclusion

So the updated modern frontend application architecture using Redux and Redux Saga workflow is:

1- A store holding an immutable applications state.
2- The state is rendered to components (html or anything else). It's also a simple and testable pure function.

const render = (state) => components  

3- Components can trigger actions to the store.
4- A new state is generated using reducers which are very simple pure functions.

const reducer = (oldState, action) => newState  

5- May be some effect is yielded and executed in response to an action or to another effect.

function* saga() {  
   yield effect;
}

6- Go back to 1.

Let me know what you think about this. Personally I'm really excited about this library and I really think that it is the missing part that Redux needed to go from good to great.

comments powered by Disqus