In this post, I’m going to step through getting a basic starter project going in ReactJS.
TL;DR
$ git clone https://github.com/bsdavidson/react-starter
Creating a React app from scratch, with tests
You can see where we will end up on my Github repo for the tutorial.
This React starter is my own opinion of a basic starting environment for creating React apps. Facebook recommends using their utility create-react-app. While nice to have something that gets all the boring setup out of the way, it is still a good idea to go through the process so you understand what things are doing and how they work in case you need to make your own modifications.
Prerequisites
Familiarity with JavaScript and the command line will be useful. You will also want to have NodeJS installed
Initial Setup
To start, we need to create a folder where everything lives
$ mkdir react-app
$ cd react-app
Now time to create a few more files and folders. For the purposes of this demo, let’s assume that we’ll need a stylesheet and some static assets like images.
$ mkdir assets
$ touch styles.scss
$ touch index.html
Notice we haven’t added a .js
file yet, since our JavaScript code will
live in the components we create later.
The index.html
will be our initial starting point when loading our page. It
won’t contain much content, but will act as a place to bootstrap the rest of
our React app.
Now it’s time to create the files and folders where the business end of our app lives. I like to keep the source files and test files isolated in their own folders so we don’t clutter things up. Let’s go ahead and create some folders and stub files, to see how that will look.
$ mkdir src
$ mkdir test
$ touch src/app.jsx
$ touch test/test_app.jsx
$ touch style.scss
Ok, so what have we got here? Looks like we have a src
folder for the app
logic and a test
folder for tests. Not bad so far. Also, since we are going
to do test driven development (TDD) we will need to have our test layout setup
at the same time.
At this point, we need to get all our supporting cast members installed. Because this is JavaScript, you have a lot of choices for these components and the combinations are pretty varied. In this tutorial, I have chosen a combination that I think works really well and which I actually use in my own apps. I won’t go into detail the merits of each one, but here is a list of what we will be using here along with their relevant links.
Components for writing our code:
- Webpack: Collects and manages all our code so it can be easily packaged for distribution.
- Sass: For writing our stylesheets.
For writing our tests:
- Karma: Runs our tests in a real or headless browser.
- Mocha for our test framework.
- Chaijs as the assertion library
- Enzyme testing utilities (instead of the built in react-test-utils). Makes writing tests far less painful.
Setup NPM and friends
First thing we need to do is initialize our NPM package. This creates a
package.json
file as well as a node_modules
folder where all our
dependencies (mentioned above) live. The package.json
file enumerates the
packages your app uses.
To initialize NPM:
$ npm init
It will ask you a number of questions. Here is what I put:
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See npm help json for definitive documentation on these fields
and exactly what they do.
Use npm install <pkg> --save afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (reactTut) react-app
version: (1.0.0)
description: Awesome Hello World app!
entry point: (index.js) src/app.jsx
test command:
git repository:
keywords:
author: Brian Davidson
license: (ISC)
Just answer them the best you can, and skip the ones you can’t. Don’t worry,
we can edit them all later in package.json
but make note of the entry point.
Notice I put the path to our newly created src/app.jsx
file.
If you look inside the package.json now, it’s pretty basic and doesn’t contain any information about our apps dependencies. That’s because we haven’t told it about them yet.
When installing dependencies, you need to know if something is used for just for development or will it also be needed for production as well?
In this starter, we won’t have any production dependencies because webpack will
take everything and package it up for our final distribution copy. This means
that when we run npm run dist
we will have a fully independent page that can
be dropped into any web server.
To get started, lets install our first dev-dependency.
$ npm install --save-dev react
Awesome. If you look at your package.json
, you’ll have a new section called
devDependencies
with one item:
...
"devDependencies": {
"react": "^15.1.0"
},
...
The --save-dev
option in the command we entered automatically added the entry
in the file.
At this point, we have a devDependencies section in our package.json
file
for defining what is going into our app, but it is not yet complete. Let’s go
ahead and install the rest of our dependencies now.
$ npm i --save-dev babel babel-core babel-loader babel-preset-es2015 babel-preset-react chai css-loader enzyme eslint eslint-plugin-react extract-text-webpack-plugin file-loader html-loader install json-loader karma karma-chrome-launcher karma-coverage karma-mocha karma-mocha-reporter karma-notify-reporter karma-sourcemap-loader karma-webpack mocha node-sass npm react react-addons-test-utils react-dom sass-loader style-loader url-loader webpack webpack-dev-server
After all that is done, you should have an entry in your package.json file that looks like this:
"devDependencies": {
"babel": "^6.5.2",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"chai": "^3.5.0",
"css-loader": "^0.23.1",
"enzyme": "^2.3.0",
"eslint": "^2.13.1",
"eslint-plugin-react": "^5.2.2",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"html-loader": "^0.4.3",
"install": "^0.8.1",
"json-loader": "^0.5.4",
"karma": "^1.1.0",
"karma-chrome-launcher": "^1.0.1",
"karma-coverage": "^1.0.0",
"karma-mocha": "^1.1.1",
"karma-mocha-reporter": "^2.0.4",
"karma-notify-reporter": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.7.0",
"mocha": "^2.5.3",
"node-sass": "^3.8.0",
"npm": "^3.10.2",
"react": "^15.1.0",
"react-addons-test-utils": "^15.1.0",
"react-dom": "^15.1.0",
"sass-loader": "^4.0.0",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
}
One more package.json section we are going to edit is scripts
. Scripts will
help us by making our commonly run commands easier to type. Find the scripts
section in package.json
and replace it with what I have below:
"scripts": {
"build": "webpack",
"dist": "npm run build && rm -rf dist && mkdir dist && cp -Rv index.html assets bundle.css bundle.js dist/",
"start": "webpack-dev-server",
"test": "karma start karma.config.js --single-run",
"test-watch": "karma start karma.config.js"
}
For each of the sections listed above, you will be able to run them using npm run xxxx
.
For example, to start our development web-server, you just need to type:
$ npm run start
These won’t do anything just yet, so, let’s continue…
Now that our app has access to all the modules we need to run and test our Hello World React app, we need to create a couple more configuration files because these modules need some direction in what to do.
Configuring Webpack
The first of these modules is Webpack. Webpack does a number of things, but at
its basic heart is its ability to take all our JavaScript/CSS dependencies and
“bundle” them up into a couple of files that we can distribute with our awesome
Hello World app. In order to do this, it needs a webpack.config.js
file
defined that tell it how to operate.
Since there isn’t a webpack init
to create our config file, let’s just
create one.
$ touch webpack.config.js
In this file, paste in the following:
// webpack.config.js
var webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
devtool: "source-map",
entry: {
"index": "./src/app",
"css": "./style.scss"
},
externals: {
"react/lib/ExecutionEnvironment": true,
"react/lib/ReactContext": true,
"assets": true
},
module: {
loaders: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
query: {
presets: ["es2015", "react"]
}
},
{
test: /\.s?css$/,
loader: ExtractTextPlugin.extract("style", "css?sourceMap", "sass?sourceMap")
}
]
},
plugins: [
new ExtractTextPlugin("bundle.css")
],
output: {
path: __dirname,
filename: "./bundle.js"
},
resolve: {
extensions: ["", ".js", ".jsx"]
}
};
So, let’s go over this. First off, it’s a JavaScript file. This means that you can put regular ol’ JavaScript in there and it will work. That might not seem useful right now, but keep it in mind anyway as you will use JavaScript syntax when editing.
var webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
First line loads the webpack module, and the second line adds in an optional plugin that we need to bundle our CSS. If you didn’t need the CSS bundling feature, you could leave this line out.
devtool: "source-map",
This is also optional. This defines a module that is used to enhance debugging of webpack. More Info
entry: {
"index": "./src/app",
"css": "./style.scss"
},
This defines the main entry points into our app. The first line index
tells
webpack to start looking for dependencies from the app.js* file. Any files that
app.js requires will also be included as well as any files that those require
and so on. “CSS” is the same as app, but for CSS :-).
externals: {
"react/lib/ExecutionEnvironment": true,
"react/lib/ReactContext": true,
"assets": true
},
Externals defines things that you want webpack to ignore completely.
Normally, webpack will work its way through your modules and bundle up all the
dependencies for each module as well as their dependencies, and their
dependencies and so on. Defining externals tells webpack to not bundle those
libraries/resources into our bundle.js
file. In our example, we are ignoring
a couple of react modules and our assets folder. The React lines are there
because of Enzyme. Enzyme has backwards compatibility with older versions of
React and because of that, it references things that don’t exist in the new
version we are using. Webpack doesn’t know that and will try and bundle the
resouces that don’t exist.. producing an error. By marking them as externals,
we prevent that.
The assets are there because we will just reference those files directly in our
code and don’t want them getting pulled into our bundle.js
file and bloating
it all up.
module: {
loaders: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
query: {
presets: ["es2015", "react"]
}
},
{
test: /\.s?css$/,
loader: ExtractTextPlugin.extract("style", "css?sourceMap", "sass?sourceMap")
}
]
},
The module section defines special tools that webpack uses to transform our modern ES6/Sass/JSX code into old fashioned ES5 and CSS. It does this through loaders, which is an array of objects that define what each loader is. I’ll break it down a little further:
test
defines the file pattern that webpack will be looking for when applying
this loader. In our file, it will be looking for any file that ends in .js
or
.jsx
to run through the loader.
loader: "babel-loader"
tells webpack that it will use be using
Babel to translate our ES6 and JSX code before putting
it into our bundle.
exclude:
defines folders/files that the loader will ignore. Since we don’t
need Babel to transpile anything in our node_modules folder, we have added it
here.
query:
defines additional settings used by the loader. In this case, Babel
needs to be told what type of code it will be transpiling. For us, this means
React and ES2015 Code and Babel uses presets
to define these. Learn more
about presets here.
The second loader we are using is optional. We are telling webpack to take our .CSS/SCSS files and extract them into a seperate CSS file instead of in-lining it into our JavaScript. It does this through the ExtractTextPlugin which you can read more about here.
For the final section..
plugins: [
new ExtractTextPlugin("bundle.css")
],
output: {
path: __dirname,
filename: "./bundle.js"
},
resolve: {
extensions: ["", ".js", ".jsx"]
}
plugins:
defines how our plugins are called. In this case, we are calling our
ExtractTextPlugin and giving it a parameter of the output filename it will be
creating from our SCSS and CSS files.
output:
defines our destination for the resulting webpack bundle. We are
telling it that its starting path is in the root of our app and to give it the
name bundle.js
.
resolve:
defines how and what webpack will look for when finding files to
include in our bundle. In this case, we are telling webpack to look for .jsx
and .js
files.
That’s it for webpack config. Now just one more config file to create before we are ready to start making our app.
Configuring Karma
Karma is our test runner and is where all of our tests will be loaded and evaluated. It does this by taking our code and running it in browser (in this case, Chrome), so that our testing framework can evaluate the output. Like Webpack, Karma is modular and can make use of a number of testing frameworks, browser configurations, preprocessors, etc. At this point, we need a configuration file to tell Karma what it will be using to do its job. We don’t have one yet, so lets create that now.
$ touch karma.config.js
Go ahead and copy the config text below and paste into this file. I’ll go over what all this does afterwards.
// karma.config.js
var webpack = require("webpack");
module.exports = function(config) {
config.set({
frameworks: ["mocha"],
files: ["test/test_app.jsx"],
preprocessors: {
"test/index.js": ["webpack", "sourcemap"]
},
reporters: ["mocha"],
mochaReporter: {
showDiff: true
},
browsers: ["Chrome"],
webpack: {
devtool: "inline-source-map",
externals: {
"react/addons": true,
"react/lib/ExecutionEnvironment": true,
"react/lib/ReactContext": true
},
module: {
loaders: [
{
test: /\.json$/,
loader: "json"
},
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
query: {
presets: ["es2015", "react"]
}
}
]
},
resolve: {
extensions: ["", ".js", ".jsx", ".json"]
},
watch: true
},
webpackServer: {
noInfo: true
},
singleRun: false
});
};
Lets start with the first declaration:
var webpack = require("webpack");
In order for Karma to do it’s thing, it needs all our modern fancypants ES6 and JSX code transpiled (just like any other browser would), so to do that, it has to run our code through all the same steps that we are doing with webpack before it can run our tests.
frameworks:
We are telling Karma to use Mocha as our framework of choice.
Other choices include Jasmine and Quint.
files:
this defines our apps starting point.
preprocessors:
Here we tell Karma what it needs to do before it can load
up our app. We are telling Karma that it will create a sourcemap of our app and
compile it using webpack. A sourcemap is an additional file that makes
debugging easier by showing errors as they relate to our source file and not
from the compiled version.
reporters:
This defines what Karma will use to output to the console the
results of our tests. I’m using Mocha’s reporter (because it’s clear and
concise).
mochaReporter: showDiff: true
An additional option for our reporter. This
will give a GitHub-like diff of any test failures. This will be VERY useful
later when testing the equality of JSON, arrays, objects, or any large blob
of text.
browsers:
Karma supports a number of browsers out of the box. We are just
using Chrome at the moment. You can add additional browsers to make sure your
tests pass in different environments. More about Karma browser
support.
webpack:
Hey, it looks like our webpack.config.js file! Because Karma
doesn’t care about some of the things we need or use in webpack for our users,
it has it’s own copy of the webpack config. In this case, you’ll notice there
is nothing in there for CSS processing since Karma or our tests don’t look at
styling.
webpackServer:
We are telling the webpack webserver not to output to our
console, since we want to pay attention to the messages coming from our testing
framework.
singleRun: false
We are telling Karma to stay running and watch our files
for changes. Anytime a change occurs, it will re-run the tests.
That wraps up our Karma config, lets make an app!
Index.html and style.scss
Lets start by filling in our empty index.html and style.scss file.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, width=device-width">
<link rel="stylesheet" type="text/css" href="bundle.css">
<title>Hello World</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>
Pretty straight forward, but I’d like to point out a few things.
...
<link rel="stylesheet" type="text/css" href="bundle.css">
...
<div id="app"></div>
<script src="bundle.js"></script>
The important part is the empty div. This is where React will load our app. The bundle.css and bundle.js files don’t exist yet, but will after Webpack runs the first time.
What we have so far..
So lets take a quick break to verify what we have so far. You should have a directory structure that looks like this:
App
│ index.html
| karma.config.js
| package.json
│ style.scss
│ webpack.config.js
└───assets
└───dist
└───node_modules
└───src
│ app.jsx
└───test
│ test_app.jsx
Hopefully yours looks like what you see above. If not, the just go back and fill in what you may have missed.
Tests first!
Lets do this right by writing a test for our yet to be made app. Open up test/test_app.jsx.
For next part, don’t copy and paste. Type it all out. It’s not much and it will help you to remember.
We will start by importing our dependencies.
import React from "react";
import { assert } from "chai";
import { shallow } from "enzyme";
import { App } from "../src/app";
We will be needing React of course, so that comes first. Followed by assert
(from Chai) and shallow
(from Enzyme). Assert is what we will be using to
actually test a condition. Enzyme is a helper and gives us a much cleaner and
easier way of writing our tests. Last is our app which needs to be loaded so
we can test it.
Next, let’s write a test. We will do this one line at a time.
describe("App", function() {
Here we are simply telling Mocha that we are starting a new group of tests. “App” could literally be anything that describes what we are testing.
it("should render Hello World!", function() {
Here we define a test case. We are stating our expected result.
let wrapper = shallow(<App />);
Here we are using Enzyme to generate a virtual version of our app (which doesn’t exist yet). Shallow means that it won’t build any child components so your tests are isolated to a specific component. We don’t have to worry too much about that at this point, but I like to use shallow where I can.
The alternative to shallow is mount
and render
which will fully render the
app and usually work anytime shallow doesn’t. Medium has a decent write up
here.
assert.equal(wrapper.find("h1").text(), "Hello World!");
We are using assert.equal to test
for equality. It takes two required parameters and one optional one. The first
param is our actual
result. In this case, we are using the find
method
defined on our wrapper (thanks to Enzyme) to look for an H1 tag and examine the
contents. Whatever it happens to find there will be passed into the
assert.equal method.
The second parameter is what we expect
it to find. If it doesn’t match that, then
the test will fail. There is an optional third parameter which is simply a text
string that will be printed out in the case of a test failure. We aren’t using
that here.
Close it out:
});
});
Go ahead and save the file. You should have something that looks like the following:
import React from "react";
import { assert } from "chai";
import { shallow } from "enzyme";
import { App } from "../src/app";
describe("App", function() {
it("Should render Hello World!", function() {
let wrapper = shallow(<App />);
assert.equal(wrapper.find("h1").text(), "Hello World!");
});
});
At this point, if you run a test:
$ npm run test
You should get something like this:
...
START:
App
✖ Should render Hello World!
Finished in 0.026 secs / 0.006 secs
SUMMARY:
✔ 0 tests completed
✖ 1 test failed
FAILED TESTS:
App
✖ Should render Hello World!
Chrome 51.0.2704 (Mac OS X 10.11.5)
TypeError: Cannot read property 'propTypes' of undefined
...
Awww, it failed. Now, let’s make it pass! For that, we need to make our app.
Building Hello World
Open up src/app.jsx
Like our tests, let’s do this line by line.
First, let’s import React.
import React from "react";
Now, we need to create a React class that will define our app.
export let App = React.createClass({
Here we are creating a component class. An app can consist of one or hundreds of components. The normal structure of a React app starts with a root component that contains many child components. Our app will have just one, the root.
render: function() {
Every component needs a render function to tell React what it will be outputting.
return (
<div>
<img src="../assets/react.png" />
<h1>Hello World!</h1>
</div>
);
A render function must be defined as a return. You will notice that our render is just plain HTML. If we weren’t using JSX, this section would look a lot different and much more verbose. It’s worth reading about raw React to get a better understanding of what is going on under the hood.
ReactDOM.render(<App />, document.getElementById("app"));
Here we are finally rendering our app to the page. ReactDOM.render
takes two
arguments. the first defines our component, the second defines where it will
end up in our HTML.
Now let’s close it out.
}
});
At this point, we should have a file that looks like this:
import React from "react";
export let App = React.createClass({
render: function() {
return (
<div>
<h1>Hello World!</h1>
</div>
);
}
});
ReactDOM.render(<App />, document.getElementById("app"));
Go ahead and save the file. You should now be able to start our webpack server and see our creation!
$ npm run start
You will see webpack build your files and start a webserver. Go ahead and open up a web browser and put in this URL http://localhost:8080/webpack-dev-server/.
It’s pretty good, but kind of ugly. Lets fix that. Leave the browser open,
open up style.scss
and paste this in.
h1 {
color: #0066ff;
}
Go ahead and save it and take a look at your still open browser window. Pretty
nice right!? The webpack server will auto update your page with any changes you
make to the app or stylesheets. Ok, let’s see if our tests pass! Press
Control+C
to kill our webpack server.
Run our test:
$ npm run test
Test failed?!! Lets see why:
Uncaught Invariant Violation: _registerComponent(...): Target container is not
a DOM element.
WAT? Ok, there is a good reason for this. In our app.jsx file we are calling the render function, but it’s trying to render to an element that doesn’t exist when the file is included. To fix this, we need to separate our component from our render so it can be properly tested.
To do this, we need to perform a little refactoring.
Refactoring for tests
Each of our components should be able to be loaded in isolation for easier testing. This means we don’t want our app component to be concerned also with rendering it out to the page.
Lets start by adding two files that will serve as our new entry points into our app.
$ touch src/index.jsx
$ touch test/index.js
Open up src/index.jsx
.
Lets copy some of what we have already written in app.jsx over to here.
<!-- src/index.jsx -->
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./app"
ReactDOM.render(<App />, document.getElementById("app"));
The only new thing here is that we are importing our app into index.jsx. Go ahead and save it.
Since we are rendering from here, you can now DELETE the following lines from app.jsx
import ReactDOM from "react-dom";
and
ReactDOM.render(<App />, document.getElementById("app"));
Let’s now open up test/index.jsx
and ADD the following:
import "./test_app";
Make sure you’ve saved all your files at this point. We are almost done. All we
need to do is tell Node, Webpack and Karma about the new changes. First, open
package.json
and update the main:
entry to the following
"main": "src/index.jsx",
Then open karma.config.js
update the files:
entry to:
files: ["test/index.js"],
And one more, open webpack.config.js
and change entry:
to read:
entry: {
"index": "./src/index",
"css": "./style.scss"
},
These changes tell the respective apps where it will look to bootstrap our app.
Go ahead and run npm start
and verify that our app still loads. If not, we
need to go back and verify all the changes have been made and saved.
And now, for the moment of truth: run npm run test
and see if it passes.
Hopefully you should have seen all green. If not, check out the errors and see
if you can see what went wrong.
Compile and ship it!
Finally, lets package our app and get it ready for production. Type:
$ npm run dist
After a few seconds, webpack will have bundled your app and a couple of bash
commands will have copied it to the dist/
folder. This is your compiled app,
ready for the browser.
Ok, that’s it. I hope you found this helpful. If you have any suggestions for the starter app, send me a pull request or open an issue. Remember, you can download a fully functional starter from my GitHub repo
For more information on getting started with React, I highly recommend Learn Raw React, by James Nelson as well as his follow up tutorials.