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.

Note

Full-stack frameworks do not require a server.

All the frameworks on this page support client-side rendering (CSR), single-page apps (SPA), and static-site generation (SSG). These apps can be deployed to a CDN or static hosting service without a server. Additionally, these frameworks allow you to add server-side rendering on a per-route basis, when it makes sense for your use case.

This allows you to start with a client-only app, and if your needs change later, you can opt-in to using server features on individual routes without rewriting your app. See your framework’s documentation for configuring the rendering strategy.

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.

Terminal
npx create-next-app@latest

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:

Terminal
npx create-react-router@latest

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:

Terminal
npx create-expo-app@latest

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.
  • RedwoodSDK: 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

Which features make up the React team’s full-stack architecture vision?

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

Terminal
yarn init

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
coverage
node_modules

npm-debug.log*
yarn-debug.log*
yarn-error.log*

.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

docs/.yarn/

/cypress/videos/
/cypress/downloads/

.env.sentry-build-plugin

.idea
.DS_Store

We need to install react now:

Terminal
yarn add react react-dom

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

Terminal
yarn add -D @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/preset-typescript
  • @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"
]
}

Note

The { "runtime": "automatic" } is to prevent the ReferenceError: React is not defined error during Jest unit test. With this config set, we should not need to use import React from 'react', which is a discouraged practice starting from React 17

TypeScript

We integrate TypeScript by going to the top directory of our monorepo project and running

Terminal
yarn add -D typescript @types/react @types/react-dom ts-loader

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

Terminal
yarn add -D jest babel-jest @types/jest ts-jest react-test-renderer @testing-library/react @testing-library/jest-dom

Pitfall

There is a bug in Jest version 28 and we need to make sure to downgrade jest to some 27 version. At the time of writing, the following versions work:

"@types/jest": "^27.5.2",
"jest": "^27.4.3",
"ts-jest": "^27.1.4",
"babel-jest": "^27.4.2",

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 the setupFilesAfterEnv option and then we will have a setupTests.ts file located inside scripts/jest directory with the following content

    import "@testing-library/jest-dom";
  • "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy": Mocking CSS Modules

  • "^.+\\.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 called cssTransform.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";
    },
    };

    Pitfall

    create-react-app abstracts a lot of what makes a React app work away from us - at least without ejecting it. It has been deprecated by React officially. We should not use it anymore.

    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.html

    module.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#L6
    const 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};`;
    },
    };

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:

Terminal
yarn add -D webpack webpack-cli webpack-dev-server style-loader css-loader babel-loader

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 start1. We use html-webpack-plugin for this.

Terminal
yarn add -D html-webpack-plugin

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.jscan 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,
};
};

Note

The historyApiFallback: true above combined with the publicPath: "/" from the webpack.config.js file shall, if we have router defined in our app, enable page refresh on the flight. Otherwise, a 404 error on sub-router page refresh will occur3.

Please checkout Server-side rendering vs Client-side rendering for more background discussion.

Finally, here is our Node.js API file:

"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 React from 'react';
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: 'Poppins', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

Finally, add the yarn workspace configuration to package.json:

"version": "0.1.0",
"engines": {
"node": ">=22.0.0"
},
"workspaces": [
"./packages/app"
],

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.

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:

  1. 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.
  2. 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();
});
});

Footnotes

  1. https://stackoverflow.com/a/44987447/14312712

  2. https://stackoverflow.com/a/63152687/14312712

  3. https://stackoverflow.com/a/72383988

  4. https://stackoverflow.com/a/65811603