React and Redux Sagas Authentication App Tutorial
Overview
In this Tutorial series we're going to take a deep, PRACTICAL guide into using some of the most popular and yet under explained React tools:
It's not necessarily that any of these are difficult to use, but wiring them together and making something beyond the trivial "hello world" apps can be a maze. Half the time it's just due to the lack of practical explanation more so than any particular piece being complicated.
And all you wanted to do was make a todo list...
While we'll stop along the way and explain the benefits, etc, this is going to be more focused on putting it together from a practical perspective - purely due to the fact that most things out there in the Redux system are nothing but "why it's so awesome and that you should use it."
This will be a 3 part series.
Part 1 - Setting up the base project and Signing up our Users
Part 2 - Making the Login and Authentication/Authorization flow
Part 3 - Working with protected resources
It will use a REAL api that you'll either be able to pull down and hack on yourself, leverage a free on I'm hosting, or bring your own json web token based api.
It'll be mentioned much later - but there's an entire tutorial here on how to build a Node, Express and Strongloop API. You can plug in what you learn from there directly into this tutorial.
Update: the live API has been taken down, but you can set the API up locally using the above mentioned tutorial.
I've tried to keep the focus purely on the front-end in order to keep this from getting toooooo long.
The entire codebase for this application can be found here:
https://github.com/jcolemorrison/redux-sagas-authentication-app
Table of Contents:
- Getting the API Setup
- Scaffolding Out the React App
- Redux Aside
- The Client State
- The Signup State
- The Signup View
- Hooking Up to Our Redux Sagas
- Summary
Getting the API Setup
0) Create a folder, code
Small successes.
1) Setup the API
There are 3 approaches we can take here:
1) Bring your own token based API.
It should satisfy the requirements of being able to signup a user, login a user and have a protected resource. Although if you're bringing your own API, you'll be able to apply what you learn here to any API EASILY.
2) Use the api at http://widgetizer.jcolemorrison.com
This is just a hosted version of that api I have live for this tutorial so that we can hone in on React and not worry about API's. Any data you save to it is wiped every 24 hours though. (also I'm not sure if it's something I'll leave up permanently - however if I do take it down I'll update this post to reflect that)
If you want to use that, there's nothing else to do. We'll just use that URL in place of the local API one when we setup the environment variables.
If you're curious about what the API is, there's a full guide on how it's setup here:
Authorized Resources and Database Migrations with Strongloop's Loopback:
3) Pulling down the raw API code
Speaking of the tutorial on how to set it up, the entire code base is also available for yo to pull down and use directly. It does require that you have Docker installed and also a Docker Hub account.
If you choose to use it:
a) create a directory api
and cd
into it
Make sure you're logged into docker hub so that you can pulldown the development images:
b) $ docker login
Clones the repo:
c) $ git clone https://github.com/jcolemorrison/strongloop-automigration-demo .
This pulls the images from docker hub.
d) $ docker-compose pull
To install the dependencies:
e) $ yarn
Alternatively $ npm i
if that's your thing.
Up the docker environment:
f) $ docker-compose up -d
And now you've got a fully running API available at http://localhost:3002
. You can see the entire API at http://localhost:3002/explorer
. This is nothing more than a very simple Client
model signup / login api with 1 protected resource known as Widget
(because we all need good widgets.)
Note: MySQL first boot takes too long to complete, you may need to do a docker-compose down
and then another docker-compose up -d
Note 2: At any time see what's happening in your API logs by running docker-compose logs -f
in the api folder
Additionally you'll have a dockerized MySQL image running at 3306
with the passwords, user and database name specified in the .env
file (which should never be committed in real apps, but saves steps for tutorial purposes). See the blog post for more details on how to use this API.
Again, this isn't necessary, but will give you a real feel for the API login. Feel free to use ANY token based API.
Scaffolding out the React App
2) Make sure you have create-react-app
installed
a) $ npm install -g create-react-app
I'd love to use yarn
here but it has problems installing globals, especially if you use something great like NVM.
3) Move into our code
directory
4) Run $ create-react-app .
Wait for it to finish up
5) Once complete run:
$ yarn add redux react-redux redux-saga react-router redux-form
or, if you truly enjoy npm
$ npm i redux react-redux redux-saga react-router redux-form --save
6) The rest of all of our work will be in the code
directory, so move into that.
Go ahead and up our Dev environment:
$ yarn start
7) In the src/
directory let's make 6 Folders.
First off let's get all the basics setup - the high level scaffolding, folder structure and initial files setup. No real explanation here since much of folder and file organization becomes preference:
src/
login/
sagas.js
reducer.js
actions.js
constants.js
index.js
signup/
sagas.js
reducer.js
actions.js
constants.js
index.js
widgets/
sagas.js
reducer.js
actions.js
constants.js
index.js
client/
reducer.js
actions.js
constants.js
notifications/
Messages.js
Errors.js
lib/
api-errors.js
check-auth.js
index-reducer.js
index-sagas.js
The login/
, signup/
and widget/
folders are all going to be the containers for our different routes. Explanations of the files are:
a) index.js
- the actual container component itself and all of the react goodness
b) sagas.js
- where we'll store our sagas to watch for API related calls
c) reducer.js
- where we'll manage the piece of state related to this container
d) actions.js
- where we'll keep all of the actions that our container dispatches
e) constants.js
- where we'll store our constants for reducers/actions
The notifications/
folder will house two components that will help us display message and error notifications without having to repeat ourselves.
The lib/
has two files - one for helping with api call errors and the other that we'll later use to setup auth checking when a user visits a route that should be protected.
The client/
- while we might create a page at some later data that deals with updating the Client's info (username, email, etc), for now this will just be a place for our actions/reducer/constants related to dealing with the Client.
Finally, index-reducer
and index-sagas
are going to be the main HQ for our reducers and sagas respectively. Whenever we setup a reducer or saga, we'll make sure to include them in these files.
Tip: If you're in Atom or Sublime Text, just use command + p
and begin typing the name of your component name followed by the related file. So to get to client/actions.js
for example you only need to type cl acti
before they'll pick up and hand you the right file
8) Open up src/index.js
In here we're going to lay out some scaffolding code:
// `src/index.js`
import React from 'react'
import ReactDOM from 'react-dom'
import { applyMiddleware, createStore, compose } from 'redux'
import { Provider } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import { Router, Route, browserHistory } from 'react-router'
// Import all of our components
import App from './App'
import Login from './login'
import Signup from './signup'
import Widgets from './widgets'
import './index.css'
// Import the index reducer and sagas
import IndexReducer from './index-reducer'
import IndexSagas from './index-sagas'
// Setup the middleware to watch between the Reducers and the Actions
const sagaMiddleware = createSagaMiddleware()
// Redux DevTools - completely optional, but this is necessary for it to
// work properly with redux saga. Otherwise you'd just do:
//
// const store = createStore(
// IndexReducer,
// applyMiddleware(sagaMiddleware)
// )
/*eslint-disable */
const composeSetup = process.env.NODE_ENV !== 'production' && typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose
/*eslint-enable */
const store = createStore(
IndexReducer,
composeSetup(applyMiddleware(sagaMiddleware)), // allows redux devtools to watch sagas
)
// Begin our Index Saga
sagaMiddleware.run(IndexSagas)
// Setup the top level router component for our React Router
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={App} >
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/widgets" component={Widgets} />
</Route>
</Router>
</Provider>,
document.getElementById('root'),
)
I won't dive deep into react-router
due to its simplicity and unusually high amount of great resources out there on how to use it. The whole idea though is just optionally rendering components based on routes. In our case, rendering them as the children
of our App
component.
Also, I highly suggest adding the Redux Devtools Chrome Extension. It's INSANELY useful for both developing AND learning.
We'll come back later to scaffold this out, but for now this is enough to continue with what we need to learn.
9) Open up src/index-reducer.js
import { combineReducers } from 'redux'
import { reducer as form } from 'redux-form'
const IndexReducer = combineReducers({
form,
})
export default IndexReducer
That's all we'll do for now. In order for redux-form
to work, it needs it's reducer injected into the global state.
10) Open up src/index-sagas.js
export default function* IndexSaga () {
yield []
}
Is all we'll put into this file for now. We'll touch on it much more when we start plugging in sagas.
The way to think about both of these index files are.. well just that they'll be the central hub for all of our reducers and sagas.
11) Copy the css from:
https://github.com/jcolemorrison/redux-sagas-authentication-app/blob/master/src/App.css
Into your App.css
file so that we can not worry about styles at all.
12) Create a .env
file in the root of our code
directory
Inside of it we're going to define one environment variable:
This will either be:
REACT_APP_API_URL=http://widgetizer.jcolemorrison.com
If you're just using the hosted API
or
REACT_APP_API_URL=http://localhost:3002
If you've pulled down the source API and are hosting it yourself.
Also, it's called REACT_APP_*
because create-react-app
only makes environment variables prefixed with this available in our application.
make sure to restart create-react-app after adding this
13) Finally let's scaffold out the base 3 class components
..so that React Router will quit complaining. It's also nice to get it out of the way.
For login/index.js
:
import React, { Component } from 'react'
class Login extends Component {}
export default Login
For signup/index.js
:
import React, { Component } from 'react'
class Signup extends Component {}
export default Signup
For widgets/index.js
:
import React, { Component } from 'react'
class Widgets extends Component {}
export default Widgets
Finally, let's modify App.js
to actually accept children:
import React, { PropTypes } from 'react'
import logo from './logo.svg'
import './App.css'
const App = props => (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to Widget Reactory</h2>
</div>
<section className="App-body">
{props.children}
</section>
</div>
)
App.propTypes = {
children: PropTypes.node,
}
export default App
Eggggscellent. Now we have the base starting point setup and are ready to go.
This is the base
branch under the aforementioned github repo.
https://github.com/jcolemorrison/redux-sagas-authentication-app/tree/base
Redux Aside
It took me far to long really grasp Redux because of how wonky it's constantly described. I remember watching some of the creator's videos, and maybe it's just me, but they made very little practical sense at the time. Therefore, I just want to leave a quick set of concepts in terms of how I think about it:
a) We setup a global "store" to "store" our application's "state". Not to be confused with React's internal state
.
Think of this like a state of states. A country.
b) We provide our store
with reducers
which are like the "gatekeepers", "managers" of their own piece of state.
So think of them like the governors of each state of that bigger state, or country. When something comes into modify their state, they accept/reject or use it.
c) We "provide" that store
to our app via the <Provider store={store} />
.
Get it? We provide
our app
with a store
. This makes it possible for our app to be aware of this global state.
d) Our app
dispatches actions
to of how we'd like to make our global applications state (store
) be due to that action.
So, we signup for an app. That is an action
that must change our application's global state (country) but particularly a specific piece (state).
e) Our reducers
, if the action
is relevant to them, capture it and modify their state accordingly.
The reducer governor in charge of, let's say the Signup state, sees that someone would like to modify it, and either accepts/rejects or changes the state.
f) As our global state changes, our connected app
receives these changes and re-renders our react components appropriately.
The Client State
Now that everything is setup, let's start with the most logical first user point of interaction - signing up.
Let's start with the really basic view. We'll spice it up as we go, but it's nice to see what it is we're working with and often helps with planning out everything else.
There's two ways we could begin this, and I really see nothing wrong in either. We could begin with the actual state
of our application and it's data and from there build out the view
. Or we could be visual and start with the view and from that determine our state
.
Even though most hardcore Reactors will call lose their minds if we don't begin with state
, the situations where I find it's advantageous to begin with the view
is when..
a) There's barely any design or product wireframes/mockups/specs to model data after. Maybe the designer is behind, or maybe we don't even have one.
b) We have little experience with a particular framework, business problem or use case. Never used Redux Form before? Well, good luck trying to model state before diving into it a few times.
c) The API is a moving target. Maybe the backend developer keeps changing how we'll receive the data and send it because they think it's funny.
DESPITE that though - we will begin with the state
, because our API is solid. At anytime you can view the ins and outs of it here (you can also play around with it in the browser or curl):
http://widgetizer.jcolemorrison.com/explorer
If you're using the one we built in the other tutorial (or that you pulled down):
http://localhost:3002/explorer
And then click on Client
.
We see that if we want to create a new Client
, aka Signup aka POST. We need to hit the endpoint:
POST /Client
with
{
"realm": "string",
"username": "string",
"email": "string",
"emailVerified": true,
"password": "string"
}
We'll ignore all but the email
property. Additionally the password
property won't show up because it's set as hidden
on the API docs.
When we successfully create a user it will return with the pattern of:
{
"email": "email of user that just signed up",
"id": 1
}
Additionally, logging in will result in an accessToken
that looks like:
{
"id": "id of accessToken",
"ttl": "how long the access token is valid",
"created": "when the token was created",
"userId": "id of the logged in user"
}
This gives us a better insight of how to model our state. So let's begin with the Client
.
14) Open up src/client/reducer.js
and modify it to be:
const initialState = {
id: null,
token: null,
}
This gives us an outline of what our Client will look like state wise. It's not much BUT it gives us a good idea of the types of actions we need to create.
Our state will have the ID of the user and the accessToken
object.
So how will we go about updating this? This brings us to actions...
15) Open up src/client/actions.js
and modify it to be:
import { CLIENT_SET, CLIENT_UNSET } from './constants'
export function setClient (token) {
return {
type: CLIENT_SET,
token,
}
}
export function unsetClient () {
return {
type: CLIENT_UNSET,
}
}
Two basic actions that we'll use to modify state - setting and unsetting the client. Simple. We should make those constants now though..
16) Open up src/client/constants.js
and modify it to be:
export const CLIENT_SET = 'CLIENT_SET'
export const CLIENT_UNSET = 'CLIENT_UNSET'
Now let's head back over to our Reducer and make this actually modify the piece of state it manages.
17) Open up src/client/reducer.js
and change it to:
import { CLIENT_SET, CLIENT_UNSET } from './constants'
const initialSate = {
id: null,
token: null,
}
const reducer = function clientReducer (state = initialSate, action) {
switch (action.type) {
case CLIENT_SET:
return {
id: action.token.userId,
token: action.token,
}
case CLIENT_UNSET:
return {
id: null,
token: null,
}
default:
return state
}
}
export default reducer
Now let's actually include it into our index-reducer.js
18) Open up src/index-reducer
and modify it to:
import { combineReducers } from 'redux'
import { reducer as form } from 'redux-form'
import client from './client/reducer' // <--
const IndexReducer = combineReducers({
client, // <--
form,
})
export default IndexReducer
Excellent, assuming that you have the Redux Dev Tools extension installed, we can see that it's been added to our state:
We can also see form
is in our state - this is that Redux Form value we injected into our reducer. We'll only interact with it indirectly, but this is needed for Redux Form to do it's work. Also, random side note, if you name it something other than form
you'll get errors down the road. Really goofy limitation that can only be overcome by really finagling with things that our time isn't worth delving into.
Our client state is setup and ready to go.
The Signup State
As with the Client, let's try and forward think about what the state of our Signup
component will need to deal with. Off the top of my head (but not really because this is all thought out), we probably deal with following states:
requesting
- we've initiated a request to signupsuccessful
- the request has returned successfullyerrors
- an array of error messages just in case there's more than 1messages
- an array of general messages to show the user
And that's really it. We don't need to track the form values because, as we'll see, Redux Form will help us out with that.
So let's begin with the end again. Let's Model our state:
19) Open up src/signup/reducer.js
and add:
const initialState = {
requesting: false,
successful: false,
messages: [],
errors: [],
}
That's all for now, we'll come back, like we did with Client
. Again though, we know that we'll need actions to deal with the above properties.
20) Open up src/signup/actions.js
and add:
import { SIGNUP_REQUESTING } from './constants'
const signupRequest = function signupRequest ({ email, password }) {
return {
type: SIGNUP_REQUESTING,
email,
password,
}
}
export default signupRequest
Oddly enough, we only have one action in here???? What about on success and error?? What about my PROMISES?? Nope. None, we're going to let our sagas
deal with those. When we dispatch this action, it will be picked up by our saga
, which will handle the API, and dispatch actions with the correct data from there.
This allows us to keep our actions "pure" which the react community absolutely loves saying. This differs from Thunks, where we instead turn our actions into this long drawn out promise chain. We'll explain more when we get to the sagas themselves. For now though, let's go ahead and finish up our basic flow here.
21) Open up src/signup/constants.js
and add:
export const SIGNUP_REQUESTING = 'SIGNUP_REQUESTING'
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS'
export const SIGNUP_ERROR = 'SIGNUP_ERROR'
Ahhh, there they are. There's those little constants we'll be using later on. You can ignore them for now, but as we see, we will indeed be dispatching actions to handle success and error of signup.
22) Open up src/signup/reducer.js
and modify it:
import { SIGNUP_REQUESTING } from './constants'
const initialState = {
requesting: false,
successful: false,
messages: [],
errors: [],
}
const reducer = function signupReducer (state = initialState, action) {
switch (action.type) {
case SIGNUP_REQUESTING:
return {
requesting: true,
successful: false,
messages: [{ body: 'Signing up...', time: new Date() }],
errors: [],
}
default:
return state
}
}
export default reducer
We'll only worry about dealing with requesting for now. We'll come back to the other states after we get the component, redux-form and of course redux-saga all ready to go.
Let's add it to our index reducer.
23) Open up src/index-reducer.js
and modify it:
import { combineReducers } from 'redux'
import { reducer as form } from 'redux-form'
import client from './client/reducer'
import signup from './signup/reducer'
const IndexReducer = combineReducers({
signup,
client,
form,
})
export default IndexReducer
Once again, if we take a look at our Redux Dev Tools console, we'll see that piece of state has now been added.
The Signup View
First off, let's go ahead and hookup our component to Redux and Redux Form. Since this is a "Higher Order Component" like Redux, we'll also simply wrap our component up and export it.
aside on testing
Since we're not covering TDD and Jest in this, I won't bother in separate exports. But this is a useful tip. If you need to unit test your components that are connected, just export the non connected component AND then the connected one as default. You don't care if the redux wrapper worked in the unit test for your general component - you're not testing to see if redux works and does its job, you're confirming that your component does.
24) Open up src/signup/index.js
and modify it:
import React, { Component } from 'react'
import { reduxForm } from 'redux-form'
import { connect } from 'react-redux'
import { signupRequest } from './actions'
class Signup extends Component {
render () {
return <div>Signup</div>
}
}
// Grab only the piece of state we need
const mapStateToProps = state => ({
signup: state.signup,
})
// Connect our component to redux and attach the `signup` piece
// of state to our `props` in the component. Also attach the
// `signupRequest` action to our `props` as well.
const connected = connect(mapStateToProps, { signupRequest })(Signup)
// Connect our connected component to Redux Form. It will namespace
// the form we use in this component as `signup`.
const formed = reduxForm({
form: 'signup',
})(connected)
// Export our well formed component!
export default formed
The flow of what's happening is in the above snippet. If you open the Redux Devtools, we'll see that our form
piece of state has a new sub state called signup
. This is the namespace that Redux Form will use in order to manage the form we'll create within this component.
Let's go ahead and add the basic form:
25) Modify src/signup/index.js
to be the following:
import React, { Component } from 'react'
import { reduxForm, Field } from 'redux-form' // <-- added Field
import { connect } from 'react-redux'
import signupRequest from './actions'
class Signup extends Component {
render () {
return (
<div className="signup">
<form className="widget-form">
<h1>Signup</h1>
<label htmlFor="email">Email</label>
<Field
name="email"
type="text"
id="email"
className="email"
label="Email"
component="input"
/>
<label htmlFor="password">Password</label>
<Field
name="password"
type="password"
id="password"
className="password"
label="Password"
component="input"
/>
<button action="submit">SIGNUP</button>
</form>
</div>
)
}
}
// ...
The <Field>
component allows Redux Form to automatically bind values to our signup
namespace within Redux Form's form
state. Alongside the typical form input values, we can also pass it a component
. Passing it input
just informs Redux Form to use the default input. However, you can pass in your own custom form component here since it accepts a PropType.Node
.
Also, we can even see it now!
In addition to binding those values, it also HANDS us a number of properties on our component's this.props
. This includes real time updated values like whether or not the form is "dirty" or "touched"; events for when we "blur" the form; much much more;
For our purposes though we're interested in leveraging Redux Form's handleSubmit
function. We'll bind this to our form. Upon a submit
action, it will pass the values into whatever handler we give it.
I'm going to spill in some helper code here just to speed this tutorial up some. I'll document the specifics, but I want the focus to be we have a lot to cover so let's move on.
26) Modify the file to reflect it's final form!
import React, { Component, PropTypes } from 'react'
import { reduxForm, Field } from 'redux-form'
import { connect } from 'react-redux'
import { Link } from 'react-router'
// Import the helpers.. that we'll make here in the next step
import Messages from '../notifications/Messages'
import Errors from '../notifications/Errors'
import signupRequest from './actions'
class Signup extends Component {
// Pass the correct proptypes in for validation
static propTypes = {
handleSubmit: PropTypes.func,
signupRequest: PropTypes.func,
signup: PropTypes.shape({
requesting: PropTypes.bool,
successful: PropTypes.bool,
messages: PropTypes.array,
errors: PropTypes.array,
}),
}
// Redux Form will call this function with the values of our
// Form fields "email" and "password" when the form is submitted
// this will in turn call the action
submit = (values) => {
// we could just do signupRequest here with the static proptypes
// but ESLint doesn't like that very much...
this.props.signupRequest(values)
}
render () {
// grab what we need from props. The handleSubmit from ReduxForm
// and the pieces of state from the global state.
const {
handleSubmit,
signup: {
requesting,
successful,
messages,
errors,
},
} = this.props
return (
<div className="signup">
{/* Use the Submit handler with our own submit handler*/}
<form className="widget-form" onSubmit={handleSubmit(this.submit)}>
<h1>Signup</h1>
<label htmlFor="email">Email</label>
<Field
name="email"
type="text"
id="email"
className="email"
label="Email"
component="input"
/>
<label htmlFor="password">Password</label>
<Field
name="password"
type="password"
id="password"
className="password"
label="Password"
component="input"
/>
<button action="submit">SIGNUP</button>
</form>
<div className="auth-messages">
{
/*
These are all nothing more than helpers that will show up
based on the UI states, not worth covering in depth. Simply put
if there are messages or errors, we show them
*/
}
{!requesting && !!errors.length && (
<Errors message="Failure to signup due to:" errors={errors} />
)}
{!requesting && !!messages.length && (
<Messages messages={messages} />
)}
{!requesting && successful && (
<div>
Signup Successful! <Link to="/login">Click here to Login »</Link>
</div>
)}
{/* Redux Router's <Link> component for quick navigation of routes */}
{!requesting && !successful && (
<Link to="/login">Already a Widgeter? Login Here »</Link>
)}
</div>
</div>
)
}
}
// Grab only the piece of state we need
const mapStateToProps = state => ({
signup: state.signup,
})
// Connect our component to redux and attach the "signup" piece
// of state to our "props" in the component. Also attach the
// "signupRequest" action to our "props" as well.
const connected = connect(mapStateToProps, { signupRequest })(Signup)
// Connect our connected component to Redux Form. It will namespace
// the form we use in this component as "signup".
const formed = reduxForm({
form: 'signup',
})(connected)
// Export our well formed component!
export default formed
Okay, so this is a rather huge code dump. And to make things worse it includes some helper components we haven't even made yet! Don't worry about those though, they're simple and not even worth going over. You've likely made components that print strings 100 times at this point.
The focus points are:
1) We connect our component to both Redux and Redux Form
2) We create a form that's bound to use Redux Form's handleSubmit
3) When we submit, the values bound to our submit
function. From here we call our action signupRequest
with the values.
4) Depending on whether or not our signup request is requesting or successful, we'll show either a link to direct them to the /login
page OR a reminder to login.
5) If we have errors
or messages
we'll pipe those to some handlers that will iterate over the list of messages and return them as a <ul>
list for the user to see.
See? Not too bad.
Okay let's go ahead and knock those little helpers out real quick. We'll use them elsewhere in the app, so might as well get them out of the way.
26) Open up src/notifications/Messages.js
and input the following:
import React, { PropTypes } from 'react'
// Iterate over each message object and print them
// in an unordered list
const Messages = (props) => {
const { messages } = props
return (
<div>
<ul>
{messages.map(message => (
<li key={message.time}>{message.body}</li>
))}
</ul>
</div>
)
}
Messages.propTypes = {
messages: PropTypes.arrayOf(
PropTypes.shape({
body: PropTypes.string,
time: PropTypes.date,
})),
}
export default Messages
Yay! Not really much to explain here.
27) Open up src/notifications/Errors.js
and input the following:
import React, { PropTypes } from 'react'
// Iterate over each error object and print them
// in an unordered list
const Errors = (props) => {
const { errors } = props
return (
<div>
<ul>
{errors.map(errors => (
<li key={errors.time}>{errors.body}</li>
))}
</ul>
</div>
)
}
Errors.propTypes = {
errors: PropTypes.arrayOf(
PropTypes.shape({
body: PropTypes.string,
time: PropTypes.date,
})),
}
export default Errors
We could just reuse the Messages
for errors, but this is here just in case you need to handle the display of errors differently.
Great, now our application is in the state where we can begin leverage Redux Saga to hook it up to our API.
Hooking Up to Our Api via Redux Sagas
As with most things React and specifically Redux - Sagas were are all the latest buzz and fuss when they came out (and still are) but the docs, examples and talks are either too simplistic; too confusing; too unorganized; "wouldn't do this in production"; or just plain incoherent.
Grasping the concept behind them is best found through practical implementation in context of a good high level overview and concept.
So here's your Light of Elendil. May it bring you clarity when the docs are driving you nuts and you've been staring at the same error for an hour.
If...
a) Our Reducers are the managers and gatekeeprs for the individual pieces of Redux State awaiting "messages" of what to do with state.
and
b) Our Actions are sent from our components as "messages" to the reducer to modify the Redux State.
then
c) Our Redux Sagas sit between the Actions and Reducers listening for "messages". When they hear about a "message" they "care" about, they take take "Actions" into their own hands and go to work. When they've completed their work, they will also dispatch actions.
So, wow, it sounds just like Thunks. And yep, the end result, for all general "practical" uses, like API calls and dealing with asynchronous code, is the same. However, we can't just say "well why not just use thunks" since that is in the same bin with "why not just use jquery instead of react, since it's just manipulating the DOM too."
The point is that it handles them differently. It uses Generator Functions to deal with the asynchronous code. This will allow us to write a non-callback-hell style of code. They're also far easier to test than thunks and promises (stubbing out tons of mocks, especially in JEST isn't very fun).
So why boil it down so simply instead of diving into 100 different technical reasons to use it? Because half the time, with a new web tech, big flashy "why to use" articles are all that's out there. So instead let's put it to practical use and you can judge whether or not it fits your needs. I personally enjoy it far more than Thunks and have greater strides of productivity with it than not.
As above, I'm going to step through this piecemeal so that we get a grasp on what's happening with the Saga.
28) Open up src/signup/sagas.js
First, let's begin with the end in mind here. What are we doing? Well, with Redux Sagas we need to export a generator function. This will be handed off to our index-sagas
. Redux Saga will then run that function that will "wait" until it sees the action we've told it to use.
So let's go ahead and write that:
import { takeLatest } from 'redux-saga/effects'
import { SIGNUP_REQUESTING } from './constants'
// This will be run when the SIGNUP_REQUESTING
// Action is found by the watcher
function* signupFlow (action) {}
// Watches for the SIGNUP_REQUESTING action type
// When it gets it, it will call signupFlow()
// WITH the action we dispatched
function* signupWatcher () {
// takeLatest() takes the LATEST call of that action and runs it
// if we we're to use takeEvery, it would take every single
// one of the actions and kick off a new task to handle it
// CONCURRENTLY!!!
yield takeLatest(SIGNUP_REQUESTING, signupFlow)
}
export default signupWatcher
So my comments kind of sum things up. In a nutshell though - this saga:
1) waits until it "sees" the SIGNUP_REQUESTING
action dispatched
2) calls the signupFlow()
with the action received
3) signupFlow()
then run in a generator stepping through each call in a non-blocking SYNCHRONOUS looking style.
The most interesting note is the way takeLatest
and takeEvery
work. They kick off background tasks that will run what we've asked for. Were we to use takeEvery
and fire tons of signup requests, Redux Saga would do and complete every single one of them concurrently.
In Part 2, we'll get to work with the generator loop at an even finer level. For now though...
29) In our src/signup/sagas.js
file, modify the signupFlow()
and add the needed constants and Redux Saga functions:
import { call, put, takeLatest } from 'redux-saga/effects'
import {
SIGNUP_REQUESTING,
SIGNUP_SUCCESS,
SIGNUP_ERROR,
} from './constants'
function signupApi () {}
// This will be run when the SIGNUP_REQUESTING
// Action is found by the watcher
function* signupFlow (action) {
try {
const { email, password } = action
// pulls "calls" to our signupApi with our email and password
// from our dispatched signup action, and will PAUSE
// here until the API async function, is complete!
const response = yield call(signupApi, email, password)
// when the above api call has completed it will "put",
// or dispatch, an action of type SIGNUP_SUCCESS with
// the successful response.
yield put({ type: SIGNUP_SUCCESS, response })
} catch (error) {
// if the api call fails, it will "put" the SIGNUP_ERROR
// into the dispatch along with the error.
yield put({ type: SIGNUP_ERROR, error })
}
}
// ..
Ah. As you can see, no call back errors in our handler code. It'll just hit the API, wait for it, once it's done, put the action. The comments have some more detail.
What's funny is, I just said that, but now we're going to make the API call which still uses promises... ugh.
30) Modify our src/signup/sagas.js
to reflect it's final form!
The final file, ABOVE the function* signupFlow
, should look like:
import { call, put, takeLatest } from 'redux-saga/effects'
import { handleApiErrors } from '../lib/api-errors'
import {
SIGNUP_REQUESTING,
SIGNUP_SUCCESS,
SIGNUP_ERROR,
} from './constants'
// The url derived from our .env file
const signupUrl = `${process.env.REACT_APP_API_URL}/api/Clients`
function signupApi (email, password) {
// call to the "fetch". this is a "native" function for browsers
// that's conveniently polyfilled in create-react-app if not available
return fetch(signupUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
})
.then(handleApiErrors) // we'll make this in a second
.then(response => response.json())
.then(json => json)
.catch((error) => { throw error })
}
// ..
This makes a call to our API, returns the response as json, or an error. Very straight forward. Let's go make that handleApiErrors
helper real quick:
31) Open up src/lib/api-errors.js
and input the following:
// because Fetch doesn't recognize error responses as
// actual errors since it's technically completing the response...
export function handleApiErrors (response) {
if (!response.ok) throw Error(response.statusText)
return response
}
AWESOME. There's our SAGA. It will listen for the SIGNUP_REQUESTING
action, call to our signup API in the background. When it succeeds it will dispatch the SIGNUP_SUCCESS
action or the SIGNUP_ERROR
action if it fails.
OBVIOUSLY, we still need to go update our reducer to HANDLE those different actions. So let's head over to our reducer and modify it.
32) Open up src/signup/reducer.js
and modify it to deal with the SIGNUP_SUCCESS
and SIGNUP_REQUESTING
actions:
import {
SIGNUP_REQUESTING,
SIGNUP_SUCCESS,
SIGNUP_ERROR,
} from './constants'
const initialState = {
requesting: false,
successful: false,
messages: [],
errors: [],
}
const reducer = function signupReducer (state = initialState, action) {
switch (action.type) {
case SIGNUP_REQUESTING:
return {
requesting: true,
successful: false,
messages: [{ body: 'Signing up...', time: new Date() }],
errors: [],
}
// reset the state and add a body message of success!
// remember our successful returned payload will be:
// {"email": "of the new user", "id": "of the user"}
case SIGNUP_SUCCESS:
return {
errors: [],
messages: [{
body: `Successfully created account for ${action.response.email}`,
time: new Date(),
}],
requesting: false,
successful: true,
}
// reset the state but with errors!
// the error payload returned is actually far
// more detailed, but we'll just stick with
// the base message for now
case SIGNUP_ERROR:
return {
errors: state.errors.concat([{
body: action.error.toString(),
time: new Date(),
}]),
messages: [],
requesting: false,
successful: false,
}
default:
return state
}
}
export default reducer
OKAY. One last step. And it's simple thankfully! We just need to add our Saga to our index-sagas
33) Open src/index-sagas.js
and modify it to include the new signup saga:
import SignupSaga from './signup/sagas'
export default function* IndexSaga () {
yield [
SignupSaga(),
]
}
And we're ready for launch!
Let's head over to the browser and successfully sign up!
34) Head over to your browser, which should be at localhost:3000
.
Navigate to /signup
And signup with an email and password!
Additionally, if we try and input a bad password:
We get a really ugly error message. If you'd like to clean that up though and be more congenial about the errors, go for it.
WOOO!!!
Summary
What'd we accomplished?
1) Hook up React, Redux, Redux Saga, React Router AND Redux Form to work together!
2) Scaffold out a starting point for our entire app
3) Model out the global state, reducer, sagas and signup reducer/state/actions/etc
4) Hook up Redux Form to our component
5) CREATE OUR SAGA!
6) Completed full signup!
Awesome, you can see the entirety of this code base:
https://github.com/jcolemorrison/redux-sagas-authentication-app
on the signup branch.
In Part 2
We'll do the next part of this flow. Logging in and diving even more into some of the cool nuances of Redux Saga!
My plan is to have it out next Tuesday/Wednesday (2/21/2017). Be sure to signup for updates!
Update: Part 2 can be found here
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
More from the blog
J Cole Morrison
http://start.jcolemorrison.comDeveloper Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io