Phoenix Todos - Finishing Authentication

On Sep 28, 2016 by Pete Corey

This post is written as a set of Literate Commits. The goal of this style is to show you how this program came together from beginning to end.

Each commit in the project is represented by a section of the article. Click each section's header to see the commit on Github, or check out the repository and follow along.

Client-side Validation Bug

You may have noticed that with our previous solution, only server-side errors would show on the sign-up form. Client-side validation was taking place in our onSubmit handler, but errors were never propagating to the UI!

This was happening because we were storing client-side validation errors in the JoinPage component’s state:


this.setState({ errors });

However, our render function was pulling errors out of the props passed into the component by Redux.

Our component didn’t have a single source of truth for the errors array.

The fix to this issue is fairly elegant. We can pull the validation checks out of the onSubmit handler and move them into our signUp action. If we detect any validation issues, we’ll return them to the JoinPage component by dispatching a SIGN_UP_FAILURE action:


if (errors.length) {
  return dispatch(signUpFailure(errors));
}

From our component’s perspective, all errors are seen as server-side errors and render correctly.

web/static/js/actions/index.js

... dispatch(signUpRequest()); + + let errors = []; + if (!email) { + errors.push({ email: "Email required" }); + } + if (!password) { + errors.push({ password: "Password required" }); + } + if (password_confirm !== password) { + errors.push({ password_confirm: "Please confirm your password" }); + } + if (errors.length) { + return Promise.resolve(dispatch(signUpFailure(errors))); + } + return fetch("/api/users", {

web/static/js/pages/AuthPageJoin.jsx

... const password_confirm = this.refs.password_confirm.value; - const errors = {}; - - if (!email) { - errors.email = 'Email required'; - } - if (!password) { - errors.password = 'Password required'; - } - if (password_confirm !== password) { - errors.password_confirm = 'Please confirm your password'; - } - - this.setState({ errors }); - if (Object.keys(errors).length) { - return; - }

Sign-out Actions

Now that we’ve established the pattern our Redux actions and reducers will follow, we can start implementing our other authentication features.

To give users the ability to sign out, we’ll start by creating three new actions: SIGN_OUT_REQUEST, SIGN_OUT_SUCCESS, and SIGN_OUT_FAILURE.

Along with the action creators for each of these actions, we’ll also create an asynchronous action function called signOut which accepts the current user’s JWT as an argument. This function makes a DELETE request to our /api/sessions endpoint, sending the jwt in the "Authorization" header:


return fetch("/api/sessions", {
  method: "delete",
  headers: {
    "Accept": "application/json",
    "Content-Type": "application/json",
    "Authorization": jwt
  }
})

Our SIGN_OUT_SUCCESS reducer clears the user and jwt fields in our application state:


case SIGN_OUT_SUCCESS:
  return Object.assign({}, state, {
    user: undefined,
    jwt: undefined
  });

And the SIGN_OUT_FAILURE resolver will save any errors from the server into errors.

Now that our sign-out actions and resolvers are set, we can wire our App component up to our Redux store with a call to connect, and replace our old Meteor.logout() code with a call to our signOut thunk:


this.props.signOut(this.props.jwt)

With that, authenticated users have the ability to sign out of our application!

web/static/js/actions/index.js

... +export const SIGN_OUT_REQUEST = "SIGN_OUT_REQUEST"; +export const SIGN_OUT_SUCCESS = "SIGN_OUT_SUCCESS"; +export const SIGN_OUT_FAILURE = "SIGN_OUT_FAILURE"; + export function signUpRequest() { ... +export function signOutRequest() { + return { type: SIGN_OUT_REQUEST }; +} + +export function signOutSuccess() { + return { type: SIGN_OUT_SUCCESS }; +} + +export function signOutFailure(errors) { + return { type: SIGN_OUT_FAILURE, errors }; +} + export function signUp(email, password, password_confirm) { ... } + +export function signOut(jwt) { + return (dispatch) => { + dispatch(signOutRequest()); + return fetch("/api/sessions", { + method: "delete", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": jwt + } + }) + .then((res) => res.json()) + .then((res) => { + if (res.errors) { + dispatch(signOutFailure(res.errors)); + return false; + } + else { + dispatch(signOutSuccess()); + return true; + } + }); + } +}

web/static/js/layouts/App.jsx

... import Loading from '../components/Loading.jsx'; +import { connect } from "react-redux"; +import { signOut } from "../actions"; ... -export default class App extends React.Component { +class App extends React.Component { constructor(props) { ... logout() { - Meteor.logout(); - - // if we are on a private list, we'll need to go to a public one - if (this.props.params.id) { - const list = Lists.findOne(this.props.params.id); - if (list.userId) { - const publicList = Lists.findOne({ userId: { $exists: false } }); - this.context.router.push(`/lists/${ publicList._id }`{:.language-javascript}); - } - } + this.props.signOut(this.props.jwt) + .then((success) => { + if (success) { + // if we are on a private list, we'll need to go to a public one + if (this.props.params.id) { + const list = Lists.findOne(this.props.params.id); + if (list.userId) { + const publicList = Lists.findOne({ userId: { $exists: false } }); + this.context.router.push(`/lists/${ publicList._id }`{:.language-javascript}); + } + } + } + }); } ... }; + +export default connect( + (state) => state, + (dispatch) => ({ + signOut: (jwt) => { + return dispatch(signOut(jwt)); + } + }) +)(App);

web/static/js/reducers/index.js

... SIGN_UP_FAILURE, + SIGN_OUT_REQUEST, + SIGN_OUT_SUCCESS, + SIGN_OUT_FAILURE, } from "../actions"; ... }); + + case SIGN_OUT_REQUEST: + return state; + case SIGN_OUT_SUCCESS: + return Object.assign({}, state, { + user: undefined, + jwt: undefined + }); + case SIGN_OUT_FAILURE: + return Object.assign({}, state, { + errors: action.errors + }); default:

Persisting Users

Unfortunately, if a user refreshes the page after signing up, they’ll lose their authenticated status. This means a user would have to sign-in every time they load the application.

This issue is caused by the fact that we’re saving the user and jwt objects exclusively in our in-memory application state. When we reload the page, that state is reset.

Thankfully, we can fix this issue fairly quickly.

In our signUp thunk, once we recieve a successful response from the server, we can store the user and jwt objects into local storage.


localStorage.setItem("user", JSON.stringify(res.user));
localStorage.setItem("jwt", res.jwt);

Similarly, when a user signs out we’ll clear these local storage entries:


localStorage.removeItem("user");
localStorage.removeItem("jwt");

Now we can popoulate our initialState with these user and jwt values, if they exist in local storage:


const user = localStorage.getItem("user");
const jwt = localStorage.getItem("jwt");

const initialState = {
  user: user ? JSON.parse(user) : user,
  jwt,
  ...

And now when a authenticated user refreshes the page, they’ll stay authenticated.

web/static/js/actions/index.js

... else { + localStorage.setItem("user", JSON.stringify(res.user)); + localStorage.setItem("jwt", res.jwt); dispatch(signUpSuccess(res.user, res.jwt)); ... else { + localStorage.removeItem("user"); + localStorage.removeItem("jwt"); dispatch(signOutSuccess());

web/static/js/reducers/index.js

... +const user = localStorage.getItem("user"); +const jwt = localStorage.getItem("jwt"); + const initialState = { - user: undefined, - jwt: undefined, + user: user ? JSON.parse(user) : user, + jwt, loading: false,

Sign In Front-end

Finally, we can continue the same pattern we’ve been following and implement our sign-in functionality.

We’ll start by copying over the SignInPage component from our Meteor application. Next, we’ll make three new actions: SIGN_IN_REQUEST, SIGN_IN_SUCCESS, and SIGN_IN_FAILURE.

In addition to our actions, we’ll make an asynchronous action creator that sends a POST request to /api/sessions to initiate a sign-in.

The reducers for our new actions will be identical to our sign-up reducers, so we’ll save some typing and re-use them:


case SIGN_IN_SUCCESS:
case SIGN_UP_SUCCESS:
  return Object.assign({}, state, {
    user: action.user,
    jwt: action.jwt
  });
...

Lastly, we can replace the call to Meteor.loginWithPassword with a call to our signIn helper. If this call is successful, we’ll redirect to /:


this.state.signIn(email, password)
  .then((success) => {
    if (success) {
      this.context.router.push('/');
    }
  });

Otherwise, we’ll render any errors we find in this.props.errors:


const errors = (this.props.errors || []).reduce((errors, error) => {
  return Object.assign(errors, error);
}, {});

And with those changes, a user can now sign up, log out, and sign into our application!

web/static/js/actions/index.js

... +export const SIGN_IN_REQUEST = "SIGN_IN_REQUEST"; +export const SIGN_IN_SUCCESS = "SIGN_IN_SUCCESS"; +export const SIGN_IN_FAILURE = "SIGN_IN_FAILURE"; + export function signUpRequest() { ... +export function signInRequest() { + return { type: SIGN_IN_REQUEST }; +} + +export function signInSuccess() { + return { type: SIGN_IN_SUCCESS }; +} + +export function signInFailure(errors) { + return { type: SIGN_IN_FAILURE, errors }; +} + export function signUp(email, password, password_confirm) { ... } + +export function signIn(email, password) { + return (dispatch) => { + dispatch(signInRequest()); + + let errors = []; + if (!email) { + errors.push({ email: "Email required" }); + } + if (!password) { + errors.push({ password: "Password required" }); + } + if (errors.length) { + return Promise.resolve(dispatch(signInFailure(errors))); + } + + return fetch("/api/sessions", { + method: "post", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }) + }) + .then((res) => res.json()) + .then((res) => { + if (res.errors) { + dispatch(signInFailure(res.errors)); + return false; + } + else { + localStorage.setItem("user", JSON.stringify(res.user)); + localStorage.setItem("jwt", res.jwt); + dispatch(signInSuccess(res.user, res.jwt)); + return true; + } + }); + } +}

web/static/js/pages/AuthPageSignIn.jsx

+import React from 'react'; +import AuthPage from './AuthPage.jsx'; +import { Link } from 'react-router'; +import { connect } from "react-redux"; +import { signIn } from "../actions"; + +class SignInPage extends React.Component { + constructor(props) { + super(props); + this.state = { + signIn: props.signIn + }; + this.onSubmit = this.onSubmit.bind(this); + } + + onSubmit(event) { + event.preventDefault(); + const email = this.refs.email.value; + const password = this.refs.password.value; + + this.state.signIn(email, password) + .then((success) => { + if (success) { + this.context.router.push('/'); + } + }); + } + + render() { + const errors = (this.props.errors || []).reduce((errors, error) => { + return Object.assign(errors, error); + }, {}); + const errorMessages = Object.keys(errors).map(key => errors[key]); + const errorClass = key => errors[key] && 'error'; + + const content = ( +
+

Sign In.

+

Signing in allows you to view private lists

+ <form onSubmit={this.onSubmit}> +
+ {errorMessages.map(msg => ( + <div className="list-item" key={msg}>{msg}
+ ))} +
+ <div className={`input-symbol ${errorClass('email')}`{:.language-javascript}}> + + + </div> + <div className={`input-symbol ${errorClass('password')}`{:.language-javascript}}> + + + </div> + + </form> + </div> + ); + + const link = Need an account? Join Now.</Link>; + + return <AuthPage content={content} link={link}/>; + } +} + +SignInPage.contextTypes = { + router: React.PropTypes.object, +}; + +export default connect( + (state) => { + return { + errors: state.errors + } + }, + (dispatch) => { + return { + signIn: (email, password) => { + return dispatch(signIn(email, password)); + } + }; + } +)(SignInPage);

web/static/js/reducers/index.js

... SIGN_OUT_FAILURE, + SIGN_IN_REQUEST, + SIGN_IN_SUCCESS, + SIGN_IN_FAILURE, } from "../actions"; ... switch (action.type) { + case SIGN_IN_REQUEST: case SIGN_UP_REQUEST: return state; + case SIGN_IN_SUCCESS: case SIGN_UP_SUCCESS: ... }); + case SIGN_IN_FAILURE: case SIGN_UP_FAILURE:

web/static/js/routes.jsx

... import AppContainer from './containers/AppContainer.jsx'; +import AuthPageSignIn from './pages/AuthPageSignIn.jsx'; import AuthPageJoin from './pages/AuthPageJoin.jsx'; ... <Route path="/" component={AppContainer}> + <Route path="signin" component={AuthPageSignIn}/> <Route path="join" component={AuthPageJoin}/>

Final Thoughts

Now that we’re getting more comfortable with React, it’s becoming more and more enjoyable to use.

The concept of a single application state, while undeniably weird at first, really simplifies a lot of complexities that can show up in more complicated applications. For example, having a single, canonical errors array that holds any error messages that might currently exist is amazing!

Coming from Blaze, the incredibly explicit data flow in a Redux-style application is comforting. It’s completely clear where each action is initiated and how it effects the application’s state.

Gone are the days of racking your brain trying to conceptualize a tree of reactive updates that brought your application into its current state.

Now that the authentication piece is finished (finally), next week we’ll move onto implementing the meat of our application!

My Kingdom for Transactions

On Sep 26, 2016 by Pete Corey

Recently, while reviewing a system I built for a client, I realized how strongly the “decision” to use MongoDB effected the architecture and structure of the system.

Using a better tool for the job would have significantly simplified the architecture of the solution and resulted in a more robust and reliable final product.

The System

The general idea of the system is that there are a set of “entities”. Each entity has a set of actions that can be done on them. For example, let’s imagine that we have a Child entity. Here are a few of the actions that can be taken on our Child:

  • put_to_bed
  • feed_breakfast
  • take_to_school
  • consume_free_time
  • etc…

Each action has two main parts. A validate method, and a do method.

Whenever you execute an action on an entity, the validate function will be called first. If this functions fails in any way, we’ll return the failure to the caller and stop all execution of the action before moving forward.

If validate doesn’t catch any problems, we’ll move on to the do function, which executes the meat of the action.

As an example, our put_to_bed action might look something like this:


put_to_bed: {
    validate({bedId}) {
        check(bedId, String);
    },
    do() {
        Child.update(this._id, {
            $set: { in_bed: true }
        });
        Bed.findOne(bedId).do("set_occupant", { childId: this._id });
    }
}

This seems all well and good. We check that bedId, or the ID of the Bed we’re putting the child into is a String. When we execute the action, we update the current Child document and set in_bed to true. Next, we find the bed we’re putting the child into and set its occupant to the child’s ID.

Broken State

But what happens if the bed already has an occupant?

Calling Bed.findOne(bedId).do(...) will trigger the set_occupant action to be triggered on the bed. It’s valiate function will be called, which might look something like this:


set_occupant: {
    validate({childId}) {
        check(childId, String);
        if (this.occupied) {
            throw new Meteor.Error("occupied");
        }
    },
    ...
}

The set_occupant action on the Bed will fail.

This leaves our system in a broken state. The child claims that it’s in bed (in_bed: true), but the bed is occupied by someone else.

Two Phase Commit Problems

The MongoDB documentation explains that this kind of multi-document transaction-style commit can be accomplished using two phase commits. The idea is that we keep track of our set of database changes as we carry out actions, and undo them if things go wrong.

The example two phase commit described in the documentation updates two documents within the same collection. Unfortunately our problem is a little more complex.

The example holds the IDs of the documents being updated in the transaction’s source and destination fields. Our transactions will update an arbitrary number of documents across any number of collections.

Instead of a single source and destination pair, we would need to maintain a list of affected documents, storing both the documents’ _id and collection:


{
    ...
    documents: [
        {
            collection: "children",
            _id: ...
        },
        {
            collection: "beds",
            _id: ...
        }
    ]
}

If something goes wrong in a two phase commit, any updates that have already been carried out need to be rolled back.

In the example scenario described in the MongoDB documentation, rollbacks are easy. All updates are simple increments ($inc: { balance: value }), and can be undone by decrementing by the same value ($inc: { balance: -value }).

But again, our scenario is more complicated.

Our actions are free to modify their respective documents in any way. This means that we have no natural way of undoing these modifications without either storing more additional data, or adding additional code.

One potential solution would be to store the original, pre-modification document along with the transaction’s _id in the pendingTransactions list:


{
    in_bed: true,
    ...
    pendingTransactions: [
        {
            _id: ...,
            document: {
                in_bed: false,
                ...,
                pendingTransactions: []
            }
        }
    ]
}

In the case of a roll-back, we could replace the entire document with this pre-modification document. The downside of this approach is that it drastically increases the size of our entity documents.

Another approach would be to create a new undo function to go along with each of our actions’ do functions. The undo function would simply undo any operations done by the do function.

This approach is very similar to the migration model used by Active Record and other migration frameworks. The obvious downsides of this approach are that we’re adding a huge amount of extra code to our application.

As my good friend Bret Lowrey says, “Code is like a war - the best code is one never written.”

My Kingdom For Transactions

It’s amazing how much architectural effort needs to be put into creating a functional, but awkward solution to this problem.

Interestingly, this kind of problem isn’t unique to this specific application. Most web applications do some kind of transactional updates against multiple documents across one or more collections.

Many developers just ignore the possibility of mid-transaction failures. If it happens, it happens. We’ll just clean up the database on an ad hoc basis.

And why not? When your alternatives are either doubling the size of your codebase or doubling the size of your database, a little manual labor starts to sound more appealing.

For this particular application, we decided that it would make more sense to invest in heavier upfront validation (via robust validate functions and simulations), rather than implementing a proper two phase commit system.

However, this entire mess could have been completely avoided if we had gone with a database that supported proper transactions.

My kingdom for transactions…

Phoenix Todos - Transition to Redux

On Sep 21, 2016 by Pete Corey

This post is written as a set of Literate Commits. The goal of this style is to show you how this program came together from beginning to end.

Each commit in the project is represented by a section of the article. Click each section's header to see the commit on Github, or check out the repository and follow along.

Front-end Join

Now that our back-end is shored up, we can turn our attention to the front-end side of the sign-up functionality.

The first thing we need to do is add the "join" route to our router:


<Route path="join" component={AuthPageJoin}/>

We can copy the majority of the AuthPageJoin component from the Meteor Todos project.

One small change we need to make is to rename all references to confirm to password_confirm to match what our User.changeset expects.

We’ll also need to refactor how we create the user’s account. Instead of using Meteor’s Accounts system, we’ll need to manually make a POST request to "/api/users", passing the user provided email, password, and password_confirm fields:


fetch("/api/users", {
  method: "post",
  headers: {
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    user: {
      email,
      password,
      password_confirm
    }
  })
})

If we receive any errors from the server, we can drop them directly into the errors field of our component’s state:


let errors = json.errors.reduce((errors, error) => {
  return Object.assign(errors, error);
});
this.setState({errors});

Otherwise, if everything went well we’ll recieve the newly signed up user’s JWT and user object in the json.jwt and json.user fields.

web/static/js/pages/AuthPage.jsx

+import React from 'react'; +import MobileMenu from '../components/MobileMenu.jsx'; + +// a common layout wrapper for auth pages +const AuthPage = ({ content, link }) => ( + <div className="page auth"> + <nav> + <MobileMenu/> + </nav> + <div className="content-scrollable"> + {content} + {link} + </div> + </div> +); + +AuthPage.propTypes = { + content: React.PropTypes.element, + link: React.PropTypes.element, +}; + +export default AuthPage;

web/static/js/pages/AuthPageJoin.jsx

+import React from 'react'; +import AuthPage from './AuthPage.jsx'; +import { Link } from 'react-router'; + +export default class JoinPage extends React.Component { + constructor(props) { + super(props); + this.state = { errors: {} }; + this.onSubmit = this.onSubmit.bind(this); + } + + onSubmit(event) { + event.preventDefault(); + const email = this.refs.email.value; + const password = this.refs.password.value; + const password_confirm = this.refs.password_confirm.value; + const errors = {}; + + if (!email) { + errors.email = 'Email required'; + } + if (!password) { + errors.password = 'Password required'; + } + if (password_confirm !== password) { + errors.password_confirm = 'Please confirm your password'; + } + + this.setState({ errors }); + if (Object.keys(errors).length) { + return; + } + + fetch("/api/users", { + method: "post", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: { + email, + password, + password_confirm + } + }) + }) + .then((res) => { + res + .json() + .then((json) => { + if (json.errors) { + let errors = json.errors.reduce((errors, error) => { + return Object.assign(errors, error); + }); + this.setState({errors}); + } + else { + // TODO: Save `json.user`{:.language-javascript} and `json.jwt`{:.language-javascript} to state + this.context.router.push('/'); + } + }); + }); + } + + render() { + const { errors } = this.state; + const errorMessages = Object.keys(errors).map(key => errors[key]); + const errorClass = key => errors[key] && 'error'; + + const content = ( + <div className="wrapper-auth"> + <h1 className="title-auth">Join.</h1> + <p className="subtitle-auth" >Joining allows you to make private lists</p> + <form onSubmit={this.onSubmit}> + <div className="list-errors"> + {errorMessages.map(msg => ( + <div className="list-item" key={msg}>{msg}</div> + ))} + </div> + <div className={`input-symbol ${errorClass('email')}`{:.language-javascript}}> + <input type="email" name="email" ref="email" placeholder="Your Email"/> + <span className="icon-email" title="Your Email"></span> + </div> + <div className={`input-symbol ${errorClass('password')}`{:.language-javascript}}> + <input type="password" name="password" ref="password" placeholder="Password"/> + <span className="icon-lock" title="Password"></span> + </div> + <div className={`input-symbol ${errorClass('password_confirm')}`{:.language-javascript}}> + <input type="password" name="password_confirm" ref="password_confirm" placeholder="Confirm Password"/> + <span className="icon-lock" title="Confirm Password"></span> + </div> + <button type="submit" className="btn-primary">Join Now</button> + </form> + </div> + ); + + const link = <Link to="/signin" className="link-auth-alt">Have an account? Sign in</Link>; + + return <AuthPage content={content} link={link}/>; + } +} + +JoinPage.contextTypes = { + router: React.PropTypes.object, +};

web/static/js/routes.jsx

... import AppContainer from './containers/AppContainer.jsx'; +import AuthPageJoin from './pages/AuthPageJoin.jsx'; import NotFoundPage from './pages/NotFoundPage.jsx'; ... <Route path="/" component={AppContainer}> + <Route path="join" component={AuthPageJoin}/> <Route path="*" component={NotFoundPage}/>

Enter Redux

Now that we’re receiving the newly signed up user and JWT from the server, we’ll need some way of updating our client-side application state.

Previously, we were doing this reactively with Meteor’s createContainer function. Since we’re not using Meteor, this is no longer an option.

Instead, we’ll switch to Redux for all of our state management needs.

The first thing we need to do to get started with Redux is to install our NPM dependencies:


npm install --save redux react-redux

Now we need to think about the “shape” of our application’s state. Thankfully, we don’t have to think too hard. The shape has been decided for us in the old AppContainer component. We’ll define this “state shape” in a new file, /web/static/js/reducers.js:


const initialState = {
  user: undefined,
  jwt: undefined,
  loading: false,
  connected: true,
  menuOpen: false,
  lists: []
};

Notice that we slipped in a new field: jwt. We’ll use this field to hold the JWT we get as a response from our server when signing in or signing up.

Now we need to define our reducer:


export default (state = initialState, action) => {
  switch(action.type) {
    default:
      return state;
  }
}

Since we don’t have any actions defined yet, our reducer is as simple as it gets.

Now that we have our reducer defined, we can create our store in app.js:


const store = createStore(reducers);

We’ll pass our store into our renderRoutes function so we can wrap our router in a <Provider> component:


<Provider store={store}>
  <Router ...>
    ...
  </Router>
</Provider>

Lastly, we’ll use subscribe to trigger a re-render any time our store changes.

Now that our Redux store is all wired up, we can pull the state initialization out of our AppContainer component and connect it to our Redux store:


const AppContainer = connect(state => state)(App);

Now our application state is passed from our store, into our AppContainer, and down into the App component.

package.json

"react-dom": "^15.3.1", + "react-redux": "^4.4.5", "react-router": "^2.7.0", + "redux": "^3.6.0", "uglify-js-brunch": ">= 1.0 < 1.8"

web/static/js/app.js

-import { render } from "react-dom"; +import ReactDOM from "react-dom"; +import reducers from "./reducers"; +import { createStore } from "redux"; import { renderRoutes } from "./routes.jsx"; -render(renderRoutes(), document.getElementById("app")); +const store = createStore(reducers); +const el = document.getElementById("app"); + +function render() { + ReactDOM.render(renderRoutes(store), el); +} + +render(); +store.subscribe(render);

web/static/js/containers/AppContainer.jsx

import App from '../layouts/App.jsx'; -import React from 'react'; +import { connect } from "react-redux"; -export default class AppContainer extends React.Component { - constructor(props) { - super(props); - this.state = { - user: undefined, - loading: false, - connected: true, - menuOpen: false, - lists: [] - }; - } +const AppContainer = connect(state => state)(App); - render() { - return (<App {...this.state} {...this.props}/>); - } -}; +export default AppContainer;

web/static/js/reducers/index.js

+const initialState = { + user: undefined, + jwt: undefined, + loading: false, + connected: true, + menuOpen: false, + lists: [] +}; + +export default (state = initialState, action) => { + switch (action.type) { + default: + return state; + } +}

web/static/js/routes.jsx

... import { Router, Route, browserHistory } from 'react-router'; +import { Provider } from "react-redux"; ... -export const renderRoutes = () => ( - <Router history={browserHistory}> - <Route path="/" component={AppContainer}> - <Route path="join" component={AuthPageJoin}/> - <Route path="*" component={NotFoundPage}/> - </Route> - </Router> +export const renderRoutes = (store) => ( + <Provider store={store}> + <Router history={browserHistory}> + <Route path="/" component={AppContainer}> + <Route path="join" component={AuthPageJoin}/> + <Route path="*" component={NotFoundPage}/> + </Route> + </Router> + </Provider> );

Sign Up Actions

Now that we’re using Redux, we’ll want to create actions that describe the sign-up process. Because sign-up is an asynchronous process, we’ll need three actions: SIGN_UP_REQUEST, SIGN_UP_SUCCESS, and SIGN_UP_FAILURE:


export function signUpRequest() {
  return { type: SIGN_UP_REQUEST };
}

export function signUpSuccess(user, jwt) {
  return { type: SIGN_UP_SUCCESS, user, jwt };
}

export function signUpFailure(errors) {
  return { type: SIGN_UP_FAILURE, errors };
}

We’ll also pull the fetch call that we’re using to hit our /api/users endpoint into a helper method called signUp. signUp will dispatch a SIGN_UP_REQUEST action, followed by either a SIGN_UP_SUCCESS or SIGN_UP_FAILURE, depending on the result of our fetch.

Because we’re using asynchronous actions, we’ll need to pull in the Redux Thunk middleware:


npm install --save redux-thunk

And then wire it into our store:


const store = createStore(
  reducers,
  applyMiddleware(thunkMiddleware)
);

Now that our actions are defined, we’ll need to create a matching set of reducers. The SIGN_UP_REQUEST reducer does nothing:


case SIGN_UP_REQUEST:
  return state;

The SIGN_UP_SUCCESS reducer stores the returned user and jwt object in our application state:


case SIGN_UP_SUCCESS:
  return Object.assign({}, state, {
    user: action.user,
    jwt: action.jwt
  });

And the SIGN_UP_FAILURE reducer stores the returned errors (you’ll also notice that we added an errors field to our initialState):


case SIGN_UP_FAILURE:
  return Object.assign({}, state, {
    errors: action.errors
  });

Great. Now we can wrap our JoinPage component in a Redux connect wrapper and pull in errors from our application state:


(state) => {
  return {
    errors: state.errors
  }
}

And create a helper that dispatches our signUp asynchronous action:


(dispatch) => {
  return {
    signUp: (email, password, password_confirm) => {
      return dispatch(signUp(email, password, password_confirm));
    }
  };
}

Now that JoinPage is subscribed to changes to our store, we’ll need to move the logic that transforms our errors into a usable form from its constructor into the render function, and replace the old fetch logic with a call to signUp.

After trying to sign up with these changes, we’ll see a runtime error in our UserMenu component. It’s expecting the newly signed-in user’s email address to be in user.emails[0].address. Changing this component to pull the address from user.email fixes the errors.

The sign-up functionality is complete!

package.json

"redux": "^3.6.0", + "redux-thunk": "^2.1.0", "uglify-js-brunch": ">= 1.0 < 1.8"

web/static/js/actions/index.js

+export const SIGN_UP_REQUEST = "SIGN_UP_REQUEST"; +export const SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS"; +export const SIGN_UP_FAILURE = "SIGN_UP_FAILURE"; + +export function signUpRequest() { + return { type: SIGN_UP_REQUEST }; +} + +export function signUpSuccess(user, jwt) { + return { type: SIGN_UP_SUCCESS, user, jwt }; +} + +export function signUpFailure(errors) { + return { type: SIGN_UP_FAILURE, errors }; +} + +export function signUp(email, password, password_confirm) { + return (dispatch) => { + dispatch(signUpRequest()); + return fetch("/api/users", { + method: "post", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: { + email, + password, + password_confirm + } + }) + }) + .then((res) => res.json()) + .then((res) => { + if (res.errors) { + dispatch(signUpFailure(res.errors)); + return false; + } + else { + dispatch(signUpSuccess(res.user, res.jwt)); + return true; + } + }); + } +}

web/static/js/app.js

... import reducers from "./reducers"; -import { createStore } from "redux"; +import { createStore, applyMiddleware } from "redux"; import { renderRoutes } from "./routes.jsx"; +import thunkMiddleware from "redux-thunk"; -const store = createStore(reducers); +const store = createStore( + reducers, + applyMiddleware(thunkMiddleware) +); const el = document.getElementById("app");

web/static/js/components/UserMenu.jsx

... const { user, logout } = this.props; - const email = user.emails[0].address; + const email = user.email; const emailLocalPart = email.substring(0, email.indexOf('@'));

web/static/js/pages/AuthPageJoin.jsx

... import { Link } from 'react-router'; +import { connect } from "react-redux"; +import { signUp } from "../actions"; -export default class JoinPage extends React.Component { +class JoinPage extends React.Component { constructor(props) { super(props); - this.state = { errors: {} }; + this.state = { + signUp: props.signUp + }; this.onSubmit = this.onSubmit.bind(this); ... - fetch("/api/users", { - method: "post", - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user: { - email, - password, - password_confirm + this.state.signUp(email, password, password_confirm) + .then((success) => { + if (success) { + this.context.router.push('/'); } - }) - }) - .then((res) => { - res - .json() - .then((json) => { - if (json.errors) { - let errors = json.errors.reduce((errors, error) => { - return Object.assign(errors, error); - }); - this.setState({errors}); - } - else { - // TODO: Save `json.user`{:.language-javascript} and `json.jwt`{:.language-javascript} to state - this.context.router.push('/'); - } - }); }); ... render() { - const { errors } = this.state; + const errors = (this.props.errors || []).reduce((errors, error) => { + return Object.assign(errors, error); + }, {}); const errorMessages = Object.keys(errors).map(key => errors[key]); ... }; + +export default connect( + (state) => { + return { + errors: state.errors + } + }, + (dispatch) => { + return { + signUp: (email, password, password_confirm) => { + return dispatch(signUp(email, password, password_confirm)); + } + }; + } +)(JoinPage);

web/static/js/reducers/index.js

+import { + SIGN_UP_REQUEST, + SIGN_UP_SUCCESS, + SIGN_UP_FAILURE, +} from "../actions"; + const initialState = { - user: undefined, - jwt: undefined, - loading: false, - connected: true, - menuOpen: false, - lists: [] + user: undefined, + jwt: undefined, + loading: false, + connected: true, + menuOpen: false, + lists: [], + errors: [] }; ... export default (state = initialState, action) => { - switch (action.type) { - default: - return state; - } + switch (action.type) { + case SIGN_UP_REQUEST: + return state; + case SIGN_UP_SUCCESS: + return Object.assign({}, state, { + user: action.user, + jwt: action.jwt + }); + case SIGN_UP_FAILURE: + return Object.assign({}, state, { + errors: action.errors + }); + default: + return state; + } }

Final Thoughts

Transitioning away from the react-meteor-data package’s createContainer-based container components to a more generalized stat management system like Redux can be a lot of work.

It’s easy to take for granted how simple Meteor makes things.

However, it’s arguable that transitioning to a more predictable state management system is worth the up-front effort. Spending time designing your application’s state, actions, and reducers will leave you with a much more maintainable and predictable system down the road.

Next week we’ll (finally) finish the authentication portion of this project by wiring up our sign-in and sign-out back-end functionality to our Redux-powered front-end.