Development Environment Setup


In the previous two videos we have seen firstly what the finished app would look like from the perspective of an end user, and secondly how a typical workflow might look from the perspective of you, the developer.

In this video we are going to start by setting up our project.

We've already covered how we will be using the React Slingshot as our starting point.

You are free to use any starter kit you like - Create React App is perhaps the most 'official' way, but this wasn't a 'thing' when I put this tutorial series together. The reasoning behind using a starter kit is to skip over the huge amount of boilerplate required to kick start a React project.

Anyway, to kick start this project we first need to clone the React Slingshot. You can find full instructions on this process in the docs, but for simplicity, here is my process:

cd /some/dir/you/do/dev/in
git clone https://github.com/coryhouse/react-slingshot.git react-registration
cd react-registration

Ok, so the next official step is to run npm run setup, but here's the thing:

JavaScript, being JavaScript, has recently improved upon the use of npm for installing. The new cool way to install is via yarn.

I want to say that I'm not one for encouraging the use of new and shiny, but this is a React / Redux / Redux Saga tutorial, so I guess that's not quite true :)

Anyway, yarn install is orders of magnitude faster to run the install process over your standard npm install, so before running npm run set, I would advise you to make a single change to the package.json file:

// package.json, line 10
    "setup": "node tools/setup/setupMessage.js && yarn install && node tools/setup/setup.js",

Save, and exit.

Now, when you run the npm run setup command, you should find the installation process is much faster than a typical node / JavaScript project setup time. It still takes a while, but not ages. One day these JavaScript guys may get a dependency manager as good as Composer ;)

After running through this process you should find the the .git directory has been removed. This is fine. This .git directory was for the React Slingshot project, not for your own project. It's highly likely that you don't want all their project commits polluting your codebase, so having to start fresh is not such a big deal:

git init
git add .
git commit -m "init"

And away we go.

With all that done, we are good to start up the development server and have a nosey around the demo application:

npm start -s

Doing this will - after a short while - open up a new browser, or a new tab in your most recently active browser window, showing you the React Slingshot demo app.

Have a poke around, see what each things does, and then also have a look at the existing code for the project. Even if this is your very first React application, it can be extremely useful to see how the bits and pieces of a working app - no matter how big or small - all fit together.

Now, the next recommended step in the React Slingshot documentation is to run npm run remove-demo, but I'm not going to advise that you do this. Why not?

Well, this goes ahead and deletes a whole bunch of useful stuff. In my opinion, it is easier to work from existing files - changing as needed - rather than being left with a blank slate and having to figure it all out yourself. Your opinion may differ, and that's ok too.

Beyond The Starting Point

As we will be using Redux Saga, rather than the supplied Redux Thunk, we now need to go ahead and remove Redux Thunk from the project. In the process, I am also going to add in a few other things we will need as we go through this tutorial:

yarn remove redux-thunk
yarn add classnames lodash redux-form redux-saga whatwg-fetch
yarn add redux-logger babel-es6-polyfill --dev

This isn't the whole list of things we will need, but these are the core pieces to get started.

Firstly, as mentioned we remove Redux Thunk, which we won't be using.

  • classnames - is a really useful utility library for creating CSS classname strings in a programatic fashion
  • lodash - is a more performant alternative to underscore, full of useful utility functions
  • redux-form - hooks up your form state with your redux store
  • redux-saga - handles side-effects, e.g. calls to your back end API, in a Redux friendly way
  • whatwg-fetch - allows us to use fetch, which is the future of XMLHttpRequests

All of these are key to way our app will work.

However, we must also add in the babel-es6-polyfill to our development environment, or Redux Saga will not work properly. More on this shortly.

And lastly we want to add in the redux-logger, which will give us something incredibly useful:

  • the previous state
  • the current action
  • and the next state after applying this action

All displayed for us inside our browser's developer tools > console. This is one of the most helpful tools for debugging I have ever used. It's unlikely that you want to display this information in your production environment, hence only adding for the development environment.

That said, simply yarn add'ing all these packages doesn't automatically make them work. Alas, we still have a bunch of config to do to set them up.

Removing Redux Thunk

The next thing we need to do is to remove any references to Redux Thunk. If we don't, then our project will simply fail to load. Bad times.

Thankfully, there's only one file we must edit: /src/store/configureStore.js.

The lines to remove here are 3, 12, and 30.

That's enough to have removed Redux Thunk from the project in a working capacity. There are a couple of left over references (in comments) in some of the React Slingshot files, but these will be removed as we clear up, so no need to worry about them at present.

Adding Redux Logger

Adding in the Redux Logger middleware is largely a case of following the documentation.

The key point to note is that the Logger middleware must always be the last middleware we call in our middleware stack. This is because if it isn't, it may end up logging the promises themselves, along with any actions that we did want to see.

Whilst this sounds complicated, it simply means that in our middlewares array, the Logger entry must always be the last entry. This is easier to see by way of some code:

// src/store/configureStore.js

import {createStore, compose, applyMiddleware} from 'redux';
import createLogger from 'redux-logger';
import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
import rootReducer from '../reducers';

const loggerMiddleware = createLogger();

function configureStoreProd(initialState) {
  const middlewares = [
    // you could add in the loggerMiddleware here if you wanted
    // to log info out in prod
  ];

  return createStore(rootReducer, initialState, compose(
    applyMiddleware(...middlewares)
    )
  );
}

function configureStoreDev(initialState) {
  const middlewares = [
    // Add other middleware on this line...

    // Redux middleware that spits an error on you when you try to mutate your state either inside a dispatch or between dispatches.
    reduxImmutableStateInvariant(),

    loggerMiddleware
  ];

  const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools
  const store = createStore(rootReducer, initialState, composeEnhancers(
    applyMiddleware(...middlewares)
    )
  );

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers').default; // eslint-disable-line global-require
      store.replaceReducer(nextReducer);
    });
  }

  return store;
}

const configureStore = process.env.NODE_ENV === 'production' ? configureStoreProd : configureStoreDev;

export default configureStore;

Our involvement here is to import the createLogger function from redux-logger. Then, we want to invoke the function and store it in to the loggerMiddleware variable (const loggerMiddleware = createLogger();), and then add this variable as the last item into the middlewares array for the configureStoreDev function only.

All the rest of this file came as part of the boilerplate - hopefully revealing why I tend to rely on boilerplates, rather than writing my own.

Adding Redux Saga

Adding in Redux Saga is very similar to the way we added in Redux Logger.

However, Redux Saga will expect to be given an array of sagas, which we don't yet have. Not to worry though, an empty array will do just fine.

Let's start by setting up our rootSaga generator function, which will yield (think: return) an array of our active watch* functions. Again, we don't have any of these currently, so don't worry about this. In fact, I'd go as far as to not worry about any of this saga / generator stuff at this stage, as I found the entire concept quite complicated when simply reading about it. Once I saw it in action, and played with it, the theory started to make a whole lot more sense.

For now, create a new directory in src, called sagas, with a single file called index.js inside it:

src/sagas/index.js

export default function *rootSaga() {
  yield [

  ];
}

Once we have the rootSaga available, we can go ahead and setup Redux Saga for our app:

// src/store/configureStore.js

import {createStore, compose, applyMiddleware} from 'redux';
import createLogger from 'redux-logger';
import createSagaMiddleware, {END} from 'redux-saga';
import sagas from '../sagas';
import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
import rootReducer from '../reducers';

const loggerMiddleware = createLogger();
const sagaMiddleware = createSagaMiddleware();

function configureStoreProd(initialState) {
  const middlewares = [
    // Add other middleware on this line...

    sagaMiddleware
  ];

  const store = createStore(rootReducer, initialState, compose(
    applyMiddleware(...middlewares)
    )
  );

  sagaMiddleware.run(sagas);
  store.close = () => store.dispatch(END);

  return store;
}

function configureStoreDev(initialState) {
  const middlewares = [
    // Add other middleware on this line...

    // Redux middleware that spits an error on you when you try to mutate your state either inside a dispatch or between dispatches.
    reduxImmutableStateInvariant(),

    sagaMiddleware,
    loggerMiddleware
  ];

  const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools
  const store = createStore(rootReducer, initialState, composeEnhancers(
    applyMiddleware(...middlewares)
    )
  );

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers').default; // eslint-disable-line global-require
      store.replaceReducer(nextReducer);
    });
  }

  sagaMiddleware.run(sagas);
  store.close = () => store.dispatch(END);

  return store;
}

const configureStore = process.env.NODE_ENV === 'production' ? configureStoreProd : configureStoreDev;

export default configureStore;

The key points here are that we import the createSagaMiddleware function, and the END constant from redux-saga.

We also import sagas from the ../sagas directory. By importing the directory as a whole, this tells JavaScript to look for an index.js file in that directory, and import that. As that file contains our rootSaga, which will in turn reference every other saga we created, this means we won't keep be needing to update this configureStore file for every new saga we create.

We call the createSagaMiddleware function and store the outcome into the sagaMiddleware variable. This is then added to both the prod and dev middleware arrays. Remember, this needs to come before the loggerMiddleware.

To actually make this work, we must tell the sagaMiddleware to run with our sagas.

Also, to tidy up when our app encounters some bad state, a function called close is added to the Redux store, which when called, will dispatch the END constant. This allows us to break any infinite while loops that may hang over if things go wrong. Who am I kidding? When things go wrong.

At this stage, if we were to try and load our app, it would die with an error of regeneratorRuntime is not defined. Thankfully, fixing this rather obscure message is fairly straightforward.

Remember we added in the babel-es6-polyfill as a dev dependency? Well, now we need to use it by import'ing it into our src/components/App.js file:

// src/components/App.js

import React, { PropTypes } from 'react';
import { Link, IndexLink } from 'react-router';
import "babel-es6-polyfill"; // <--- this is the important line

class App extends React.Component {
  render() {
    return (
      <div>
        blah blah
      </div>
    );
  }
}

App.propTypes = {
  children: PropTypes.element
};

export default App;

And now our app should be loading just fine, and if you notice, we should also see the output from Redux Logger in your browser's developer console.

Adding Bootstrap

Of course our project wouldn't be complete without the web's most overused styling framework.

There's a bunch of ways to add Bootstrap to your project. I'm opting for the most basic - adding the externally hosted stylesheet to our index.ejs file:

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

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta charset="utf-8"/>
  <title>React Login and Registration Example</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

I've removed a bunch of comments and other fluff from this output for the sake of brevity. I've also gone ahead and changed the title tag to something more appropriate.

And whilst we aren't directly going to be using Bootstrap as of yet, we now have all the pieces in place to start putting together our (soon to be well-styled) app :)

Code For This Course

Get the code for this course.

Episodes