loading...
Cover image for React Async/Await API layer, with REDUX Sagas

React Async/Await API layer, with REDUX Sagas

nodefiend profile image chowderhead ・4 min read

Photo Cred : Naomi Koelemans

Assumptions

  • you know how to redux
  • you are familiar with async await
  • you know a little bit of sagas

I spent at least 4 hours on a saturday night trying to get this to work.

my understanding of async await was shakey at best and i was brand new to sagas - maybe this can help clear the air for you a bit, and give you a practical explanation along with some code examples.

Alt Text

Context

So i have this component here. when a user doesn't enter any information, it is informed by an alertsReducer.js with errors from the backend.

alertsReducer.js


import { Users } from '../constants'
const initialState = {}

export function alertsReducer(state = initialState, action) {

  switch (action.type) {
    case Users.handleUserSignupError:
      return {
        ...state,
        data: action.payload.errors
      }
    default:
      return { data: initialState }
  }
}

As you can see it needs an errors object attached to the payload in order to function correctly.

Cool, now we know what we need to expect - lets go back to the beginning of the request:

In the component it self, i have a simple redux action that gets fired when the user presses submit.

...

handleSubmit = (data) => {
  this.props.handleSubmit(data)
}

render() {
  <div>
    <button onClick={handleSubmit}>submit</button>
  </div>
}

...

totally a watered down example, but you get the idea, it is calling a redux action, and then in the same component, we are reading the state of the alerts reducer, and when we receive the errors, we funnel it back into the component.

Alright - heres where it gets hairy, trying to figure out sagas, while at the same time abstracting out an API layer to make everything nice and neat was my end goal.

establishing an architecture pattern for my redux front end.

/actions.js

export const sendSignupDetails = (data) => {
  return {
    type: Users.sendSignupDetails,
    payload: data
  }
}

export const signupSuccess = (data) => {
  return {
    type: Users.handleUserSignupSuccess,
    payload: data
  };
}

export const signupError = (errors) => {
  return {
    type: Users.handleUserSignupError,
    error: true,
    payload: errors
  };
}

you can see here, when this action fires, it sends the constant action type, and also passes the data into payload!

Okay great , so far so good...

Enter Sagas

I'm not gonna get into all the hairy details that took at least 4 hours of figuring out, ill just do my best to explain my thinking for how i chose this as the best pattern for my sagas setup...

/sagas.js


function* sendSignupDetails(action) {
  yield takeLatest(Users.sendSignupDetails, postUserSignup)
}

function* postUserSignup(action) {
  const response = yield call(usersApi.signupUser, action.payload);
  if (response.errors) {
    yield put(signupError(response));
  } else {
    yield put(signupSuccess(response))
  }

}

function* handleUserSignupSuccess(action){
  yield takeLatest(Users.handleUserSignupSuccess, redirectToDashboard)
}

of course there is some more setup involved, but I wanted to focus mainly on the sagas themselves....

as you can see sendSignupDetails is a watcher, that waits for the correct action to be dispatched, in this case it's the one we set up earlier which is : Users.sendSignupDetails

using takeLatest it will watch for the latest call of the action. you can google more info on takeLatest all over the interwebs.

the first argument of takeLatest is the action itself, and the second, is what you want it to do when it sees this action.

here i am calling function* postUserSignup , this does a few things.

yield is calling an API request for us, and passing our payload into the Api request. we set this equal to a response so that we can grab the insides out and dispatch them off into either a success or error , based on the contents of the response.

notice if(response.errors) there are probaly better ways to do this, but if our server returns an errors key, which i have it doing , then we will dispatch the signupError action we set up earlier in the actions file.

if you can tell me how to get this work with try catch please leave comments below, otherwise, do this, cause it has served me well thus far.

API File

since im a big fan of abstracting things out and making them nice and neat, i have an /API file that i import into my sagas file, its basically a class that looks like this:

/API.js

// notice how i am extending a base class here
import Api from './base-api';

export class UsersAPI extends Api {
  signupUser = async (data) => {
    const { user } = data;
    let res = await Api.postRequest('/users', {user})
    let response = await res.json()
    return response
  }
}

great, so to make this as neat as possible , I am using async on the function definition, and then to handle the responses from the actual API requests , I am using await, and then using await again to render json out of the initial response.

/base-api.js


const headers = {
  'Accept': 'application/json',
  'Content-Type': 'application/json'
}

export default class Api {
  static postRequest = (url, data) => {

    const options = {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(data)
    }

    return fetch(url,options)
  }
}

I have all of my crud options CREATE,READ,UPDATE,DELETE seprated into neat little methods on my base class to clean it up even more.

THATS IT !

  1. the static method on the base class will return an fetch request to my /API.js file

  2. In /API.js I will handle the response from the API using async/await

  3. once i have a nice clean JSON response, i will send it back to the saga

  4. where it will call the action, and pass the payload into the reducer.

Maybe, just maybe this will save someone some time- it took me along time to figure out a pattern that worked, since everything i typed async/await in google, it would show me articles like :

ASYNC AWAIT VS REDUX SAGAS

hahahhaa, well thats all for now, until we meet again! happy coding!!!

Ken

Posted on by:

nodefiend profile

chowderhead

@nodefiend

code is my paint and the interwebs is my canvas. [https://nodefiend.github.io]

Discussion

markdown guide