As of April 2022, we are deprecating Underreact and do not recommend usage on new projects. Development will only occur on an exemption basis for existing, internal Mapbox use-cases.
New projects should use an alternative such as Create React App or Vite.
Minimal, extensible React app build system that you won't need to eject.
It's a pretty thin wrapper around Babel, Webpack, and PostCSS, and will never accumulate an ecosystem of its own. And it aims to be just as useful for production applications with idiosyncratic demands as for simple prototypes.
Requirements:
Install Underreact as a devDependency of your project:
npm install --save-dev @mapbox/underreact
If you are building a React application, you also need to install React dependencies:
npm install react react-dom
Add _underreact*
to your .gitignore
, and maybe other ignore files (e.g. .eslintignore
). That way you'll ignore files that Underreact generates. (If you set the outputDirectory
option, you'll want to ignore your custom output directory.)
src/index.js
.// src/index.js
console.log('hello world!');
underreact
.npx underreact start
# or
node node_modules/.bin/underreact start
src/index.js
.// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return <div>Hello world</div>;
}
}
const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<App />, container);
underreact
.npx underreact start
# or
node node_modules/.bin/underreact start
You should not install the Underreact CLI globally. Instead, install Underreact as a devDependency of your project and use the underreact
command via npx
, npm "scripts"
, or node_modules/.bin/underreact
. The easiest way is probably to set up npm scripts in package.json
, so you can use npm run start
, npm run build
, etc., as needed.
The CLI provides the following commands:
start
: Start a development server.build
: Build for deployment.serve-static
: Serve the files that you built for deployment.Tip: In this readme we frequently use the command npx
, if you find it unfamiliar please read this blog post by npm.
To configure Underreact, create an underreact.config.js
file at the root of your project.
Please note that no configuration is necessary to get started. On most production projects you'll want to set at least a few of the configuration object
properties.
Your underreact.config.js
can export a function or an object.
You can also directly export the configuration object. This is a great way to start tweaking Underreact's configuration. For example, in the code below we simply modify the siteBasePath
:
// underreact.config.js
module.exports = {
siteBasePath: 'fancy'
};
You can also export a function that returns your configuration object.
This function is called with the following named parameters:
// underreact.config.js
/**
* @param {Object} opts
* @param {Webpack} opts.webpack - Underreact's version of Webpack. Use this as needed to apply core Webpack plugins like `PrefetchPlugin`, `IgnorePlugin`, and `SourceMapDevToolPlugin`, so that your project is not dependent on its own Webpack version.
* @param {'start'|'build'|'serve-static'} opts.command - The current Underreact command.
* @param {'production'|'development'} opts.mode - The current mode of Underreact.
* @returns {Promise<Object> | Object}
*/
module.exports = function underreactConfig({ webpack, command, mode }) {
return {
/* Underreact configuration object */
};
};
This approach is quite powerful, because you can also return a Promise or use an async function to generate configurations with asynchronous dependencies from the filesystem or Internet. For example:
// underreact.config.js
const path = require('path');
const downloadAssets = require('./scripts/fetchAssets');
module.exports = async function underreactConfig({ webpack, command, mode }) {
const publicAssetsPath = 'public';
await downloadAssets(path.resolve(publicAssetsPath));
return {
publicAssetsPath,
webpackPlugins: [command === 'build' ? new webpack.ProgressPlugin() : null]
};
};
Underreact is intended for single-page apps, so you only need one HTML page. If you are building a React application, you can also use it to define a div
element for react-dom
to mount your React component tree on.
You have 2 choices:
htmlSource
configuration option, which is an HTML string, a Promise or a Function returning HTML string or promise, that resolves to an HTML string.<title>
.If you provide a Promise for htmlSource
, you can use any async I/O you need to put together the page. For example, you could read JS files and inject their code directly into <script>
tags, or inject CSS into <style>
tags. Or you could make an HTTP call to fetch dynamic data and inject it into the page with a <script>
tag, so it's available to your React app.
If you provide a Function for htmlSource
, Underreact would call it with the named parameter basePath
. This gives you the flexibility to load assets with a root relative URL. The example below shows how to load a favicon from your public
directory:
// underreact.config.js
module.exports = {
/**
* @param {Object} opts
* @param {Webpack} opts.basePath - the normalized value of your site's base path
* @returns {Promise<string> | string}
*/
htmlSource: ({ basePath }) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Words that rhyme with fish</title>
<meta name="description" content="A website about words that rhyme with fish, like plish">
<link rel="shortcut icon" href="https://github.com/mapbox/underreact/blob/main/${basePath}/img/favicon.ico" type="image/x-icon" />
</head>
<body>
<div id="app">
<!-- React app will be rendered into this div -->
</div>
</body>
</html>
`
};
Note: Underreact would automatically inject the relevant script
and link
tags to your HTML template.
In the example below, we are defining our HTML in a separate file and requiring it in underreact.config.js
:
// underreact.config.js
const html = require('./html');
module.exports = function underreactConfig({ webpack, command, mode }) {
return {
htmlSource: html(mode)
};
};
// html.js
const fs = require('fs');
const { promisify } = require('util');
const minimizeJs = require('./minimize-js');
module.exports = async mode => {
// read an external script, which we will inline
let inlineJs = await promisify(fs.readFile)('./path/to/some-script.js');
if (mode === 'production') {
inlineJs = minimizeJs(inlineJs);
}
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Words that rhyme with fish</title>
<meta name="description" content="A website about words that rhyme with fish, like plish">
<script>${inlineJs}</script>
</head>
<body>
<div id="app">
<!-- React app will be rendered into this div -->
</div>
</body>
</html>
`;
};
Underreact provides two different modes of execution: development and production
The development mode is the default mode of the start
command. This mode is meant to be used in a local development environment, ideally your computer. Underreact does a bunch of optimizations to make compilation as fast as possible and enable developer tools like hot reloading and live reloading.
You can use this mode by simply running underreact start
:
npx underreact start
# or being explicit
npx underreact start --mode=development
You can also use this mode with the build
command and then serve it with serve-static
, if you want to perform quick inspection of unminified files.
npx underreact build --mode=development
# serve it
npx underreact serve-static
Warning: Do not host code generated by development mode in a production environment.
This mode is geared towards running the build output in a production environment. Underreact performs a bunch of optimizations to make your application run fast and reduce the bundle size.
You can use this mode by simply running underreact build
:
npx underreact build
# or being explicit
npx underreact build --mode=production
You can also use this mode with the start
command, in case you need to debug a problem that does not show up in development
mode (e.g. one caused by minification):
npx underreact start --mode=production
Out of the box Underreact doesn't require you to setup a babel.config.js
file. It uses @mapbox/babel-preset-mapbox
internally to provide a top-notch default configuration.
babel.config.js
There are many cases — for example, when using Jest — when you want a babel.config.js
to exist at the root your project. In this case it is best to create a babel.config.js
at the root of your project and install @mapbox/babel-preset-mapbox
as a devDependency:
npm install --save-dev @mapbox/babel-preset-mapbox
// babel.config.js
module.exports = {
presets: ['@mapbox/babel-preset-mapbox']
};
While you are free to use any Babel presets & plugins, we strongly recommend that you use @mapbox/babel-preset-mapbox
, as it provides a good combination of presets and plugins that are necessary for any Underreact application to work properly. For more advanced configuration visit the documentation for @mapbox/babel-preset-mapbox
.
Note: Underreact doesn't support .babelrc
; please use babel.config.js
. (Read more about the difference here).
One of the founding principles of the Internet is its ability to support a multitude of devices. With the ever changing JavaScript ecosystem, new features of the language coming yearly and it has become difficult to use them while also supporting older browsers. Underreact wraps tools that solve these problems for you.
In Underreact you can use the Browserslist notation to specify the browser versions that you want to support. By default, Underreact uses a query that supports all major browsers including ie 11
. You can change this behaviour by customizing the browserslist
property:
// underreact.config.js
module.exports = {
// The % refers to the global coverage of users from browserslist
browserslist: ['>0.25%', 'not ie 11']
};
In the example above we are setting browserslist
to target all the browsers with greater than 0.25%
market share but not IE 11. This information will be passed to Autoprefixer to add vendor prefixes to CSS and to Babel to transpile your JavaScript to ES5.
By default, Underreact polyfills the following JavaScript features:
Array.from
Object.assign
Promise
Symbol
The above polyfills (combined with Babel's transpilation) allow you to freely use for..of
loops, async functions, and the spread operator.
If your application needs any other polyfill (e.g. fetch
), you can install it and import it at the top of your jsEntry
file:
// src/index.js
import 'whatwg-fetch';
@babel/polyfill
If you don't care about bundle size and want to polyfill all standard JS, you can install @babel/polyfill
and import it in your jsEntry
file.
Warning: polyfill
must be set to false
to use @babel/polyfill
and you should only import @babel/polyfill
once and only once in your application.
Underreact allows you to inject environment variables into your client-side code at build time. You can set them up by using the environmentVariables
option in your configuration.
// underreact.config.js
module.exports = {
environmentVariables: {
SERVER_URL: 'https://ketchup.com'
}
};
Note: DEPLOY_ENV
& NODE_ENV
are special environment variables in Underreact, so cannot be set in Underreact configuration.
DEPLOY_ENV
and NODE_ENV
NODE_ENV
will default to 'development'
in development mode and 'production'
in production mode.NODE_ENV
manually: use Underreact modes instead. But if you do set NODE_ENV
, it must be 'development'
, 'production'
, or 'test'
.DEPLOY_ENV
is set to 'development'
. You can it to any value you wish to better align with your target environments (e.g. DEPLOY_ENV=something npx underreact build
) and this value will be made available in your client-side code on process.env.DEPLOY_ENV
. For example, you may want to set it to 'staging'
when building for a staging environment, 'production'
when building for production, or 'test'
when testing.DEPLOY_ENV
is not the same as NODE_ENV
: see below.A recommend way to use DEPLOY_ENV
is set it in your npm scripts:
// package.json
{
"scripts": {
"build": "underreact run build", // if not set, DEPLOY_ENV will be set to `production` automatically
"build:staging": "DEPLOY_ENV=staging underreact run build",
"build:sandbox": "DEPLOY_ENV=sandbox underreact run build"
}
}
DEPLOY_ENV
instead of NODE_ENV
?If you are used to using NODE_ENV
to target different deployment environments, you should instead use DEPLOY_ENV
, instead.
Underreact discourages setting NODE_ENV
manually, as a number of libraries depend on its value and a wrong value could result in an unoptimized build. You should instead use Underreact's modes, which will set the right NODE_ENV
for your app.
Type: Array<string>
| Object
. A valid Browserslist value. Default:['>0.2%', 'not dead', 'not ie < 11', 'not op_mini all']
.
This value is used by Autoprefixer to set vendor prefixes in the CSS of your stylesheets, and is used to determine Babel compilation via babel-preset-env.
You can also target different settings for different Underreact modes by sending an object:
// underreact.config.js
module.exports = {
browserslist: {
production: ['> 1%', 'ie 10'],
development: ['last 1 chrome version', 'last 1 firefox version']
}
};
Type: boolean
| Array<string>
. Default: true
.
Many npm packages are now written in ES2015+ syntax, which is not compatible with all the browsers you may be supporting. So by default Underreact compiles all node_modules
to ES5.
You can set compileNodeModules: false
to disable compilation of node_modules
, or pass an array of package names to selectively compile. In the example below we are only compiling the specified npm packages:
// underreact.config.js
module.exports = {
compileNodeModules: ['p-finally', 'p-queue']
};
Type: boolean
. Default: false
.
Set to true
if you want to use HTML5 History for client-side routing (as opposed to hash routing). This configures the development server to fall back to index.html
when you request nested routes.
Tip: This should only be intentionally turned on, when you know you're going to configure your server to allow for HTML5-History-powered client-side routing.
Type: { [string]: string | number | boolean }
.
Environment variables that you'd like to make available in your client-side bundle on process.env
. For example, if you set environmentVariables: { ORIGIN: 'foo.com' }
, you can use process.env.ORIGIN
in your JavaScript.
Type: boolean
. Default: true
.
Enable hot module reloading of Underreact. Read "How do I enable hot module reloading?" for more details.
Type: string
|Promise<string>
|Function<string | Promise<string>>
. Default:see the default HTML.
The HTML template for your app, or a Promise that resolves to it. Read "Defining your HTML" for more details.
Type: string
. Absolute path. Default: ${project-root}/src/index.js
.
The entry JS file for your app. In a typical React app, this is the file where you'll use react-dom
to render your app on an element.
In the default value, project-root
refers to the directory of your underreact.config.js
file.
Type: boolean
. Default: true
.
Set it to false
to prevent automatic reloading of your app on code changes. Switching off liveReload
also disables hot reloading.
Type string
. Absolute path, please. Default: ${project-root}/_site/
.
The directory where webesite files should be written.
You'll want to ignore this directory with .gitignore
, .eslintignore
, etc.
In the default value, project-root
refers to the directory of your underreact.config.js
file.
Type: boolean
. Default: true
.
Whether or not to use Underreact's default polyfills. Read more at "Polyfilling newer JavaScript features".
Type: number
. Default: 8080
.
Preferred port for development servers. If the specified port is unavailable, another port is used.
Type: Array<Function>
. Default: []
.
All of the CSS that you import is run through PostCSS, so you can apply any PostCSS plugins to it. Underreact always runs Autoprefixer for you.
Type: string
. Default: underreact-assets
.
The directory where Underreact assets will be placed, relative to the website's root.
By default, for example, the main JS chunk will be written to underreact-assets/js/main.chunk.js
.
Tip: It's important to know about this value so you can set up caching and other asset configuration on your server.
Type string
. Absolute path, please. Default: ${project-root}/public/
.
Any files you put into this directory will be copied, without processing, into the outputDirectory
.
You can put images, favicons, data files, and anything else you want in here. To reference these assets in your Javascript code, you can use the BASE_PATH
environment variable. Read "How do I include SVGs, images, and videos?".
In the default value, project-root
refers to the directory of your underreact.config.js
file.
Type: string
. Default: '/'
.
Path to the base directory on the domain where the site will be deployed. The default value is the domain's root. To help create valid links, Underreact exposes this value to your source code with an environment variable BASE_PATH
. The table below gives an example of how Underreact sets BASE_PATH
environment variable for a given siteBasePath
value:
siteBasePath | process.env.BASE_PATH |
---|---|
(not set) | "" |
"/" | "" |
"ketchup" | "/ketchup" |
"/ketchup" | "/ketchup" |
"/ketchup/" | "/ketchup" |
This normalization behaviour comes in handy when writing statements like process.env.BASE_PATH + '/my-path'
. Read "How do I include SVGs, images, and videos?".
Underreact also passes this as a named parameter to the htmlSource
function. Read "Defining your HTML" for more details.
Tip: There's a good chance your app isn't at the root of your domain. So this option represents the path of your site within that domain. For example, if your app is at https://www.special.com/ketchup/*
, you should set siteBasePath: '/ketchup'
.
Type: string
. Absolute path. Default: ``.
The directory where Webpack would write stats. By default, no stats file will be generated.
Type: Array<string>
. Default: []
.
Identifiers of npm packages that you want to be added to the vendor bundle. The purpose of the vendor bundle is to deliberately group dependencies that change relatively infrequently — so the vendor bundle can stay cached for longer than the others.
By default, the vendor bundle includes react
and react-dom
.
Tip: It is good idea to include big stable libraries your project depends on: for example, redux
, moment.js
, lodash
, etc.
Type: config => transformedConfig
. Default x => x
(identify function).
If you want to make changes to the Webpack configuration beyond what's available in the above options, you can use this, the nuclear option. Your function receives the Webpack configuration that Underreact generates and returns a new Webpack configuration, representing your heart's desires.
Tip: You should think twice before using webpackConfigTransform
, as Underreact tries its best to abstract away Webpack so that you can focus on your application.
Type: Array<Rule>
.
Webpack Rule
s specifying additional loaders that you'd like to add to your Webpack configuration.
If you need more fine-grained control over the Webpack configuration, use webpackConfigTransform
.
Tip: You should be careful before adding support for a new source type (for example, scss
, less
, ts
), as it will make your application dependent on Webpack and its ecosystem.
Type: Array<Object>
.
Additional plugins to add to your Webpack configuration.
For plugins exposed on the webpack
module itself (e.g. webpack.DefinePlugin
), you should use Underreact's version of Webpack instead of installing your own.
That will prevent any version incompatibilities.
That version is available in the context object passed to your configuration module function.
Here, for example, is how you could use the DefinePlugin
in your underreact.config.js
:
// underreact.config.js
module.exports = ({ webpack }) => {
return {
webpackPlugins: [new webpack.DefinePlugin(..)]
};
}
Jest expects a babel.config.js
at the root of your application. Read "Exposing babel.config.js
". Underreact will only work with Jest version >=23.6. To install Jest, follow the steps mentioned for Babel 7 in the official installation docs.
You can use the import()
syntax to asynchronously load a valid JavaScript module. For example:
// src/index.js
import("./math").then(math => {
console.log(math.add(16, 26)); // 42
});
// src/math.js
export default add(a,b) {
return a + b;
}
Read official React docs for more information on how to load your React component dynamically.
To reduce the build size you can try the following:
browserslist
.node_modules
. By either selectively compiling or disabling compilation of node_modules
, you can save some compilation time and reduce build size.import logo from './logo.png';
console.log(logo); // /logo.84287d09.png
function Header() {
// Import result is the URL of your image
return <img src={logo} alt="Logo" />;
}
It is generally a good idea to use the above method for importing assets because:
If you cannot use this method, you can place assets in the publicDirectory
and create a link using the BASE_PATH
environment variable as shown below:
function Header() {
return <img src={process.env.BASE_PATH + '/logo.png'} alt="Logo" />;
}
The BASE_PATH
environment variable is automatically set by Underreact and is equivalent to the value of siteBasePath
. BASE_PATH
's value will never end with a /
, even if your siteBasePath
does.
Hot module reloading allows you to reload only the module that has changed, without affecting the rest of the code or reloading the page in the browser. This is different from liveReload
which reloads the entire application when code changes. Underreact first tries to hot reload, then falls back to live reloading.
Underreact supports CSS and JavaScript hot reloading. CSS hot reloading should work out of the box. To implement hot reloading for JavaScript modules, you can follow the steps in the Webpack docs. (You can skip the parts about Webpack configuration, as it has already been taken care of by Underreact.)
For React apps, you'll' benefit from hot module reloading of React components. Luckily this setup is fairly straightforward. First, you need to get your own babel.config.js
file by following the steps in "Exposing babel.config.js
". Then, you need to install react-hot-loader:
npm install react-hot-loader
And then add it to your babel.config.js
:
// babel.config.js
module.exports = {
presets: ['@mapbox/babel-preset-mapbox'],
plugins: ['react-hot-loader/babel']
};
You can then make any of your React components hot:
// src/app.js
import React from 'react';
import { hot } from 'react-hot-loader';
const App = () => <div>Hello World!</div>;
export default hot(module)(App);
You can read more about hot reloading your React components by reading react-hot-loader
docs.