chowderhead
Posted on February 23, 2020
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.
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 !
the
static
method on the base class will return anfetch
request to my/API.js
fileIn
/API.js
I will handle the response from the API usingasync/await
once i have a nice clean JSON response, i will send it back to the saga
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 February 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.