App Walkthrough - Developer Experience


Having seen how our React-powered front end application is to look and behave, in this video we will take a look at some of the JavaScript code that powers the site.

Throughout the rest of this series we will cover each part of this project in much greater depth, but for the purposes of this video we will follow the login process from a high level, in order to understand how the major pieces of this puzzle fit together.

A Word On Boilerplate

Before we begin I want to point out that I based this project on the Corey House React Slingshot starter kit. You are, of course, entitled to your opinion of starter kits / React boilerplate, but hear me out on why I chose this one, and what it means for this project.

When I started writing this course there were, at the very least, tens of available boilerplates. My aim was not to learn the intricacies of WebPack, nor the myriad other libraries needed to get an ES6 project just to load up and work. The now popular Create React App was not yet a thing. At least, not publicly.

I went with the React Slingshot as it worked, and it contained a whole bunch of things that I wanted - React (of course), Redux, React Router, and importantly, a decent user guide and notes on how to put this thing into a production environment. You see, my aim for this was to use it as the foundations for the third iteration of CodeReviewVideos front end.

There's also a nice starter application which demonstrates one way of combining all these pieces together that results in a working web app.

Over the lifetime of working on this codebase, the original provided folder structure - particularly for src/ - has changed. Therefore, as we begin coding, we will remove the vast majority of the implementation from the boilerplate, and roll our own. Really, the only interesting part for me is the shortcut to a working Babel / Webpack / JS build process.

Walking Through The App

With the boilerplate discussion out of the way, let's start proper by looking at some code.

The boilerplate provides a key piece to kickstarting this puzzle: tying the JS and HTML together.

You are free to dive into the webpack.config.dev.js and webpack.config.prod.js files yourself. We have no need to modify either at this stage.

As part of the config, it is specified that our HTML template will be from index.ejs, and our JS will come from index.js:

<!-- /src/index.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Symfony 3 + React Registration Example</title>

  <script type="text/javascript" src="https://js.stripe.com/v2/"></script>

  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

React needs a div to work with. In our case, this will the the div with the id of app.

We add in a reference to the Bootstrap's styling - but of course you are free to use whatever you like here. A better way (in my opinion) would be to build your CSS into your own dist bundle, but that's not really something we will cover in this tutorial.

Tying to this is the index.js file:

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Router, browserHistory} from 'react-router';
import routes from './routes';
import configureStore from './store/configureStore';
import './styles/styles.scss'; // Yep, that's right. You can import SASS/CSS files too! Webpack will run the associated loader and plug this into the page.
import {syncHistoryWithStore} from 'react-router-redux';
import DevTools from './containers/DevTools';
import "whatwg-fetch";
require('./favicon.ico'); // Tell webpack to load favicon.ico

const store = configureStore();

const devTools = () => process.env.NODE_ENV !== 'production' ? <DevTools /> : '';

// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store);

render(
  <Provider store={store}>
    <div>
      <Router history={history} routes={routes} />
      {devTools()}
    </div>
  </Provider>, document.getElementById('app')
);

There are lots of things happening here. Fortunately, we don't need to concern ourselves with the vast majority of this at this stage.

What is important is that our div with the id of app is used as the container that React will display its contents into. The Provider component is key to enabling Redux in our application, and the store is what will hold our application's state.

How these pieces fit together requires a deeper dive than this video has in scope, so for the moment, we will brush over these topics and continue. The links provided above do go deeper should you wish to do so at this point. My opinion is that these concepts begin to make more sense once you have seen what they enable.

Wrapped in the Provider is the React Router setup - which takes in our routes, more on this momentarily.

Also, we only want the DevTools component adding if we are in any other environment than production, so the outcome of the call to devTools() will either be to add the DevTools component, or this will be ignored in prod. Our DevTools implementation is what enables seeing the state and action inside our browser's console, and also enables a handy DockMonitor, which you may prefer.

Adding Our Own Routes

Most of the code so far has been your typical boilerplate - it's going to be the same whether you're working on an Accounting system, or a CRM, some custom sales tool, or any other typical web app.

Where things start to differ is in the routing.

In our case, our finished routing file will look as follows:

// /src/routes.js

import React from 'react';
import {Route, IndexRoute} from 'react-router';
import {routerActions} from 'react-router-redux';
import {UserAuthWrapper} from 'redux-auth-wrapper';

import App from './containers/App';
import AboutPage from './components/AboutPage.react.js';
import HomePage from './components/HomePage.react';
import LoginPage from './containers/LoginPage';
import LogoutPage from './containers/LogoutPage';
import ProfileContainer from './containers/ProfileContainer';
import RegistrationContainer from './containers/RegistrationContainer';
import NotFoundPage from './components/NotFoundPage.react.js';

// Redirects to /login by default
const userIsAuthenticated = UserAuthWrapper({ // eslint-disable-line babel/new-cap
  authSelector: (state) => state.auth, // how to get the user state
  predicate: (user) => user.isAuthenticated, // function to run against the user state to determine is authenticated
  redirectAction: routerActions.replace, // the redux action to dispatch for redirect
  wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check,
});

export default (
  <Route path="/" component={App}>
    <IndexRoute component={HomePage}/>
    <Route path="about" component={AboutPage}/>
    <Route path="login" component={LoginPage}/>
    <Route path="logout" component={LogoutPage}/>
    <Route path="profile" component={userIsAuthenticated(ProfileContainer)}/>
    <Route path="register" component={RegistrationContainer}/>
    <Route path="*" component={NotFoundPage}/>
  </Route>
);

This file dictates what URLs will be available in our app, and when accessed, what component to show.

At this point, I would advise you follow the React Router tutorial if any of this looks confusing to you. We will go deeper into this for each of our components, but for now, just be aware that this exists.

As we are following the login flow, we can see that when the path of /login is hit, our application will load the App container (think: smart component), and also the LoginPage container.

Containers and Components

When working with React, you will frequently come across the term 'component'.

A component is a way of splitting up your user interface into smaller, often reusable chunks. This brings with it the benefit of making each piece of your user interface more easy to understand, and test.

There is a concept on top of this around 'smart' vs 'dumb' components.

Somewhere along the way, someone took offence with the term 'dumb' component and offered up an alternative vernacular of 'component' and 'container'.

The long and short of this is that a 'component' is unaware of the world outside itself. If it needs to do something, it should be told how to do it. An example of this would be the login form. It shouldn't know how to log in, it should instead be provided with a function (via props) that it should call whenever the user clicks the 'submit' button.

A 'container' is still a 'component'. Only, a container is aware of a bigger picture - but not necessarily the whole picture. In the login example, the LoginPage container defines the function to run when the LoginForm component submit event is triggered. Think of the 'container' components as the glue.

LoginPage and LoginForm

Knowing this, we can take a look at the LoginPage:

// /src/containers/LoginPage.js

import React, {PropTypes, Component} from 'react';
import {connect} from 'react-redux';
import {withRouter} from 'react-router';
import LoginForm from '../components/LoginForm.react';
import * as types from '../constants/ActionTypes';
import '../styles/login-page.css';

class LoginPage extends Component {

  componentWillReceiveProps(newProps) {
    if (newProps.pageState.auth.isAuthenticated) {
      this.props.router.replace('/');
    }
  }

  doLogin(formData) {
    this.props.dispatch({
      type: types.LOGIN__REQUESTED,
      payload: {
        username: formData.username,
        password: formData.password
      }
    });
  }

  render() {
    return (
      <div>
        <LoginForm
          onSubmit={this.doLogin.bind(this)}
          isSubmitting={!!this.props.pageState.request.sendingRequest}
        />
      </div>
    );
  }
}

LoginPage.propTypes = {
  dispatch: PropTypes.func.isRequired,
  pageState: PropTypes.object.isRequired,
  router: PropTypes.object.isRequired
};

const mapStateToProps = (state) => {
  return {
    pageState: state
  };
};

export default connect(
  mapStateToProps
)(withRouter(LoginPage));

The LoginPage component is our 'container'. It defines the doLogin function - which when called will dispatch a Redux action containing the user supplied data from the LoginForm component.

The LoginPage component is also aware of whether any HTTP requests are currently in progress, and passes that information into our LoginForm also - useful should we wish to display a loading spinner.

Whilst this component is 'smart', it is still doing as little as possible.

The LoginForm by comparison is simpler:

// /src/components/LoginForm.react.js

import React  from 'react';
import {Field, reduxForm} from 'redux-form';
import FormField from './FormField';
import * as FORMS from '../constants/Forms';

const LoginForm = (props) => {

  const {handleSubmit} = props;

  return (
      <form className="form-signin" onSubmit={handleSubmit(props.onSubmit)}>

        <h2 className="form-signin-heading">Welcome back</h2>

        <Field component={FormField}
               name="username"
               id="username"
               type="text"
               placeholder="Username or Email Address"
               required="required"
        />

        <Field component={FormField}
               name="password"
               id="password"
               type="password"
               placeholder="Password"
               required="required"
        />

        <button className="btn btn-lg btn-primary btn-block"
                disabled={props.pristine || props.isSubmitting}
                type="submit">
          {props.isSubmitting ? <i className="fa fa-spin fa-spinner" /> : null} Log in
        </button>

      </form>
  );
};

LoginForm.propTypes = {
  handleSubmit: React.PropTypes.func.isRequired,
  onSubmit: React.PropTypes.func.isRequired,
  pristine: React.PropTypes.bool.isRequired,
  isSubmitting: React.PropTypes.bool.isRequired
};

// Decorate the form component
export default reduxForm({
  form: FORMS.LOGIN // a unique name for this form
})(LoginForm);

We're using Redux Form here to make working with forms that bit easier. We expect to be given a function to call when the form is submitted, and if the form is submitting then we want to display a little spinner icon, and make sure the button is marked as disabled. Aside from that, not a great deal to see here.

Starting The Saga

Where things become more interesting is in what happens once an action is dispatched.

A big problem once you get past the basic tutorials for Redux is what happens when functions aren't pure. In other words, if we can run a function many times with the same parameters - but get different outcomes, such as if a call to your Symfony 3 API fails because your server is offline - then how do we handle this without creating one giant headache?

Enter Redux Saga.

Remember back to the LoginPage container, we called dispatch and passed in an object (a Redux Standard Action) which contained the information provided from the login form:

// /src/containers/LoginPage.js

  doLogin(formData) {
    this.props.dispatch({
      type: types.LOGIN__REQUESTED,
      payload: {
        username: formData.username,
        password: formData.password
      }
    });
  }

Using Redux Sagas we can define a function to 'watch' for actions of a given type, and then do something appropriate:

/src/sagas/auth.saga.js

export function *watchLogin() {
  yield* takeLatest(types.LOGIN__REQUESTED, doLogin);
}

We must also setup Redux Sagas as part of the configuration for our store. This is not shown here, but will be covered in future videos as appropriate. For now, just be aware that a central rootSaga maintains an array of all the watch functions in our application.

In this instance, when an action is dispatched with the type of types.LOGIN__REQUESTED, then the doLogin generator function will be called:

/src/sagas/auth.saga.js

export function *doLogin(action) {
  try {

    const {username, password} = action.payload;

    yield put({
      type: types.REQUEST__STARTED,
      payload: {
        requestFrom: REQUESTS.REQUEST__DOLOGIN__SAGA
      }
    });

    const responseBody = yield call(api.login, username, password);

    if (responseBody.token === undefined) {
      throw new Error(MESSAGES.UNABLE_TO_FIND_TOKEN_IN_LOGIN_RESPONSE);
    }

    yield put({
      type: types.LOGIN__SUCCEEDED,
      payload: {
        idToken: responseBody.token
      }
    });

  } catch (e) {

    yield put({
      type: types.LOGIN__FAILED,
      payload: {
        message: e.message,
        statusCode: e.statusCode
      }
    });

  } finally {
    yield put({
      type: types.REQUEST__FINISHED,
      payload: {
        requestFrom: REQUESTS.REQUEST__DOLOGIN__SAGA
      }
    });
  }
}

Again, we will dive deeper into this function in the login videos.

Essentially we grab the username and password information from the action's payload, then immediately dispatch a REQUEST__STARTED event. Should we wish to, we could listen for these actions and then use them to determine if a request is currently in progress. If it is, maybe we wish to display a loading spinner, or disable a button, or anything really - that is the beauty of this setup.

Next, we yield a call to the API. This pauses the generator function whilst the request takes place, continuing only once a response is received. My initial concern when first reading about this was that I wasn't sure how, or when, I should restart the function. Thankfully, we needn't concern ourselves with this. Redux Saga takes care of it for us.

If the request is successful, we dispatch another action (via put) to say the LOGIN__SUCCEEDED.

If the request was not successful, an error may be thrown which dispatches a different kind of action - the LOGIN__FAILED action type.

Both of these actions are completely controlled by us. That's cool, as it means we can literally do whatever we want.

It also means this function itself needn't worry about the implementation of handling either scenario - we can define two different functions to handle the individual cases, keeping our code a lot more modular, and easy to reason about.

Finally, or should I say, finally, we always want to say the REQUEST__FINISHED. This ensures no requests are left in limbo, leaving a loading spinner spinning forever... hey, it happened to me quite a lot during initial development ;)

Keep On Generating

We aren't done yet.

Well, we might be if we hit the LOGIN__FAILED path. But let's pretend we didn't.

What happens next is cool. We have dispatched this new action of LOGIN__SUCCEEDED, so what handles this?

Another watch function, of course.

The pattern is the same, the implementation differs:

/src/sagas/auth.saga.js

export function *doLoginSucceeded(action) {

  const {idToken} = action.payload;

  if (idToken === undefined) {
    throw new Error(MESSAGES.UNABLE_TO_FIND_TOKEN_IN_ACTION);
  }

  yield call(storage.save, 'id_token', idToken);

  const {userId, username} = yield call(jwtDecode, idToken); // pull out the user data from the JWT

  if (userId === undefined) {
    throw new Error(MESSAGES.UNABLE_TO_FIND_USER_ID);
  }

  yield call(storage.save, 'profile', JSON.stringify({userId, username}));

  yield put({
    type: types.LOGIN__COMPLETED,
    payload: {
      userId,
      username
    }
  });
}

export function *watchLoginSucceeded() {
  yield* takeLatest(types.LOGIN__SUCCEEDED, doLoginSucceeded);
}

We want to save off some of this information to some form of local storage. Now, this may indeed be localStorage. In my case, that's what I use. I intentionally made this storage object easy to change - in case localStorage is not sufficient for your use case. It's only three functions, and I will explain how to change them as we go through.

Assuming everything saves, we then dispatch the LOGIN__COMPLETED action.

At this point this action should hit the authReducer:

// /src/reducers/authReducer.js

import * as types from '../constants/ActionTypes';
import persistentState from '../utils/localStorage';

export default function auth(state = {
  isAuthenticated: isAuthenticated(),
  userId: getUserId(),
  username: getUsername()
}, action) {

  switch (action.type) {

    case types.LOGIN__COMPLETED: {
      const {userId, username} = action.payload;
      return Object.assign({}, state, {
        isAuthenticated: true,
        userId,
        username
      });
    }

    case types.LOGIN__FAILED: {
      return Object.assign({}, state, {
        isAuthenticated: false,
        errorMessage: action.payload.message
      });
    }

    case types.LOGOUT__SUCCESS: {
      return Object.assign({}, {
        isAuthenticated: false
      });
    }

    default: {
      return state;
    }
  }
}

function isAuthenticated() {
  return !!persistentState.getItem('id_token');
}

function getUserId() {
  try {

    let profile = persistentState.getItem('profile') || {};
    profile = JSON.parse(profile);

    return profile.userId;

  } catch (e) {
    return undefined;
  }
}

function getUsername() {
  try {

    let profile = persistentState.getItem('profile');
    profile = JSON.parse(profile);

    return profile.username;

  } catch (e) {
    return undefined;
  }
}

There is a little funky-ness to this. The initial state involves impure functions. Heresy. I'm open to a better implementation here, but having wracked my brain I cannot think of an implemenation here that fits the paradigm. Do shout up if you know of a better solution.

Anyway, aside from this funk, the thing of most interest to us here is in the outcome of running the reducer with this action:

    case types.LOGIN__COMPLETED: {
      const {userId, username} = action.payload;
      return Object.assign({}, state, {
        isAuthenticated: true,
        userId,
        username
      });
    }

Redux defines that we are going to have a state. A single source of truth for our application's current setup.

However this state is loaded - whether it be a 'hardcoded' object, or some bastardisation of this where we may have saved off to localStorage to keep our users 'logged in' when they close the browser, we must start from an initial state.

From there, we apply actions in sequence to end up with the current state of the application.

In this case, for our auth state, we will now be setting the values to be:

{
  isAuthenticated: true,
  userId,
  username
}

Again, we are using some ES6 syntax here to set a key of userId, and a value of whatever the current value of the userId variable may be. Pretend it is 6, and our username is 'fred', we end up with the next state for auth being:

{
  isAuthenticated: true,
  userId: 6,
  username: 'fred'
}

And here's the really interesting part - coming from a PHP background, at least:

What happens next is not pre-determined.

Sure, we have defined a whole structure of code to handle all the expected - and some unexpected - outcomes.

But what the next action will be - we just don't know. That's up to the user.

If you come from a largely programatic background, is in you are used to telling the computer exactly what it could do with a lifetime's supply of cholcate - then this is a whole new way of working.

Hopefully as we continue on through this course, you will come to agree.

Code For This Course

Get the code for this course.

Episodes