Redux Saga

(Psssssssst! You can find all the code referenced in this post at https://github.com/NerdcoreSteve/ReduxSaga, just look at front-end/js/main.js!)

Hey all, sorry I've been gone from this blog for so long, but let's pick up from where I left off.

I've written about Redux as a way to handle state and generators as a way to handle async code; redux-saga is a library that combines the two very nicely.

The idea is that you can set up your app to send off an action to redux for some asynchronous thing (usually an API call but it could be anything that doesn't complete right away) which is then intercepted by the redux-saga middleware.

redux-saga then does the asynchronous thing, and when it's finished sends another action to your redux data store when the async thing is completed.

Got it? You send a request as an action in some part of your UI, just like you would with any other action, but it gets hijacked, and the response to the request is what your reducer sees.

Ok, let's get to some code. Here's a bit of Reacty UI:

<div>  
    <p>{store.getState()}</p>
    <input
        type="text"
        onChange = {({target:{value: text}}) => 
            store.dispatch({type: 'ADD_TEXT', text})}
        value = {store.getState()}
        onKeyPress=
            {e => {
                if (e.key === 'Enter') 
                    store.dispatch({
                        type: 'SEND_MESSAGE'
                    }) 
            }}/>
    <button
        onClick={() => store.dispatch({
            type: 'GET_POKEMON'
        })}
        type="button">
            Get Pokemon
    </button>
</div>  

Which renders to this:
An input box and a button that is labelled

The redux model is just a string. When you type into the input box the ADD_TEXT action is sent, updating the string, in turn populating both the text field and the paragraph element.

            case 'ADD_TEXT':
                return action.text

If I hit the enter key in the input box I trigger the SEND_MESSAGE action. This action is not actually in the redux reducer. It's in this rootSaga generator function:

    rootSaga = function* () {
        yield takeEvery('SEND_MESSAGE', messageSaga)
        yield takeEvery('GET_POKEMON', pokemonSaga)
    }

takeEvery is a function from the redux-saga library. It's the function that hijacks actions like we were talking about earlier. It hijacks both SEND_MESSAGE and GET_POKEMON to use the messageSaga and the pokemonSaga generator functions respectively.

Here's messageSaga:

    messageSaga = function* () {
        const
            response = yield call(
                fetch,
                '/message',
                {
                    method: 'post',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type':
                            'application/json'
                    },
                    body: JSON.stringify({
                        data: {
                            message: store.getState()
                        }
                    })
                }),
            json = yield call([
                response,
                response.json])

        yield put({
            ...json,
            type: 'MESSAGE_RECIEVED',
        })
    }

This should make sense if you're familiar with generators, fetch and promises.

redux-saga uses the yielded promise returned by the fetch call to return the async value this promise eventually contains. We then do the same thing to get the json data from the second promise.

We use the redux-saga functions put and call because that's the redux-saga way to do things. Also because (I'm given to understand) it makes testing easier. More on testing redux-saga in a later post.

The MESSAGE_RECIEVED action is caught by the regular old redux reducer:

            case 'MESSAGE_RECIEVED':
                return action.response

It replaces the model with the new string received by the ajax response.

Here's what happens if I type "fluffy bunny" into the text field and hit enter:
both the paragraph element and the input box say 'message was

Here's the back-end code for POST /message btw:

app.post(  
    '/message',
    (req, res) => res.json({
        response:
            `message was "${req.body.data.message}"`
    }))

pokemonSaga works very similarly:

    pokemonSaga = function* () {
        const
            response = yield call(
                fetch,
                '/pokemon',
                {
                    method: 'get',
                    headers: {
                        'Accept': 'application/json',
                        'Content-Type':
                            'application/json'
                    },
                }),
            json = yield call([
                response,
                response.json])

        yield put({
            ...json,
            type: 'GOT_POKEMON',
        })
    }

We make the ajax call, get the json data, and then send it to the data store.

/pokemon just picks a pokemon randomly from an array and sends it back to you.

And that's redux-saga. There's more of course, but this is definitely enough to get you started.

Happy coding. :)

Looking for a software developer?