Blog

React without webpack: fast path to a working app from scratch

Published on by

Many people use Webpack because it's popular. create-react-app makes it even easier. But what if you prefer zero configuration? How minimal can your configuration for a React app get? This post contains my two favorite recipes for prototyping and shipping production React apps.

Truly single page application

These days I start every React project (including DataStation) the same way: a single HTML file.

First import React from a CDN.

index.html
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

Then import Babel so we can use JSX.

index.html
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

Make a basic app with a few components:

index.html
<div id="root"></div>
<script type="text/babel">
  function Header() {
    return (
      <header>
        <div className="container">
          My great app
        </div>
      </header>
    );
  }

  function Body() {
    return (
      <main>
        <div className="container">
          <h1>Home</h1>
          <p>This is one great app.</p>
        </div>
      </main>
    );
  }

  function App() {
    return (
      <div>
        <Header />
        <Body />
      </div>
    );
  }
  ReactDOM.render(<App />, document.getElementById('root'));
</script>

Add some styles:

index.html
<style>
body {
  margin: 0;
  padding: 0;
}

.container {
  width: 1200px;
  max-width: 100%;
  margin: 0 auto;
}

header {
  background: black;
  color: white;
  padding: 15px 0;
}
</style>

Put it all together:

index.html
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<style>
body {
  margin: 0;
  padding: 0;
}

.container {
  width: 1200px;
  max-width: 100%;
  margin: 0 auto;
}

header {
  background: black;
  color: white;
  padding: 15px 0;
}
</style>

<div id="root"></div>
<script type="text/babel">
  function Header() {
    return (
      <header>
        <div className="container">
          My great app
        </div>
      </header>
    );
  }

  function Body() {
    return (
      <main>
        <div className="container">
          <h1>Home</h1>
          <p>This is one great app.</p>
        </div>
      </main>
    );
  }

  function App() {
    return (
      <div>
        <Header />
        <Body />
      </div>
    );
  }
  ReactDOM.render(<App />, document.getElementById('root'));
</script>

I was able to build a fairly complex but working prototype of DataStation using this single file approach. It is very simple.

Downsides and alternatives

There are a few downsides to this approach. Breaking out this file into separate CSS with link tags and multiple JavaScript with or without esmodules is easy enough. But all non-esmodule libraries will be in the global window namespace. And it's easier to manage dependencies through package.json rather than in index.html referencing a CDN URL.

Recently there have been a number of projects that have tried to solve the complexity of Webpack by providing zero configuration. Parcel and esbuild are two I've used. Esbuild is one of the fastest bundlers today, and is the one I now default to.

Esbuild

All esbuild does is bundle and "browserify" all JavaScript files imported by some entrypoint script. So in this form we'll have three separate parts of the project: a 1) index.html file that references a 2) CSS file and a 3) single JavaScript file.

The CSS file is easy:

style.css
body {
  margin: 0;
  padding: 0;
}

.container {
  width: 1200px;
  max-width: 100%;
  margin: 0 auto;
}

header {
  background: black;
  color: white;
  padding: 15px 0;
}

The HTML entrypoint will no longer need to include React or Babel since esbuild will handle that when imported from our JavaScript file. All it needs to do now is contain the "root" div and reference the CSS and JavaScript files:

index.html
<link rel="stylesheet" type="text/css" href="/style.css">
<div id="root"></div>
<script src="/app.js"></script>

Now we can break the React code up into three separate files: index.jsx, body.jsx, and header.jsx. And we can switch to using import to include React.

header.jsx
import React from 'react';

export function Header() {
  return (
    <header>
      <div className="container">
          My great app
        </div>
    </header>
  );
}
body.jsx
import React from 'react';

export function Body() {
  return (
    <main>
      <div className="container">
        <h1>Home</h1>
        <p>This is one great app.</p>
      </div>
    </main>
  );
}
index.jsx
import React from 'react';
import ReactDOM from 'react-dom';

import { Header } from './header';
import { Body } from './body';

function App() {
  return (
    <div>
      <Header />
      <Body />
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById('root'));

And that's it! Now we need to set up package.json with all the necessary packages.

$ yarn init
$ yarn add --dev esbuild
$ yarn add react react-dom

Calling esbuild will make the bundle.

$ yarn esbuild --bundle index.jsx --outfile=build/app.js && cp index.html style.css build

And then using a simple HTTP server will make all of the site accessible.

$ python3 -m http.server --directory build

And you're done! If you want to make it even more developer-friendly you can install fswatch like we do in DataStation development.

Share

With questions, criticism or ideas, email or Tweet me.

Also, check out DataStation and dsq.