Overview

This is a the 3rd part of the React and Redux Sagas Authentication App Tutorial. In this part we're going to work with creating and fetching our protected API resources with our newly setup authentication and authorization process.

Recap

In the previous section we:

Modeled our Login State

Setup our Login Actions and Constants

Stepped through and creating our Login Saga

Learned about the fun loop feature of Sagas

Handled authorization and authentication!

The Previous Tutorials

Part 1 - Setting up the base project and Signing up our Users

Part 2 - Making the Login and Authentication/Authorization flow

Part 3 (This one) - Working with protected resources

Additionally, we also use the API that I cover extensively how to build and setup using Node, Express and Loopback. (In hindsight, I probably should've named this something like Guide to creating a node+express API with loopback.).

All of the code for this portion of the series can be found here:

Github Part 3 Branch

Table of Contents

The goal at this point, now that we've accomplished the rough parts of signup and auth, will be to create and fetch our widgets. Our goal is to create another view in our /widgets route, that includes a <form> to create new Widgets and a list that shows all the ones we have.

As simplistic as this is, we'll still learn some more about JWT authentication, some Redux Form and multiple Saga listeners along the way.

Let's dive in.

As is the pattern in this series - we will begin with the end in mind.. and this means starting with our Redux State and then moving to actions and constants . Essentially, UI aside, how do we change the state of our application? This step also disregards the api to an extent, because it's often easier to model state when we assume the calls will succeed (or fail).

0. Make sure you're in your code/src/ folder from last time and stay here

All paths I mention will be relative to this directory!

1. Open up widgets/reducer.js

Because we've spent so a great deal of time in the past tutorials discussing the thought process, we're going to go full blown "end in mind" and just create the entire reducer and state for the create process here:

import { WIDGET_CREATING, WIDGET_CREATE_SUCCESS, WIDGET_CREATE_ERROR, } from './constants' const initialState = { list: [], // where we'll store widgets requesting: false, successful: false, messages: [], errors: [], } const reducer = function widgetReducer (state = initialState, action) { switch (action.type) { case WIDGET_CREATING: return { ...state, requesting: true, successful: false, messages: [{ body: `Widget: ${action.widget.name} being created...`, time: new Date(), }], errors: [], } // On success include the new widget into our list // We'll render this list later. case WIDGET_CREATE_SUCCESS: return { list: state.list.concat([action.widget]), requesting: false, successful: true, messages: [{ body: `Widget: ${action.widget.name} awesomely created!`, time: new Date(), }], errors: [], } case WIDGET_CREATE_ERROR: return { ...state, requesting: false, successful: false, messages: [], errors: state.errors.concat([{ body: action.error.toString(), time: new Date(), }]), } default: return state } } export default reducer

This should look very familiar to the way we've created the states in for our other views. The only differences here are (a) we've included a list property that we'll use to store our widgets and (b) we'll come back later and deal with fetch ing our widgets.

Just to be clear, this isn't some hack for me to just copy and paste and avoid writing. Instead, this is something slightly more akin to "TDD" in the sense that we're doing a red-green flow. Yes this will throw errors - our constants our not defined. However, we now have a definite next step. Create the constants.

2. Open up widgets/constants.js and input the following:

export const WIDGET_CREATING = 'WIDGET_CREATING' export const WIDGET_CREATE_SUCCESS = 'WIDGET_CREATE_SUCCESS' export const WIDGET_CREATE_ERROR = 'WIDGET_CREATE_ERROR'

Straightforward. Onwards to action !

3. Open up widgets/actions.js and input the following:

import { WIDGET_CREATING, WIDGET_CREATE_SUCCESS, WIDGET_CREATE_ERROR, } from './constants' // Create requires that we pass it our current logged in client AND widget params // which you can view at http://widgetizer.jcolemorrison.com/explorer OR at // localhost:3002/explorer if you're using the local API version. export const widgetCreate = function widgetCreate (client, widget) { return { type: WIDGET_CREATING, client, widget, } } export const widgetCreateSuccess = function widgetCreateSuccess (widget) { return { type: WIDGET_CREATE_SUCCESS, widget, } } export const widgetCreateError = function widgetCreateError (error) { return { type: WIDGET_CREATE_ERROR, error, } }

We're doing things a bit differently here than we did previously. Instead of dispatching the raw action types from our saga we'll instead create accompanying helper functions (aka action creators) to call. Although we can send the object returned from widgetCreate() directly from anywhere, we'll use widgetCreate() for consistency and DRYness. Similarly we'll do the same thing with widgetCreateSuccess() and widgetCreateError() .

But wait. Why do this differently now and not earlier? Previously we we're dealing with either (a) getting up to speed with sagas in general or (b) authentication/authorization in context of new saga ideas. Throwing this on top seemed a bit much.

But wait again. Why?

Because between our action , constant and reducer we now have all we need as if the sagas didn't exist. This lets us truly think about changing our application without worrying about the middlewares.

Of course, the caveat here is that if you dispatch either widgetCreateSuccess() or widgetCreateError() is that nothing will occur unless they're called in context of our saga.

When it comes down to it though the main benefit is for us to think about our app state without worrying about api/async actions and to enforce consistency. It also follows the patterns that most Redux apps do. That's really it.

3.5 Finally, let's open up index-reducer.js and include our newly created reducer:

import { combineReducers } from 'redux' import { reducer as form } from 'redux-form' import client from './client/reducer' import signup from './signup/reducer' import login from './login/reducer' import widgets from './widgets/reducer' // <-- ADD const IndexReducer = combineReducers({ signup, client, login, form, widgets, // <-- ADD }) export default IndexReducer

Thanks to Craig Cannon for reminding me that we should actually include our reducer!

Now let's go spin up the view.

Learn how to deploy production-ready Node and React applications on AWS. Get notified when my next AWS DevOps Workshop opens:

Instead of just reusing the typical Redux Form <Field> , this time we'll actually make use of some of its neat validation tools.

What is <Field> though? Well, like our Widgets component, it's a wrapper that accepts another component and extends the functionality. So far we've only passed it a basic <input> html element, but we can pass it our own custom component as well. When/if we do so, Redux Form will return to us in that component a long list of properties, optional and required ones, that we utilize. The list of all of them can be seen here:

http://redux-form.com/6.5.0/docs/api/Field.md/

In addition to leveraging custom components, we can also pass <Field> a set of validators. For example:

const myValidator = inputValue => (inputValue ? undefined : 'WRONG!') const myComponent = ({ input, meta: {error }}) => ( <div> <input {...input} type="text" /> {error && (<div>{error}</div>)} </div> ) // .. <Field component={myComponent} validate={myValidator} />

In the above example:

a. We tell <Field> that we'd like to use our own component myComponent (please don't name things myPrefix ever :D)

b. <Field> passes to us all of the properties associated with <Field> that Redux Form watches and responds to. It is now our responsibility to deal with those.

List of all the Properties again

c. From the properties it passes us, we take out the entire set of input properties and "spread" them onto our <input>

This includes things like value , onChange , onBlur etc. Even though these share the same names as a typical input, they're special in the sense that they're live updated and watched by Redux Form. When they occur/change, Redux Form will update them automatically in the Redux Store.

d. We pass our <Field> a validate function myValidator .

When <Field> is passed a validate function it will run it whenever the value changes. So in this example, if the value exists, we'll pass undefined and if it does not we'll pass WRONG! . If a value is passing/valid, Redux Form requires that we return undefined .

Now the fun part about validate functions, is that doing so does far more than just validate our fields. When use Redux Form in our component with a <form> , it will give to us a flag called invalid on our this.props . When/if one of our validate functions fails on a field, Redux Form will mark the invalid field as true . This gives us a very easy way to control submission.

4. Open up widgets/index.js

This file is going to get relatively big, so we're going to take it in stride. First off, let's lay the groundwork and scaffolding. Modify widgets/index.js to be the following:

import React, { Component, PropTypes } from 'react' import { reduxForm, Field } from 'redux-form' import { connect } from 'react-redux' import Messages from '../notifications/Messages' import Errors from '../notifications/Errors' import { widgetCreate } from './actions' // Our validation function for `name` field. const nameRequired = value => (value ? undefined : 'Name Required') class Widgets extends Component { // .. } // Pull in both the Client and the Widgets state const mapStateToProps = state => ({ client: state.client, widgets: state.widgets, }) // Make the Client and Widgets available in the props as well // as the widgetCreate() function const connected = connect(mapStateToProps, { widgetCreate })(Widgets) const formed = reduxForm({ form: 'widgets', })(connected) export default formed

This should all be familiar. The only real difference here is that we're pulling in our client piece of state to make use of AND we're defining a nameRequired function for usage in validation.

Next up we're going to create the render() function for our component

5. In our widgets/index.js Widgets class, insert the following render() function:

// .. class Widgets extends Component { render () { // pull in all needed props for the view // `invalid` is a value that Redux Form injects // that states whether or not our form is valid/invalid. // This is only relevant if we are using the concept of // `validators` in our form. const { handleSubmit, invalid, widgets: { list, requesting, successful, messages, errors, }, } = this.props return ( <div className="widgets"> <div className="widget-form"> <form onSubmit={handleSubmit(this.submit)}> <h1>CREATE THE WIDGET</h1> <label htmlFor="name">Name</label> {/* We will use a custom component AND a validator */} <Field name="name" type="text" id="name" className="name" component={this.renderNameInput} validate={nameRequired} /> <label htmlFor="description">Description</label> <Field name="description" type="text" id="description" className="description" component="input" /> <label htmlFor="size">Size</label> <Field name="size" type="number" id="size" className="number" component="input" /> {/* the button will remain disabled until not invalid */} <button disabled={invalid} action="submit" >CREATE</button> </form> <hr /> <div className="widget-messages"> {requesting && <span>Creating widget...</span>} {!requesting && !!errors.length && ( <Errors message="Failure to create Widget due to:" errors={errors} /> )} {!requesting && successful && !!messages.length && ( <Messages messages={messages} /> )} </div> </div> </div> ) } } // ..

The only differences here from our other forms are:

a. We're passing the name field a custom component this.renderNameInput , that we'll create in a sec, and our validator nameRequired ,

b. We're including the invalid property that Redux Form makes available to us. If name fails its validate function, invalid will become true and disable our <button> .

The fields we're concerned with passing up are that of name , description and size . Why these params? Well they're what the API expects/allows on the Widgets resource. Remember you can view the API docs anytime at http://widgetizer.jcolemorrison.com/explorer or if you pulled it down locally at localhost:3002/explorer .

While we're here, open up App.css and add the following anywhere:

button[disabled] { opacity: 0.6; }

It'll just make the button a bit more transparent when disabled to cue the user.

6. In our widgets/index.js Widgets class, insert the following this.renderNameInput() function:

class Widgets extends Component { renderNameInput = ({ input, type, meta: { touched, error } }) => ( <div> {/* Spread RF's input properties onto our input */} <input {...input} type={type} /> {/* If the form has been touched AND is in error, show `error`. `error` is the message returned from our validate function above which in this case is `Name Required`. `touched` is a live updating property that RF passes in. It tracks whether or not a field has been "touched" by a user. This means focused at least once. */} {touched && error && ( <div style={{ color: '#cc7a6f', margin: '-10px 0 15px', fontSize: '0.7rem' }}> {error} </div> ) } </div> ) render () { // ... } }

Redux Form actually hands over a number of "meta" properties that provide useful, live updating, functionality. We're just using touched and error , but we could also check for things like submitFailed or submitting etc. A full list is here:

http://redux-form.com/6.5.0/docs/api/Field.md/#meta-props

7. In our widgets/index.js Widgets class, insert the following this.submit() function:

class Widgets extends Component { // Redux form passes the `values` of our fields as an object // to our submit handler. I'm just calling it `widget` instead // of `values`, since that's basically what it is. It will still // include `name`, `description` and `size` properties/values. submit = (widget) => { const { client, widgetCreate, reset } = this.props // call to our widgetCreate action. widgetCreate(client, widget) // reset the form upon submit. reset() } renderNameInput = ({ input, type, meta: { touched, error } }) => ( // ... ) render () { // ... } }

We pass the values of our form to our widgetCreate() action function.

We also see another Redux Form passed in property here called reset . This is a method that will simply reset the form when we call it and clear the fields.

8. In our widgets/index.js Widgets class, insert our propTypes:

class Widgets extends Component { static propTypes = { handleSubmit: PropTypes.func.isRequired, invalid: PropTypes.bool.isRequired, client: PropTypes.shape({ id: PropTypes.number.isRequired, token: PropTypes.object.isRequired, }), widgets: PropTypes.shape({ list: PropTypes.array, requesting: PropTypes.bool, successful: PropTypes.bool, messages: PropTypes.array, errors: PropTypes.array, }).isRequired, widgetCreate: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, } submit = (widget) => { // ... } renderNameInput = ({ input, type, meta: { touched, error } }) => ( // ... ) render () { // ... } }

For validation and future understanding. A huge, unsung benefit of PropTypes is that, other developers can look at them and get an idea of what our component uses/expects.

Now our create form is ready. If we fail to supply a name, we'll get a nice little required message AND the <button> is disabled. We can also see that Redux Form is keeping track of this state in our dev tools:

Once we fill in a name, the message disappears and the button is now enabled:

Before we dive into showing the widgets, we'll go ahead and deal with creating our widgets over the API. The only thing new here will be passing in our authentication token on create requests. Other than that, it will look very similar to what we've done in other sagas. We just listen for an event, call the API, and dispatch actions when the API is complete.

9. Open up widgets/sagas.js and modify it to be the following:

import { call, put, takeLatest } from 'redux-saga/effects' import { handleApiErrors } from '../lib/api-errors' import { WIDGET_CREATING, } from './constants' import { widgetCreateSuccess, widgetCreateError, } from './actions' const widgetsUrl = `${process.env.REACT_APP_API_URL}/api/Clients` function widgetCreateApi (client, widget) { const url = `${widgetsUrl}/${client.id}/widgets` return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', // passes our token as an "Authorization" header in // every POST request. Authorization: client.token.id || undefined, // will throw an error if no login }, body: JSON.stringify(widget), }) .then(handleApiErrors) .then(response => response.json()) .then(json => json) .catch((error) => { throw error }) } function* widgetCreateFlow (action) { try { const { client, widget } = action const createdWidget = yield call(widgetCreateApi, client, widget) // creates the action with the format of // { // type: WIDGET_CREATE_SUCCESS, // widget, // } // Which we could do inline here, but again, consistency yield put(widgetCreateSuccess(createdWidget)) } catch (error) { // same with error yield put(widgetCreateError(error)) } } function* widgetsWatcher () { // each of the below RECEIVES the action from the .. action yield [ takeLatest(WIDGET_CREATING, widgetCreateFlow), ] } export default widgetsWatcher

Almost everything is the same here except:

the usage of our authentication token the use of action functions vs the straight action in our watcher, yield ing an array, since we'll also watch for the list action later.

10. Finally, let's add our Widget Saga to our index-sagas.js :

import SignupSaga from './signup/sagas' import LoginSaga from './login/sagas' import WidgetSaga from './widgets/sagas' export default function* IndexSaga () { yield [ SignupSaga(), LoginSaga(), WidgetSaga(), ] }

Just include it in and we can create our widgets now!

We can see we send the request being sent up with the payload and the authorization header!

Feel free ot skip if you understand token based authentication/authorization and JWT basics.

So these tokens we're sending up...

They're just JSON Web Tokens. There's a ton of knowledge and resources out there in what they do, but essentially, instead of storing a session server-side, for every single user, we drop the usage of session and instead just see if you have a valid token that permits you to access the desired resource.

Okay mouth full eh. So server side sessions work like this (more or less):

a. User logs in.

b. Server starts a "session" for the user.

This means it keeps in memory, whether in DB, memory, redis, memcache, that this user has logged in and is allowed to do whatever a logged in user can do. The server remembers the user.

c. Server sends back the user some sort of identifier / store that will allow the user to reference their session.

In most cases this is a cookie with basic information. On each request, the cookie with this information is sent up. The server uses the cookie to find the user's current session and that session is used to do stuff with the request.

So more or less, the server remembers who you are, gives you a name tag (cookie). When you come back the server uses your name tag to give you access to the correct resources.

With JWTs:

a. User authenticates

b. Server signs a JSON Web Token with a "secret" and returns it to the user

That's it. The server doesn't remember, doesn't care. Just gives a token back signed with it's secret and some contextual data. In this case, the token is both the proof of authentication AND identify that particular user.

c. In any future requests, the User sends up the token.

On every request, the server just checks against the token, if it matches everything we've signed it with AND works for that user, we give access.

The reason this is "light weight" is because there's no need to keep a session in memory. We don't have to store anything anywhere because the user takes care of the token. This is super advantageous in service based architecture because we don't have to deal with directing users to the server that their session is started on. They can instead go to any server that, and be all the same.

Now, it crosses the fine lines of lightweight once people start zipping every single thing into a JWT like they would a cookie. Ours, however, just is the proof of authenticity and user. That's all.

But what if someone gets my JWT??

Well what if someone gets your cookie? Or your password and email? It's the same thing. Sure they'd be able to do things with it until it expired or changed. We could go through the pains of associating the token to an IP or etc. but most of the time this will suffice. These aren't some stop gap security measure - they're a form of authenticating. You still need SSL to make sure no man-in-the-middle attacks occur. You still need to lock down your infrastructure and potentially only whitelist requests from your web application domain. Security is 1000 cuts (or bandaids). However, for the role expected of them, JWTs are fabulous.

I heard this one guy say this one guy said that JWTs are bad!

I heard this one guy say that this other guy say that cookies and sessions are bad. Security, again, is a 1000 cuts (or bandaids). JWTs are great. You can have all the little authenticity checks you want and have cookies and sessions and jwts and whatever else ... and if your user just makes their password "password" ---- game over. Anyhow, that's another topic for another time.

Let's dive back into it and finish out our application by allowing fetching of our created widgets!

Learn how to deploy production-ready Node and React applications on AWS. Get notified when my next AWS DevOps Workshop opens:

Alrighty! Now that we've gone through all of the hard parts, this is about as straight forward as it gets. Everything we'll do in this section we've done before on some level.

Let's begin with our state and reducer.

11. Open up widgets/reducer.js and modify it to include our widget requesting functionality:

import { WIDGET_CREATING, WIDGET_CREATE_SUCCESS, WIDGET_CREATE_ERROR, WIDGET_REQUESTING, WIDGET_REQUEST_SUCCESS, WIDGET_REQUEST_ERROR, } from './constants' const initialState = { list: [], // where we'll store widgets requesting: false, successful: false, messages: [], errors: [], } const reducer = function widgetReducer (state = initialState, action) { switch (action.type) { // .. all the WIDGET_CREATE cases case WIDGET_REQUESTING: return { ...state, // ensure that we don't erase fetched ones requesting: false, successful: true, messages: [{ body: 'Fetching widgets...!', time: new Date(), }], errors: [], } case WIDGET_REQUEST_SUCCESS: return { list: action.widgets, // replace with fresh list requesting: false, successful: true, messages: [{ body: 'Widgets awesomely fetched!', time: new Date(), }], errors: [], } case WIDGET_REQUEST_ERROR: return { requesting: false, successful: false, messages: [], errors: state.errors.concat[{ body: action.error.toString(), time: new Date(), }], } default: return state } } export default reducer

This is just showing the additions. The WIDGET_CREATE actions are still there, I just didn't include them for brevity!

Every single bit of this should be familiar.

Let's move on and create the constants.

12. Open up widgets/constants.js and add the following:

export const WIDGET_CREATING = 'WIDGET_CREATING' export const WIDGET_CREATE_SUCCESS = 'WIDGET_CREATE_SUCCESS' export const WIDGET_CREATE_ERROR = 'WIDGET_CREATE_ERROR' export const WIDGET_REQUESTING = 'WIDGET_REQUESTING' export const WIDGET_REQUEST_SUCCESS = 'WIDGET_REQUEST_SUCCESS' export const WIDGET_REQUEST_ERROR = 'WIDGET_REQUEST_ERROR'

Woo! Now on to the actions.

13. Open up widgets/actions.js and add the following:

import { WIDGET_CREATING, WIDGET_CREATE_SUCCESS, WIDGET_CREATE_ERROR, WIDGET_REQUESTING, WIDGET_REQUEST_SUCCESS, WIDGET_REQUEST_ERROR, } from './constants' // .. all the widgetCreate actions export const widgetRequest = function widgetRequest (client) { return { type: WIDGET_REQUESTING, client, } } export const widgetRequestSuccess = function widgetRequestSuccess (widgets) { return { type: WIDGET_REQUEST_SUCCESS, widgets, } } export const widgetRequestError = function widgetRequestError (error) { return { type: WIDGET_REQUEST_ERROR, error, } }

Again, the widgetCreate actions are still there, just not included for brevity.

And that does it for our application's state and dealing with included the widgets. If we request widgets, we'll flag requesting. On success, we expect an action to be dispatched with the full list of widgets. On error, we expect the error to be returned.

Now for the list view.

14. Open up widgets/index.js and modify the render() function to include our list:

class Widgets extends Component { // ... all of our other functions render () { // ... all of our props return ( <div className="widgets"> <div className="widget-form"> <form onSubmit={handleSubmit(this.submit)}> <h1>CREATE THE WIDGET</h1> <label htmlFor="name">Name</label> {/* We will use a custom component AND a validator */} <Field name="name" type="text" id="name" className="name" component={this.renderNameInput} validate={nameRequired} /> <label htmlFor="description">Description</label> <Field name="description" type="text" id="description" className="description" component="input" /> <label htmlFor="size">Size</label> <Field name="size" type="number" id="size" className="number" component="input" /> {/* the button will remain disabled until not invalid */} <button disabled={invalid} action="submit" >CREATE</button> </form> <hr /> <div className="widget-messages"> {requesting && <span>Creating widget...</span>} {!requesting && !!errors.length && ( <Errors message="Failure to create Widget due to:" errors={errors} /> )} {!requesting && successful && !!messages.length && ( <Messages messages={messages} /> )} </div> </div> {/* The Widget List Area */} <div className="widget-list"> <table> <thead> <tr> <th>Name</th> <th>Description</th> <th>Size</th> </tr> </thead> <tbody> {list && !!list.length && ( list.map(widget => ( <tr key={widget.id}> <td> <strong>{`${widget.name}`}</strong> </td> <td> {`${widget.description}`} </td> <td> {`${widget.size}`} </td> </tr> )) )} </tbody> </table> {/* A convenience button to refetch on demand */} <button onClick={this.fetchWidgets}>Refetch Widgets!</button> </div> </div> ) } }

And once again, we didn't just delete everything in the component, this is the new stuff.

All that's happening here is that we're iterating through our list of widgets . If there is a list AND it has widgets, we'll iterate over them and display. We've also referenced a this.fetchWidgets function that we'll need to create.

15. Still in our widgets/index.js file, add the fetchWidgets() and constructor() function to our Widgets component:

// .. rest of the imports ^^ // include our widgetRequest action; ADDED widgetRequest() import { widgetCreate, widgetRequest } from './actions' // Our validation function for `name` field. const nameRequired = value => (value ? undefined : 'Name Required') class Widgets extends Component { static propTypes = { handleSubmit: PropTypes.func.isRequired, invalid: PropTypes.bool.isRequired, client: PropTypes.shape({ id: PropTypes.number.isRequired, token: PropTypes.object.isRequired, }), widgets: PropTypes.shape({ list: PropTypes.array, requesting: PropTypes.bool, successful: PropTypes.bool, messages: PropTypes.array, errors: PropTypes.array, }).isRequired, widgetCreate: PropTypes.func.isRequired, widgetRequest: PropTypes.func.isRequired, // <-- new reset: PropTypes.func.isRequired, } // Add the constructor constructor (props) { super(props) // call the fetch when the component starts up this.fetchWidgets() } // the helper function for requesting widgets // with our client as the parameter fetchWidgets = () => { const { client, widgetRequest } = this.props if (client && client.token) return widgetRequest(client) return false } // .. all of the other functions } // Pull in both the Client and the Widgets state const mapStateToProps = state => ({ client: state.client, widgets: state.widgets, }) // Make the Client and Widgets available in the props as well // as the widgetCreate() AND widgetRequest() function vvvv const connected = connect(mapStateToProps, { widgetCreate, widgetRequest })(Widgets) // ... rest of file

The changes are:

importing in our widgetRequest() function and connecting it to our component at the bottom

function and connecting it to our component at the bottom adding the widgetRequest as a propType

in our constructor() calling to our fetch widgets immediately, so we can get the latest ones

calling to our fetch widgets immediately, so we can get the latest ones creating the fetchWidgets() function that, upon call, will trigger our action widgetRequest() with our client

Finally, let's hammer out the last of our saga to fetch these Widgets.

16. Open up our widgets/sagas.js file and modify it to be the following:

import { call, put, takeLatest } from 'redux-saga/effects' import { handleApiErrors } from '../lib/api-errors' import { WIDGET_CREATING, WIDGET_REQUESTING, // <-- add this } from './constants' import { widgetCreateSuccess, widgetCreateError, widgetRequestSuccess, // <-- import widgetRequestError, // <-- import } from './actions' const widgetsUrl = `${process.env.REACT_APP_API_URL}/api/Clients` // ADDED // Nice little helper to deal with the response // converting it to json, and handling errors function handleRequest (request) { return request .then(handleApiErrors) .then(response => response.json()) .then(json => json) .catch((error) => { throw error }) } function widgetCreateApi (client, widget) { const url = `${widgetsUrl}/${client.id}/widgets` const request = fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', // passes our token as an "Authorization" header in // every POST request. Authorization: client.token.id || undefined, // will throw an error if no login }, body: JSON.stringify(widget), }) return handleRequest(request) // <-- ADDED } function* widgetCreateFlow (action) { try { const { client, widget } = action const createdWidget = yield call(widgetCreateApi, client, widget) // creates the action with the format of // { // type: WIDGET_CREATE_SUCCESS, // widget, // } // Which we could do inline here, but again, consistency yield put(widgetCreateSuccess(createdWidget)) } catch (error) { // same with error yield put(widgetCreateError(error)) } } // ADDED function widgetRequestApi (client) { const url = `${widgetsUrl}/${client.id}/widgets` const request = fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', // passe our token as an "Authorization" header Authorization: client.token.id || undefined, }, }) return handleRequest(request) } // ADDED function* widgetRequestFlow (action) { try { // grab the client from our action const { client } = action // call to our widgetRequestApi function with the client const widgets = yield call(widgetRequestApi, client) // dispatch the action with our widgets! yield put(widgetRequestSuccess(widgets)) } catch (error) { yield put(widgetRequestError(error)) } } function* widgetsWatcher () { // each of the below RECEIVES the action from the .. action yield [ takeLatest(WIDGET_CREATING, widgetCreateFlow), takeLatest(WIDGET_REQUESTING, widgetRequestFlow), // <-- ADDED ] } export default widgetsWatcher

No omissions this time, this is the full file.

The changes we've made:

Include our request constant and actions.

Create our widgetRequestFlow and widgetRequestApi functions.

and functions. Abstract converting requests to json and handling errors into a helper function handleRequest() .

. Refactoring our widgetCreateApi() to use the handleRequest() helper.

to use the helper. Adding WIDGET_REQUESTING to our widgetsWatcher() .

And BOOM. We're done! The most beautiful app in the world:

Learn how to deploy production-ready Node and React applications on AWS. Get notified when my next AWS DevOps Workshop opens:

In this tutorial we covered the following:

Modeling out our Widget state

Using Redux Form to do on-the-fly validations

Using Redux Saga to Create and Fetch our Widgets

JWT Aside

All the base concepts have been touched upon and developing new features for our widget app is just as simple as:

more states ->

more actions ->

more views ->

more views

Interacting with any API endpoint would follow the exact same pattern as previous API requests. We'd just change the request type and send any required parameters.

Congratulations you are now a Widget Master!

The Entire Code Base Here!

So what now? Well there's a couple of places you can take your learning.

If you'd like to learn how to build out the API, as mentioned previously, here is an entire guide on doing so with Node + Express + Loopback:

Authorized Resources and Database Migrations with Strongloop's Loopback

Admittedly, I could've named it much better, since really it is just a guide on Node + Express + Loopback.

If you'd like to learn how to deploy this app, any create-react-app or any front-end app that you can compile down to html/css/js, to AWS, there's an entire guide for that as well:

Guide to Fault Tolerant and Load Balanced AWS Docker Deployment on ECS

Finally, if you'd like to learn how to Dockerize your react app, leverage SASS and React Storybook, here's a brief guide on that:

Create React App with SASS, Storybook and Yarn in a Docker Environment

The only other missing area that I dive into, based on feedback, will be testing with Jest.

As usual, if you find any technical glitches or hiccups PLEASE leave a comment or hit me up on twitter or with a message!

Enjoy Posts Like These? Sign up to my mailing list!

My Tech Guides and Thoughts Mailing List