Creating a React App
If you want to build a new app or website with React, we recommend starting with a framework.
If your app has constraints not well-served by existing frameworks, you prefer to build your own framework, or you just want to learn the basics of a React app, you can build a React app from scratch.
Full-stack frameworks
These recommended frameworks support all the features you need to deploy and scale your app in production. They have integrated the latest React features and take advantage of React’s architecture.
Next.js (App Router)
Next.js’s App Router is a React framework that takes full advantage of React’s architecture to enable full-stack React apps.
Next.js is maintained by Vercel. You can deploy a Next.js app to any hosting provider that supports Node.js or Docker containers, or to your own server. Next.js also supports static export which doesn’t require a server.
React Router (v7)
React Router is the most popular routing library for React and can be paired with Vite to create a full-stack React framework. It emphasizes standard Web APIs and has several ready to deploy templates for various JavaScript runtimes and platforms.
To create a new React Router framework project, run:
React Router is maintained by Shopify.
Expo (for native apps)
Expo is a React framework that lets you create universal Android, iOS, and web apps with truly native UIs. It provides an SDK for React Native that makes the native parts easier to use. To create a new Expo project, run:
If you’re new to Expo, check out the Expo tutorial.
Expo is maintained by Expo (the company). Building apps with Expo is free, and you can submit them to the Google and Apple app stores without restrictions. Expo additionally provides opt-in paid cloud services.
Other frameworks
There are other up-and-coming frameworks that are working towards our full stack React vision:
- TanStack Start (Beta): TanStack Start is a full-stack React framework powered by TanStack Router. It provides a full-document SSR, streaming, server functions, bundling, and more using tools like Nitro and Vite.
- RedwoodJS: Redwood is a full stack React framework with lots of pre-installed packages and configuration that makes it easy to build full-stack web applications.
Deep Dive
Next.js’s App Router bundler fully implements the official React Server Components specification. This lets you mix build-time, server-only, and interactive components in a single React tree.
For example, you can write a server-only React component as an async
function that reads from a database or from a file. Then you can pass data down from it to your interactive components:
// This component runs *only* on the server (or during the build).
async function Talks({ confId }) {
// 1. You're on the server, so you can talk to your data layer. API endpoint not required.
const talks = await db.Talks.findAll({ confId });
// 2. Add any amount of rendering logic. It won't make your JavaScript bundle larger.
const videos = talks.map(talk => talk.video);
// 3. Pass the data down to the components that will run in the browser.
return <SearchableVideoList videos={videos} />;
}
Next.js’s App Router also integrates data fetching with Suspense. This lets you specify a loading state (like a skeleton placeholder) for different parts of your user interface directly in your React tree:
<Suspense fallback={<TalksLoading />}>
<Talks confId={conf.id} />
</Suspense>
Server Components and Suspense are React features rather than Next.js features. However, adopting them at the framework level requires buy-in and non-trivial implementation work. At the moment, the Next.js App Router is the most complete implementation. The React team is working with bundler developers to make these features easier to implement in the next generation of frameworks.
Start From Scratch
If your app has constraints not well-served by existing frameworks, you prefer to build your own framework, or you just want to learn the basics of a React app, there are other options available for starting a React project from scratch.
Starting from scratch gives you more flexibility, but does require that you make choices on which tools to use for routing, data fetching, and other common usage patterns. It’s a lot like building your own framework, instead of using a framework that already exists. The frameworks we recommend have built-in solutions for these problems.
If you want to build your own solutions, see our guide to build a React app from Scratch for instructions on how to set up a new React project starting with a build tool like Vite, Parcel, or RSbuild.
If you’re a framework author interested in being included on this page, please let us know.
Create Monorepo from Scratch
There are a couple of hurdles to starting a React monorepo. The first is that node can’t process all of the syntax (such as import/export and JSX). The second is that we will either need to build our files or serve them somehow during development for our app to work - This is especially important in the latter situations. These issues with be handled by Babel and Webpack, which we cover below
Setup
To get started, create a new directory for our new React monorepo. Inside the monorepo directory, initialize a project with
Thinking ahead a little bit, we’ll eventually want to build our app and we’ll probably want to exclude the built version and our node modules from commits, so let’s go ahead and, at the root level of the monorepo, add a .gitignore
file excluding (at least) thenode_modules
, dist
, etc:
dist
/build
.DS_Store
/coverage
node_modules
.env.
npm-debug.log*
yarn-debug.log*
yarn-error.log*
We need to install react now:
Note that we do save those as regular dependencies, i.e. without --save-dev
option.
Babel
Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. For example, Babel transforms syntax:
// Babel Input: ES2015 arrow function
[1, 2, 3].map(n => n + 1);
// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
return n + 1;
});
To install Babel in our project, go to the top directory of our monorepo project and run
@babel/core
is the main babel package - We need this for babel to do any transformations on our code. @babel/cli
allows us to compile files from the command line. preset-env
and preset-react
are both presets that transform specific flavors of code - in this case, the env
preset allows us to transform ES6+ into more traditional javascript and the react preset does the same, but with JSX instead. @babel/preset-typescript
is used by Jest we setup later, because we will need to transpile Jest into TypeScript via Babel
The following 2 links explains in details why the 4 dependencies above are needed in our react app:
In our monorepo project root, create a Babel configuration file called babel.config.json. Here, we’re telling babel that we’re using the env
and react
presets (and some typescript support for Jest testing which we discuss later):
{
"presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }], "@babel/preset-typescript"]
}
TypeScript
We integrate TypeScript by going to the top directory of our monorepo project and running
Let’s set up a configuration to support JSX and compile TypeScript down to ES5 by creating a file called tsconfig.json in project root directory with the content of:
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"strict": true,
"allowJs": true,
"jsx": "react-jsx",
"outDir": "./dist/",
"noImplicitAny": true,
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["packages"]
}
See TypeScript’s documentation to learn more about tsconfig.json configuration options. The thing we need to mention here is the "jsx": "react-jsx"
option. In short, react-jsx
is a more-modern option compared to other such as old react
and we will use this newer feature.
Jest
Let’s jump into test setup with Jest. At the monorepo root directory run
Jest transpiles TypeScripts before running test harness. We will need a configuration file to specify how TypeScript is going to be transpiled. The file name is jest.config.json:
{
"preset": "ts-jest",
"testEnvironment": "jsdom",
"setupFilesAfterEnv": ["<rootDir>/scripts/jest/setupTests.ts"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"moduleNameMapper": {
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
}
}
-
"preset": "ts-jest"
and"testEnvironment": "jsdom"
are neede by ts-jest config -
setupFilesAfterEnv
: This is related to the@testing-library/jest-dom
dependency. In terms of Jest configuration, rather than import it in every test file it is better to do it in the Jest config file via thesetupFilesAfterEnv
option and then we will have a setupTests.ts file located insidescripts/jest
directory with the following contentimport "@testing-library/jest-dom"; -
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
: This was taken from an ejected CRA that specifies how to mock out CSS imports. We should have a file calledcssTransform.js
under<rootDir>/config/jest/
directory with the following contents:"use strict";module.exports = {process() {return "module.exports = {};";},getCacheKey() {// The output is always the same.return "cssTransform";},};Similarly, the next line specifies file mock (same location with file name of
fileTransform.js
):"use strict";const path = require("path");const camelcase = require("camelcase");// This is a custom Jest transformer turning file imports into filenames.// http://facebook.github.io/jest/docs/en/webpack.htmlmodule.exports = {process(src, filename) {const assetFilename = JSON.stringify(path.basename(filename));if (filename.match(/\.svg$/)) {// Based on how SVGR generates a component name:// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6const pascalCaseFilename = camelcase(path.parse(filename).name, {pascalCase: true,});const componentName = `Svg${pascalCaseFilename}`;return `const React = require('react');module.exports = {__esModule: true,default: ${assetFilename},ReactComponent: React.forwardRef(function ${componentName}(props, ref) {return {$$typeof: Symbol.for('react.element'),type: 'svg',ref: ref,key: null,props: Object.assign({}, props, {children: ${assetFilename}})};}),};`;}return `module.exports = ${assetFilename};`;},}; -
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
: Mocking CSS Modules
Additional Jest References
Webpack
We configure Webpack now. We’ll need a few more packages as dev dependencies. Run the following commands at the root directory of our monorepo:
Setup Webpack Dev Server
We’ve mentioned previously the need to “build our files or serve them somehow during development for our app to work”. Essentially, we will need to achieve this by enabling yarn start
command using Webpack Dev Server. First, we put a config file of it under config/webpack
called webpack.config.js:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || "10000");
module.exports = function (webpackEnv) {
const isProdEnvironment = webpackEnv === "production";
return {
entry: "./packages/app/src/index.tsx",
mode: isProdEnvironment ? "production" : "development",
output: {
publicPath: "/",
path: path.resolve(__dirname, "dist"),
filename: isProdEnvironment ? "static/js/[name].[contenthash:8].js" : "static/js/bundle.js",
},
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
exclude: /(node_modules|bower_components)/,
loader: "babel-loader",
options: { presets: ["@babel/env"] },
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.webp$/],
type: "asset",
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: "./packages/app/public/index.html",
},
isProdEnvironment
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
],
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
},
};
};
entry
tells Webpack where our application starts and where to start bundling our files. The following line lets webpack know whether we’re working in development mode or production build mode. This saves us from having to add a mode flag when we run the development server.
The module
object helps define how our exported javascript modules are transformed and which ones are included according to the given array of rules.
Our first rule is all about transforming ES6 and JSX syntax. The test and exclude properties are conditions to match file against. In this case, it’ll match anything outside of the node_modules
and bower_components
directories. Since we’ll be transforming our .js
and .jsx
files as well, we’ll need to direct Webpack to use Babel. Finally, we specify that we want to use the env
preset in options.
The next rule is for processing CSS. Since we’re not pre-or-post-processing our CSS, we just need to make sure to addstyle-loader
and css-loader
to the use property. css-loader
requires style-loader
in order to work.
We want to use Hot Module Replacement so that we don’t have to constantly refresh to see our changes. All we do for that in terms of this file is instantiate a new instance of the plugin in the plugins property, i.e. new webpack.HotModuleReplacementPlugin()
We need to add an index.html
to our webpak config, so it can work with it; otherwise webpack-dev-server will simply get us a blank screen with yarn start
1. We use html-webpack-plugin for this.
The new HtmlWebpackPlugin(...)
snippet above was taking from an ejected CRA.
The resolve
property allows us to specify which extensions Webpack will resolve - this allows us to import modules without needing to add their extensions2. For example, we can safely put
import App from "./App"
when we have a file App.tsx
. Without resolve
above, import above will throw a runtime-error because only App.js
can be imported without specifying an extension.
We are going to use dev-server through the Node.js API. But before we proceed, it is worth mentioning that the webpack.config.js
file above does not have the devServer
field.
Because the field is ignored if we run dev server using Node.js API. We will, instead, pass the options as the first parameter: new WebpackDevServer({...}, compiler)
. For separation of concerns, the option is defined in yet another config file called webpackDevServer.config.js (located in the same directory as webpack.config.js) and this file will be imported in Node.js API file:
"use strict";
module.exports = function () {
return {
historyApiFallback: true,
};
};
"use strict";
const configFactory = require("../config/webpack/webpack.config");
const devServerConfig = require("../config/webpack/webpackDevServer.config");
const Webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const webpackConfig = configFactory("development");
const compiler = Webpack(webpackConfig);
const devServerOptions = { ...devServerConfig(), open: true };
const server = new WebpackDevServer(devServerOptions, compiler);
server.startCallback(() => {
console.log("Starting server on http://localhost:3000");
});
We put this in scripts/start.js
so that we will be able to call this script during yarn start
by adding the following line to package.json
:
"scripts": {
"start": "node scripts/start.js",
...
},
Creating a Production Build
We will use yarn build
to create a build
directory with a production build of our app. Inside the build/static
directory will be our JavaScript and CSS files. Each filename inside of build/static
will contain a unique hash of the file contents. This hash in the file name enables long term caching techniques, which allows us to use aggressive caching techniques to avoid the browser re-downloading our assets if the file contents haven’t changed. If the contents of a file changes in a subsequent build, the filename hash that is generated will be different.
"use strict";
const Webpack = require("webpack");
const configFactory = require("../config/webpack/webpack.config");
const webpackConfig = configFactory("production");
const compiler = Webpack(webpackConfig);
console.log("Creating an optimized production build...");
compiler.run();
We put this in scripts/build.js
so that we will be able to call this script during yarn build
by adding the following line to package.json
:
"scripts": {
...
"build": "node scripts/build.js",
...
},
App Package
Next, in the new project folder, create the following directory:
mkdir -p packages/app/
Next, create the following structure inside packages/app/
.
+-- public
+-- src
Our public
directory will handle any static assets, and most importantly houses our index.html
file, which react will utilize to render our app. The following code is an example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.json" />
<link rel="shortcut icon" href="./favicon.ico" />
<title>My App</title>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
</body>
</html>
The manifest.json
and favidon.ico
will be placed in the same directory as the index.html
, i.e. the public
directory.
The
manifest.json
provides metadata used when our web app is installed on a user’s mobile device or desktop.
packages/app/src/App.tsx
The TypeScript code in App.tsx creates our root component. In React, a root component is a tree of child components that represents the whole user interface:
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import MyHomeComponent from "somePathTo/MyHomeComponent";
import MySettingsPageComponent from "somePathTo/MySettingsPageComponent";
export default function App(): JSX.Element {
return (
<Router>
<Routes>
<Route path="/" element={<MyHomeComponent />} />
<Route path="/settings" element={<MySettingsPageComponent />} />
</Routes>
</Router>
);
}
packages/app/src/index.tsx
index.tsx is the bridge between the root component and the web browser.
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
packages/app/src/index.css
This file defines the styles for our React app. Here is an example:
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
Now that we’ve got our HTML page set up, we can start getting serious. We’re going to need to set up a few more things. First, we need to make sure the code we write can be compiled, so we’ll need Babel, which we discuss next.
Upgrade to Yarn 2
https://yarnpkg.com/migration/guide#migration-steps
Project Configuration Management
This document describes Configuration Management for our monorepo project.
Context
Configuration management is essential for every application that will be deployed into multiple environments, which is pretty much the majority of Apps and APIs. Focusing on React Apps in this post, we will cover how to store configurations in Nexus Graph, how to configure them, and finally how to read them.
Configuration Types
There are different types of configurations in Nexus Graph
Environment Dependent
Those are configurations that change from one environment to the other. A good example would be FQDNs (Fully Qualified Domain Names). A URL in local dev environment, might point to https://localhost:6500
, however the same URL in production, would point to https://theresa-api.com
.
Storing Environment Dependent Configs
The way we manage these types of configurations is through env files. We maintain a separate env file for each environment.
We have:
- .env: for local dev environment
- .env.test: for test environment
- .env.production: for production
When the application is packaged for each environment by WebPack, the right configuration file will be picked up. The content of such file is key-value pair, such as below:
HTTPS=true
PORT=8500
HOST=localhost
REACT_APP_API_URL=https://localhost:6011
REACT_APP_API_PORTAL_SUBSCRIPTION=NotRequiredForLocalUse
REACT_APP_INSTRUMENTATION_KEY=NotApplication
PUBLIC_URL=https://localhost:8500
EXTEND_ESLINT = true
REACT_APP_Environment=development
For .env.(environment), we should have the same set of keys in each file, with different values specific to that environment.
Reading from .env File
We will use dotenv in our project
-
Create .env file at the root of project
API_URL=http://localhost:8000 -
Install dotenv
yarn add dotenv -
Config webpack to add env variables
const webpack = require('webpack');const dotenv = require('dotenv');module.exports = () => {// call dotenv and it will return an Object with a parsed keyconst env = dotenv.config().parsed;// reduce it to a nice object, the same as beforeconst envKeys = Object.keys(env).reduce((prev, next) => {prev[`process.env.${next}`] = JSON.stringify(env[next]);return prev;}, {});return {plugins: [new webpack.DefinePlugin(envKeys)]};
Lastly, to access these config values, we simply use process.env.(key-name), such as process.env.API_URL
Static Configurations
Static configurations are the ones that don’t change from one environment to the other. Examples would be telephone numbers, company names, messages and copies, etc. We simply store these values in a separate file, as they might be subject to change from time to time, and this makes it easier to find and change them.
Storing Static Configuration Values
One way to manage these configs, is to store them in simple json file, such as below:
{
"locations": {
"fetchCountriesUrl": "v1/countries"
},
"seo": {
"domain": "https://test.com",
"siteName": "Test",
"defaultTitle": "Test | Fast Engineering Eco-Systems with No Compromise",
"defaultDescription": "...",
"contact": {
"email": "info@test.com",
"phone": "+61 2 816238786"
},
"address": {
"streetAddress": "U7 678 Orouke Rd",
"addressLocality": "RedFern",
"addressRegion": "NSW",
"addressCountry": "Australia",
"postalCode": "2000"
}
}
}
JSON structure also enables us to store the values in a specific hierarchy which makes it easier to manage.
Reading Static Config Values
All we need to do to access such config values, is to import it in our .ts/.tsx
files and access the values like any other json object:
import config from '../../config.json';
and then I can write:
config.locations.fetchCountriesUrl
Constants
The other alternative to manage static values in code, is through TS objects. We typically manage two types of values using TS objects:
- To store/read those values that are highly unlikely to change from time to time, but we want to keep them separate from our code anyway
- To store/read those values that won’t be stored in JSON files as they are (and not as strings), such as Regular Expressions (RegEx)
An example would look like below, and you can read them exactly like Option 2, as above:
export const Auth = {
PasswordRegEx: /^(?=.*[A-Z])(?=.*[\W])(?=.*[0-9])(?=.*[a-z]).{8,128}$/,
PasswordFailMessage: "Passwords must have at least 8 characters, 1 lowercase, 1 upper case, 1 number, and 1 special character."
}
Creating a new Package
In general, each sub-package should have 3 basic components to start with:
-
src that contains the package source files
-
package.json which hosts the package specific info and dependencies (and dev dependencies)
-
index.ts
Deep Dive
Inside my-package/index.ts we would simply do something like this:
import MyComponent from './src/MyComponent.tsx';export default MyComponent;and that’s it. This is helpful because inside other components or containers we can do this:
import MyComponent from '../my-package';because it tries to access the index.ts file by default thus not requiring any more info from us. It would import automatically the index.ts file which imports the actual component itself. If we did not have an index.ts file we would have had to do this:
import MyComponent from '../my-package/src/MyComponent';which is kind of awkward. I
At the end of the day, a package fits into a monorepo with the following file structure:
.
└── monorepo/
├── packages/
│ └── my-package/
│ ├── src
│ ├── index.ts
│ └── package.json
├── tsconfig.json
├── package.json
└── ...
Troubleshooting
Some warnings pops up from some test files while running yarn test
:
● Console
console.error
Warning: An update to ToolbarPlugin inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
We need to do 2 things:
- To prepare a component for assertions, we need to wrap the code rendering it and performing updates inside an
act()
call. This makes our test run closer to how React works in the browser. - Someone out there points out using
waitFor
construct, although I have no idea why that works4
For example:
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { act } from 'react-dom/test-utils';
import MyComponent from "./MyComponent";
test("renders component properly", async () => {
act(() => render(<MyComponent />));
await waitFor(() => {
const linkElement = screen.getByText(/My Great App/i);
expect(linkElement).toBeInTheDocument();
});
});