Docker for Development – React

by Javier Treviño Saldaña

The goal of this post is to:

  • Install React locally, using Docker
  • Create a simple React component using JSX
  • See the component in our browser

Prerequisites:

  • Docker
  • Npm container

In a previous blog post (Simplifying Docker for Development), we got npm running via Docker using a really simple setup. If your Docker-fu is weak, I encourage you to give it a read. In this article we’ll build on top of that setup.

Getting Started

There are many posts outlining how to get started with React, the reason why I’m documenting this journey is that every step will be running via Docker, not in our host OS.

Writing modern JavaScript

“ECMAScript” is the official name for “JavaScript”. The first version dates back to 1997. Now in 2020 we’re roughly in version 10. Babel is a utility which allows us to write concise code in modern ES, then transforms it into a format supported by older browsers.

For example, you can write this neat version:

[1, 2, 3].map(n => n ** 2);

Which Babel transforms into:

[1, 2, 3].map(function(n) {
  return n + 1;
});

Let’s try it out. First we gotta install Babel:

$ npm install --save-dev @babel/core @babel/cli @babel/preset-env
  • The core library
  • The CLI to run it from our terminal
  • @babel/preset-env is a predetermined config which according to their docs: “allows you to use the latest JavaScript without needing to micromanage which syntax transforms are needed by your target environment(s).”

Note: if you followed our previous blog post (linked in the intro), the npm command will actually spin up a Docker container to execute it.

We also have to create a .babelrc file in our project root to configue Babel:

{
  "presets": ["@babel/env"]
}

Now we can write some modern ES code and test our setup:

$ echo "[4, 5, 6].map(n => n ** 3);" > src/map.js

$ cat src/map.js
[4, 5, 6].map(n => n ** 3);
$ ./node_modules/.bin/babel src -d dist
Successfully compiled 1 file with Babel.

$ cat dist/map.js
"use strict";

[4, 5, 6].map(function (n) {
  return Math.pow(n, 3);
});

There we go :)

Note: “dist” stands for “distribution” and seems to be a popular standard in the JS community to store compiled assets (in other languages it might be “build”, “out”, “target”, etc).

First React Component

Alright, now let’s create a React component using JSX, and understand the pieces that have to work together in order to see it in the browser.

First, our simple component:

$ cat src/Counter.jsx
import React from "react"

class Counter extends React.Component {
  render() {
    return <div>
      This will eventually be a Counter. Nothing too interesting yet.
    </div>
  }
}

I want to use JSX (a special syntax optimized for React) because the plain JS alternative is less readable/maintainable (to me).

Now we can try using Babel to transform JSX, but I expect it to fail:

$ rm src/map.js

$ ls src
Counter.jsx

$ ./node_modules/.bin/babel src -d dist
SyntaxError: /Code/experiment/src/Counter.jsx: Unexpected token (5:11)

  3 | class Counter extends React.Component {
  4 |   render() {
> 5 |     return <div>
    |            ^
  6 |       This will eventually be a Counter. Nothing too interesting yet.
  7 |     </div>
  8 |   }
    at Parser.raise (/Users/jts/Code/dyn/reconsulta/ui-web/node_modules/@babel/parser/lib/index.js:7017:17)
    at Parser.unexpected (/Users/jts/Code/dyn/reconsulta/ui-web/node_modules/@babel/parser/lib/index.js:8395:16)

We’ll need to install a Babel config that supports JSX:

$ npm install @babel/preset-react --save-dev

Update your .babelrc:

diff --git a/.babelrc b/.babelrc
-  "presets": ["@babel/env"]
+  "presets": ["@babel/env", "@babel/preset-react"]

Now we can retry our JSX transformation to plain JS:

$ ./node_modules/.bin/babel src -d dist
Successfully compiled 1 file with Babel.

$ head dist/Counter.js
"use strict";

var _react = _interopRequireDefault(require("react"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

Looks like our new Babel/JSX config worked.

However, the require("react") statement above in our compiled JS is not supported by browsers (yet). To prove it, we can include Counter.js in an HTML file (e.g. create “dist/test.html”):

<!DOCTYPE html>
<html>
  <script src="Counter.js"></script>
</html>

When I open it in my browser, I expect this to fail because, on one hand of course we don’t even have React installed yet, but the failure we’ll get should be because “require” is not defined:

"require" is not defined (Screenshot)

[Error] ReferenceError: Can't find variable: require
	Global Code (Counter.js:3)

We’ll have to introduce our final dependency (hopefully)– webpack, to support “require” & organize our code into multiple files.

There are two ways to run JavaScript in a browser. a) Include a script for each functionality; this solution is hard to scale because loading too many scripts can cause a network bottleneck. b) Use a big .js file containing all your project code, but this leads to problems in scope, size, readability and maintainability.

[Webpack is] a tool that lets you bundle your JavaScript applications, and it can be extended to support many different assets such as images, fonts and stylesheets.

Webpack does more than we need in this case, but the extra features are optional. Understanding this tool probably deserves its own blog post series; we’ll keep it simple for now.

$ npm install webpack webpack-cli babel-loader --save-dev

We can tell webpack where our source code is, and it will “process” it. The definition of “process” in our case will be:

  • Use Babel to transform modern ECMAScript, and JSX, into more verbose JS
  • Define the root, or “entry point” to our JS application
  • Compile a giant unreadable JS file, merging all the assets found via the entry point

Our “entry point” will be src/index.js:

import React from "react";
import ReactDOM from "react-dom";
import Counter from "./Counter";

ReactDOM.render(<Counter />, document.getElementById("root"));

Webpack will try to find everything we tell it to import in this entry point.

React will be provided by npm:

$ npm install react react-dom --save-dev

and we’ll need to “export” our Counter component:

diff --git a/src/Counter.jsx b/src/Counter.jsx
@@ -7,3 +7,5 @@ class Counter extends React.Component {
     </div>
   }
 }
+
+export default Counter;

Alright. We’re almost there. We have all the source files ready, now we can run webpack to “package” them into a single, giant JS file.

Recall our desired webpack “process”:

  • Use Babel to transform modern ECMAScript, and JSX, into ugly older JS
  • Define the root, or “entry point” to our JS application
  • Compile a giant unreadable JS file, merging all the assets found via the entry point

Now expressed in webpack.config.js:

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  mode: "development",
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: { presets: ["@babel/env"] }
      }
    ]
  },
  resolve: {
    extensions: [".js", ".jsx"]
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist")
  }
};

$ ./node_modules/.bin/webpack --config webpack.config.js

Hash: a39d8b0bf79d2b27801d
Version: webpack 4.41.6
Time: 662ms
Built at: 02/12/2020 7:04:06 PM
    Asset      Size  Chunks             Chunk Names
bundle.js  1.08 MiB    main  [emitted]  main
Entrypoint main = bundle.js
[./src/Counter.jsx] 2.67 KiB {main} [built]
[./src/index.js] 179 bytes {main} [built]
    + 11 hidden modules

This command created a single, giant JS file:

$ head dist/bundle.js
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;

The last piece of the puzzle is creating an HTML file to import this bundle.js via a regular <script> tag.

In “public/index.html” or wherever you like:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Dynamic.Tech</title>
  </head>

  <body>
    <div id="root"></div>
    <noscript>This website requires JavaScript.</noscript>
    <script src="../dist/bundle.js"></script>
  </body>
</html>

Remember, the gist of our entry point src/index.js is:

ReactDOM.render(<Counter />, document.getElementById("root"));

…and here it is, our “Counter” component in our browser:

React Counter (Component) in browser (Screenshot)

It’s not flashy but we achieved quite a lot:

  • Install React locally, using Docker
  • Create a simple React component using JSX
  • See the component in our browser

Takeaways

The steps we outlined in our previous blog, Docker for Development - Start Simple, were enough to create the productive dev environment we used throughout this article– without touching a Dockerfile, Docker Compose, etc.

In this blog post we took an incremental approach to prepare our dev environment to support modern ECMAScript and React. Each step was exactly documented, and the project ended up with very few direct dependencies.

I hope you enjoyed the journey so far. Next time I plan to continue growing this simple Docker setup in a similar fashion– testing each step taken as we add more behavior to the system.