DEV Community

Norbert Braun
Norbert Braun

Posted on

Async React Basics with Redux-thunk & Redux-saga

I have lots of free time lately so I decided to play around a bit with React & Redux. If you want to write maintainable asynchronous code using Redux, you need to pick a middleware like redux-thunk or redux-saga.

What we're building

I love cats, so the functionality of the application is based on the Cat API. You can clone/fork the GitHub repo from here.

The application looks something like this:
catapi app

If you click the "Fetch cats" button, it sends an HTTP GET request which returns a random cat image. If you click on "Fetch more cats" it returns an array of 5 random cats.
I know it's ugly and stuff but I don't really want to waste time with css. If you are interested in the full "project" and the css files as well, check out the github repo that I have already mentioned above.

The fetchCats function will be implemented using redux-thunk and fetchMoreCats will be written using redux-saga so that we can compare them.

Getting started

create-react-app catapi_app
Enter fullscreen mode Exit fullscreen mode

Let's install some dependencies first.

npm i --save react-redux redux redux-logger redux-saga redux-thunk
Enter fullscreen mode Exit fullscreen mode

Next, we need to setup redux in index.js.

//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers/index'

const loggerMiddleware = createLogger()

const store = createStore(
    rootReducer,
    applyMiddleware(
        thunkMiddleware,
        loggerMiddleware ))

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>, 
    document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

This code will fail, because we do not have our rootReducer. So let's continue with that.

// ./reducers/index.js

import { combineReducers } from 'redux'
import fetchCatReducer from './fetchCatReducer'

export default combineReducers({
    cats: fetchCatReducer
})
Enter fullscreen mode Exit fullscreen mode

We have only one reducer so far, but I like to use combineReducer because if I need to add another one, it's much easier.

This code will still fail because now, we are missing the fetchCatReducer.

// ./reducers/fetchCatReducer.js

const fetchCatReducer = (state = [], action) => {
    switch(action.type) {
        case "FETCH_CATS_SUCCESS":
            return [
                ...action.payload,
                ...state
            ]
        case "FETCH_CATS_START":
            return state
        case "FETCH_CATS_ERROR":
            return state
        default:
        return state
    }
}

export default fetchCatReducer
Enter fullscreen mode Exit fullscreen mode

Whenever we dispatch an action, that action goes through fetchCatReducer and it updates our state accordingly.

  • "FETCH_CATS_SUCCESS": HTTP request was successful, we must update the state.
  • "FETCH_CATS_START": HTTP request has been started, this is the right place to for example display a busy indicator to the user. (Loading screen or something)
  • "FETCH_CATS_ERROR": HTTP request has failed. You can show an error component or something.

To keep the app simple, in case of "FETCH_CATS_START" or "FETCH_CATS_ERROR" I do nothing but returning the previous state.

Redux-thunk

Currently, our app does nothing, because we need an action creator, to fire an action that our reducer handles.

//./actions/fetchCats.js

/*Helper functions. remember, we have 3 action types so far,
 these functions return a plain object that has a 
type attribute that our reducer can handle.
in case of success request, 
the action has a payload property as well. 
That's the response cat from the server 
that we have requested*/

const fetchCatsError = () =>{
    return {type: "FETCH_CATS_ERROR"}
}

const fetchCatsStarted = () =>{
    return {type: "FETCH_CATS_START"}
}

const fetchCatsSuccess = (cat) => {
    return {type: "FETCH_CATS_SUCCESS", payload: cat}
}

// fetching a random cat starts now
const fetchCats = () => dispatch => {
    dispatch(fetchCatsStarted())

    fetch("https://api.thecatapi.com/v1/images/search",{
        headers: {
            "Content-Type": "application/json",
            "x-api-key": "YOUR_API_KEY"
        }
    })
    .then( catResponse => catResponse.json()) 
    .then( cat => dispatch(fetchCatsSuccess(cat)) )
    .catch( err => dispatch(fetchCatsError()))
}
Enter fullscreen mode Exit fullscreen mode

Ye, in order to use this endpoint on CAT API, you need an api key.
fetchCats might look strange at first, its basically a function that returns another function that has a parameter dispatch. Once you call dispatch, the control flow will jump to your reducer to decide what to do. In our case, we only update our application state, if the request has been successful. Btw, that's why I have installed redux-logger. It constantly logs the changes of your state, and actions so its much easier to follow what's happening.

If you prefer the Async/await syntax, then you can implement the above function like this:

const fetchCats =  () => async dispatch => {
    dispatch(fetchCatsStarted())
    try{
        const catResponse = await fetch("https://api.thecatapi.com/v1/images/search",{
            headers: {
                "Content-Type": "application/json",
                "x-api-key": "YOUR_API_KEY"
            }
        })

        const cat = await catResponse.json()
        dispatch(fetchCatsSuccess(cat))
    }catch(exc){
        dispatch(fetchCatsError())
    }
}
Enter fullscreen mode Exit fullscreen mode

App component

I don't want this post to be too long, so I skip the implementations of the components. I'll show you how the App.js looks like, if you are interested in the complete code, check it out on GitHub.


//./components/App.js

import React, { Component } from 'react'
import Button from './proxy/Button'
import CatList from './CatList'
import '../css/App.css'
import { connect } from 'react-redux'
import fetchCats from '../actions/fetchCats'

class App extends Component {
    render() {
        return (
            <div className="App">
                <Button className="primary" text="Fetch cats" onClick={this.props.fetchCats}/>
                <Button className="secondary" text="Fetch more cats"/>
                <header className="App-header">
                    <CatList cats={this.props.cats}/>
                </header>
            </div>
        )
    }
}

const mapStateToProps = (state, ownProps) => ({
        cats: state.cats
})

export default connect(mapStateToProps, { fetchCats })(App);
Enter fullscreen mode Exit fullscreen mode

Redux-saga

Redux-saga is a redux middleware that allows us to easily implement asynchronous code with redux.

To initialize it, we need to adjust our index.js a bit.

//./index.js
...
import createSagaMiddleware from 'redux-saga'
import watchFetchMoreCatsSaga from './saga/fetchMoreCats'

//init
const sagaMiddleware = createSagaMiddleware()

//run
sagaMiddleware.run(watchFetchMoreCatsSaga)
...
Enter fullscreen mode Exit fullscreen mode

In the saga folder, create a new file called fetchMoreCats.

//./saga/fetchMoreCats

import { takeLatest, put } from "redux-saga/effects";

//Every time we dispatch an action 
//that has a type property "FETCH_MORE_CATS"
// call the fetchMoreCatsSaga function
export default function* watchFetchMoreCatsSaga(){
    yield takeLatest("FETCH_MORE_CATS", fetchMoreCatsSaga)
}

//query 5 cat image at the same time
function* fetchMoreCatsSaga(){
    yield put({type: "FETCH_MORE_CATS_SAGA_START"})

   const catResponse = yield fetch("https://api.thecatapi.com/v1/images/search?limit=5",{
        headers: {
            "Content-Type": "application/json",
            "x-api-key": "YOUR_API_KEY"
        }
    })

    const cats = yield catResponse.json()

    yield put({type: "FETCH_MORE_CATS_SAGA_SUCCESS", payload: cats})
}
Enter fullscreen mode Exit fullscreen mode

Those function* thingies are called generator functions. If you want to know more about them, click here.

The takeLatest function can be replaced by takeEvery for example, but one cool feature of takelatest is that it only takes the last "event". In our case, if we rapidly click the button like 100 times, then our app sends 100 requests pretty much DDOSing the API :D. So instead of disabling the button every time it's getting clicked, we can use takeLatest.

As you can see, by calling the put function we can fire actions just like we did with dispatch. So let's adjust our ./reducers/fetchCatReducer.js to handle our new saga actions.

//./reducers/fetchCatReducer.js

...
case "FETCH_MORE_CATS_SAGA_SUCCESS":
            return [
                ...action.payload,
                ...state
            ]
        case "FETCH_MORE_CATS_SAGA_START":
            return state
        case "FETCH_MORE_CATS_SAGA_ERROR":
            return state
...
Enter fullscreen mode Exit fullscreen mode

The watchFetchMoreCatsSaga generator function is constantly listening to the "FETCH_MORE_CATS" action and calls our fetchMoreCatsSaga. So in order to make this work, we need to first fire that action.

//./actions/fetchMoreCats.js

const fetchMoreCats = () => dispatch =>{
    dispatch({type: "FETCH_MORE_CATS"})
}

export default fetchMoreCats
Enter fullscreen mode Exit fullscreen mode

That's it. Every time we call fetchMoreCats, it dispatches {type: "FETCH_MORE_CATS"} which "invokes" our watchFetchMoreCatsSaga that calls fetchMoreCatsSaga.

So we need to import fetchMoreCats in our App.js and call it when the user clicks that button.

//App.js

...
import fetchMoreCats from '../actions/fetchMoreCats'

//put this button in the render method
<Button className="secondary" text="Fetch more cats" onClick={this.props.fetchMoreCats}/>

//we need to map that function to the props of the App


export default connect(mapStateToProps, { fetchCats, fetchMoreCats })(App);
Enter fullscreen mode Exit fullscreen mode

The end

If you want to know more: Saga documentation

If you have any questions, please let me know in the comment section or feel free to email me.

Top comments (11)

Collapse
 
lexlohr profile image
Alex Lohr

Thanks for the nice article. Since you're already familiarized yourself with thunks and sagas, you should probably learn about epics in redux-observables. I find them a cleaner abstraction of side effects in redux than the other options. The only problem is that they are so powerful, you'll soon end up using epics for everything.

Collapse
 
benjamindaniel profile image
Benjamin Daniel

Everyone keeps recommending redux-observables, will definitely check it out.

Collapse
 
bnorbertjs profile image
Norbert Braun

Hi Alex, thanks for the hint I'll definitely check it out!

Collapse
 
adenforshaw profile image
Aden Forshaw

Awesome Article, great to see the Cat API being used to teach, thanks Norbert!

Collapse
 
bnorbertjs profile image
Norbert Braun

Thanks, Aden great api by the way :)

Collapse
 
jordanchristie profile image
Jordan Christie

Awesome stuff. When deciding between the two I chose Redux-Thunk just because it clicked faster. I've been curious about Saga lately, what's the advantage?

Collapse
 
bnorbertjs profile image
Norbert Braun

Hi Jordan, thank you!
I think the main advantage saga has over thunk is the testability. Generator functions return iterators. So you can pretty easily see, what's going on in your function by calling .next().

Here's an example to test sagas: redux-saga.js.org/docs/basics/Disp...

OR

redux-saga.js.org/docs/advanced/Te...

Collapse
 
sagar profile image
Sagar

In depth article on redux-thunk and redux-saga. Thanks for writing wonderful article.

Collapse
 
bnorbertjs profile image
Norbert Braun

Thanks, glad you like it. :)

Collapse
 
hdlinhit95 profile image
Lin

Wonderful article!
Thanks u

Collapse
 
chadsteele profile image
Chad Steele

I'd love it if you wanted to dissect my Redux one line replacement hook... useSync
dev.to/chadsteele/redux-one-liner-...