React and Redux Sagas Authentication App Tutorial Part 3
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:
Table of Contents
- What We'll Build
- Modeling the Widget State
- The Widget View and Redux Form Validation
- The Widget Create Saga
- JSON Web Token (JWT) Aside
- Extending the Widget State for Fetching!
- Making the Widgets View!
- Summary
- The End?
What We'll Build
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.
Modeling the Widget State
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.
The Widget View and Redux Form Validation
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:
The Widget Create Saga
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!
JSON Web Token (JWT) Aside
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!
Extending the Widget State for Fetching!
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.
Making the Widgets 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 - adding the widgetRequest as a propType
- in our
constructor()
calling to our fetch widgets immediately, so we can get the latest ones - creating the
fetchWidgets()
function that, upon call, will trigger our actionwidgetRequest()
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
andwidgetRequestApi
functions. - Abstract converting requests to json and handling errors into a helper function
handleRequest()
. - Refactoring our
widgetCreateApi()
to use thehandleRequest()
helper. - Adding
WIDGET_REQUESTING
to ourwidgetsWatcher()
.
And BOOM. We're done! The most beautiful app in the world:
Summary
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 End?
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
J Cole Morrison
http://start.jcolemorrison.comDeveloper Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io