In 2023, starting a new React app involves embracing cutting-edge tools and techniques that not only boost productivity but also enhance code quality.

Whether you’re a seasoned developer or just getting started, this comprehensive guide will walk you through the process of bootstrapping a new React app with the minimum recommended to create your app with confidence.

We’ll cover important steps that are both essential, including:

  • Bootstrapping the app with the lightning-fast Vite instead of the traditional create-react-app utility.
  • Installing a CSS framework like Tailwind CSS,
  • Making a robust foundation for your code through effective linting and formatting with ESLint and Prettier.
  • Ensuring code stability and functionality with comprehensive unit, end-to-end, and component tests using tools like Vitest and Cypress.
  • Incorporating Storybook for isolated and focused component development,
  • Implementing a CI/CD pipeline with GitHub Actions.

By following these steps, you will be able to ensure a solid foundation for your upcoming React projects.

Create a git repository

Setting up a proper version control system for your project is still crucial before going any further. This would allow you to save your modifications and revert them when needed, but also to share your progress and collaborate with other developers around the world.

Assuming that you’ve created a new repository on GitHub called my-app, under the account unnamedcoder, you can initialize a new git repository locally by following the provided instructions.

$ mkdir my-app && cd my-app
$ git init
$ git remote add origin git@github.com:unnamedcoder/my-app.git

Use Vite to bootstrap your app

We’ve been using create-react-app for years, but it’s time to move on. New alternatives like Vite offer several advantages, including faster performance, improved configuration flexibility, native support for modern web standards and compatibility with other popular frontend frameworks.

Bootstrapping a new React application using TypeScript with Vite is as simple as running the following command:

$ npm init vite@latest . -- --template react-ts

Need to install the following packages:
  create-vite@4.3.1
Ok to proceed? (y) y

Scaffolding project in /home/unnamedcoder/my-app...

Done. Now run:

  npm install
  npm run dev

As stated above, we just have to execute npm install to have our dependencies ready, then npm run dev to run our fresh new app.

$ npm run dev

  VITE v4.3.7  ready in 326 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

Open your favorite web browser, then head to the URL mentioned in the output. You should see the default React app generated by Vite running flawlessly.

Everything is already configured, including the hot module replacement allowing the app to reload automatically every time you do a modification. You won’t need to execute npm run eject anymore.

Install a CSS framework

Writing pure CSS code is fun, but also time-consuming and error-prone. It looks like writing assembly code to me. But thanks to Vite, we can directly write SASS, SCSS or LESS code in our React components.

However, you may want to use a CSS framework to speed up your development such as Tailwind CSS, now is your chance to finally take a look at this marvel.

$ npm install -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p

Add the following lines at the bottom of the src/index.css file:

@tailwind base;
@tailwind components;
@tailwind utilities;

Configure Tailwind CSS to look for index.html and every kind of source code inside the src folder by applying the following modifications inside the tailwind.config.js file:

 /** @type {import('tailwindcss').Config} */
 export default {
-  content: [],
+  content: [
+    "./index.html",
+    "./src/**/*.{js,ts,jsx,tsx}",
+  ],
   theme: {
     extend: {},
   },
   plugins: [],
 }

Now you can use Tailwind CSS classes in your React components like in the following example:

<h1 className="text-3xl font-bold underline">
  Hello world!
</h1>

Feel free to explore their website for more documentation, configuration guides and best practice tips. You can also check out Tailwind UI which offers a curated collection of beautifully designed components and templates to give you more inspiration.

Setup code linting

Always assume that you are working with other people, even if you’re still a beginner or working on a personal project. By setting up a code linter, you will ensure consistency across your codebase, avoid common mistakes and ease the onboarding of new developers by helping them sending pull requests.

We’re lucky that Vite already comes with a built-in ESLint configuration, but there are still more useful rules to apply:

  • eslint-plugin-import to check import/export syntax and prevent issues with misspelling of file paths and import names
  • eslint-plugin-jsx-a11y to check accessibility rules in JSX elements to ensure inclusive, user-friendly applications
  • eslint-plugin-react to enforce React-specific coding patterns and best practices, helping to maintain consistent and high-quality code

You can apply these rules by installing the following packages:

$ npm install -D eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

And update your .eslintrc.cjs file with the following content:

 module.exports = {
   env: { browser: true, es2020: true },
   extends: [
     'eslint:recommended',
     'plugin:@typescript-eslint/recommended',
     'plugin:react-hooks/recommended',
+    'plugin:react/recommended',
+    'plugin:import/recommended',
+    'plugin:jsx-a11y/recommended',
   ],
   parser: '@typescript-eslint/parser',
   parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
   plugins: ['react-refresh'],
+  settings: {
+    react: {
+      version: 'detect',
+    },
+    'import/resolver': {
+      node: {
+        paths: ['src', 'public'],
+        extensions: ['.js', '.jsx', '.ts', '.tsx'],
+      },
+    },
+  },
   rules: {
     'react-refresh/only-export-components': 'warn',
   },
 }

Running the npm run lint -- --fix command should output the following:

$ npm run lint -- --fix

> my-app@0.0.0 lint
> eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix


/home/unnamedcoder/my-app/src/App.tsx
  3:22  error  Unable to resolve path to module '/vite.svg'  import/no-unresolved

✖ 1 problem (1 error, 0 warnings)

This error still occurs because of the vite.svg file imported in the App.tsx component. I couldn’t find a way to fix this issue, so we need to disable this rule exceptionally for this import by adding the following comment at the top of it:

 import { useState } from 'react'
 import reactLogo from './assets/react.svg'
+// eslint-disable-next-line import/no-unresolved
 import viteLogo from '/vite.svg'
 import './App.css'

Running npm run lint again should produce no output, thus confirming that everything is working as expected.

Setup code formatting

We’ve been expressing concerns about the misuse of ESLint, which was primarily designed for detecting syntax errors and enforcing code rules. It was often utilized for code formatting as well, a task for which it was not specifically tailored, leading to a lot of confusion and frustration.

The introduction of Prettier as a complementary tool to ESLint has addressed this issue. Prettier is a dedicated code formatter that automatically formats code according to a consistent style, allowing ESLint to focus on its core purpose of identifying and preventing potential coding issues.

You can install Prettier by running the following command:

$ npm install -D prettier eslint-config-prettier

And disable ESLint rules that might conflict with Prettier by updating the extends section of your .eslintrc.cjs file:

   extends: [
     'eslint:recommended',
     'plugin:@typescript-eslint/recommended',
     'plugin:react-hooks/recommended',
     'plugin:react/recommended',
     'plugin:import/recommended',
     'plugin:jsx-a11y/recommended',
+    'plugin:prettier/recommended',
   ],
   // ...
   rules: {
+    'prettier/prettier': 'warn',
     'react-refresh/only-export-components': 'warn',
   },

Next time you’ll run the npm run lint command, you shall see a lot of warnings about code formatting. You can fix them by running the npm run lint -- --fix or prettier -c -w . commands.

Write unit tests

This step is highly recommended to keep your application working as intended and avoid breaking it when you will be adding new features or fixing bugs.

You don’t have to take snapshots, mock states, effects or internal functions of your components anymore. Unit testing became as simple as imitating user’s behavior and interactions over your components, thanks to the React Testing Library.

Vitest also simplifies the process by providing a built-in configuration for Jest, eliminating the struggle of setting up the testing environment, babel config, file transforms, … with the following steps:

$ npm install -D vitest @vitest/ui @vitest/coverage-c8 \
  @testing-library/react \
  @testing-library/jest-dom \
  @testing-library/user-event \
  @types/react @types/react-dom jsdom

Add the following entries inside the scripts section of your package.json file:

   "scripts": {
     "dev": "vite",
     "build": "tsc && vite build",
     "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
-    "preview": "vite preview"
+    "preview": "vite preview",
+    "test": "vitest",
+    "test:coverage": "vitest --coverage",
+    "test:ui": "vitest --ui"
   },

Update your vite.config.ts by adding the testing configuration:

+/// <reference types="vitest" />

 import { defineConfig } from 'vite'
 import react from '@vitejs/plugin-react'

 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [react()],
+  test: {
+    globals: true,
+    environment: 'jsdom',
+  },
 })

We know that the default application generated by Vite is a simple counter which can be incremented by clicking on a button. We want to ensure that the button works as intented.

The unit test for this component will be as simple as the following:

// src/App.test.tsx

import { expect, test } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import App from './App.tsx'

test('displays initial counter and increments upon click', async () => {
  render(<App />)

  await userEvent.click(screen.getByText('count is 0'))
  expect(screen.getByRole('button').textContent).toEqual('count is 1')
})

You can run the test by running npm test. You should see the following output:

DEV  v0.31.0 /home/unnamedcoder/my-app

✓ src/App.test.tsx (1)

Test Files  1 passed (1)
     Tests  1 passed (1)
  Start at  11:12:44
  Duration  1.29s (transform 208ms, setup 0ms, collect 398ms, tests 79ms, environment 282ms, prepare 158ms)

You can also execute npm run test:ui to open a user interface and enjoy a more interactive experience. It’s also possible to run npm run test:coverage to check the coverage of your tests.

Add the following line at the bottom of your .gitignore file to avoid pushing the coverage report to your repository:

coverage

Write end-to-end tests

Everytime I’ve been hearing about end-to-end testing, I was immediately remembering about the struggles I had with Selenium, telling my teammates to download the latest version of ChromeDriver, and the pain of writing tests in a very locked down environment.

I believe that you’ve also felt the same way, our prayers have been answered explaining the reason why Cypress has been created.

Cypress is still a wonderful option for end-to-end testing, also providing us a great way to develop our application without depending on the backend, thus focusing on the frontend development in an isolated way.

You can install Cypress by running the following command:

$ npm install -D cypress typescript start-server-and-test

Add the following entry inside the scripts section of your package.json:

   "scripts": {
     "dev": "vite",
     "build": "tsc && vite build",
     "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
     "preview": "vite preview",
     "test": "vitest",
     "test:coverage": "vitest --coverage",
-    "test:ui": "vitest --ui"
+    "test:ui": "vitest --ui",
+    "test:e2e-start": "cypress open --e2e",
+    "test:e2e": "start-server-and-test dev http-get://localhost:3000 test:e2e-start"
   },

The test:e2e script will execute the dev script, wait for the application to be ready on the port 3000 before executing test:e2e-start which will open Cypress in interactive mode.

It is therefore necessary to assign a specific port instead of a random one to our development server by updating our vite.config.ts file:

 export default defineConfig({
   plugins: [react()],
   test: {
     globals: true,
     environment: 'jsdom',
   },
+  server: {
+    host: true,
+    port: 3000,
+  },
 })

Then run the following command to open Cypress for the first time and generate the initial configuration files:

$ cypress open

Once you’ve selected “E2E Testing”, you can close the window and create the first test inside the cypress/e2e/App.cy.ts with the following content:

describe('default vite react app', () => {
  it('increments the counter', () => {
    cy.visit('/')
    cy.get('button').should('have.text', 'count is 0')
    cy.get('button').click().should('have.text', 'count is 1')
  })
})

We also need to specify the base url of our application in the file to match the port we’ve set into the Vite configuration file, by adding the following line inside the cypress.config.ts file:

 import { defineConfig } from "cypress";

 export default defineConfig({
   e2e: {
+    baseUrl: "http://localhost:3000",
     setupNodeEvents(on, config) {
       // implement node event listeners here
     },
   },
 });

Then execute it by running npm run test:e2e. You should now be able to see the default Vite welcome page, with the tests passing.

Your IDE might have troubles while resolving describe, it and cy objects. This can be fixed by creating a tsconfig.json file inside the cypress folder with the following contents:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts"]
}

Write component tests

Jest is still popular to run unit tests for our components. Its popularity is also due to its execution speed thanks to the jsdom dependency which allows it to execute the tests inside a minimal browser and fake DOM environment.

While it’s still a good choice for basic features, it can be a bit limited when you want to check the visibility of an element, from its CSS attribute or its dynamic position on the screen.

You would also have to spend more time mocking unsupported APIs, in order to retrieve some data from cookies of local storage, which is why it would be better to run the tests inside a real browser like the ones your end-users will use.

This is where Cypress saves the day again, by providing us a way to run our component tests inside a real browser.

First, add the following entry inside the scripts section of your package.json file:

   "scripts": {
     "dev": "vite",
     "build": "tsc && vite build",
     "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
     "preview": "vite preview",
     "test": "vitest",
     "test:coverage": "vitest --coverage",
     "test:ui": "vitest --ui",
     "test:e2e-start": "cypress open --e2e",
-    "test:e2e": "start-server-and-test dev http-get://localhost:3000 test:e2e-start"
+    "test:e2e": "start-server-and-test dev http-get://localhost:3000 test:e2e-start",
+    "test:component": "cypress open --component"
   },

Create a new file src/App.cy.tsx with the following content:

import App from './App'

describe('<App />', () => {
  it('displays initial counter and increments upon click', () => {
    cy.mount(<App />)
    cy.get('button').should('have.text', 'count is 0')
    cy.get('button').click().should('have.text', 'count is 1')
  })
})

Then run the following command to open Cypress for the first time and run the component test:

$ npm run test:component

Select React.js as the front-end framework, and Vite as the bundler. At the time of writing this article, Cypress will expect you to have Typescript 4 installed instead of 5. You can safely click on the “Skip” button to continue.

Once you’ve chose your favorite browser and started the test, you should see the component being displayed with the tests passing.

Which framework should you pick? It depends on your needs. In my opinion, I would stick to Jest for very logical code and utility functions not involving anything about React, even if that part is not needed anymore since I would prioritize component functionality.

For any other tests needing a web browser, I would keep using Cypress for both component and E2E tests, while avoiding jsdom at all costs.

By the way, your IDE might have troubles while resolving describe, it and cy objects. This can be fixed by adding the cypress item inside the include array at the bottom of the root tsconfig.json file:

   // ...
   },

-  "include": ["src"],
+  "include": ["src", "cypress"],
   "references": [{ "path": "./tsconfig.node.json" }]
 }

Adopt Storybook

This step is not required at all, but again, recommended. Incorporating Storybook into your development workflow offers a valuable advantage, as it allows isolated component development without the need for a backend to test various states such as success, loading and error.

Storybook can also be used as a true replacement of Jest’s snapshot testing feature, which was only checking the rendered HTML of a component, instead of its true appearance by taking in account the CSS styles applied to it.

Storybook can be installed with its dependencies by running the following command:

$ npx storybook@latest init

You can run it by executing npm run storybook, your favorite web browser will open the served Storybook instance at http://localhost:6006.

Default stories were created during the installation inside the src/stories folder. It can be removed safely, as we will create the story for our App component inside the src/App.stories.tsx file with the following content:

import type { Meta, StoryObj } from '@storybook/react';
import App from './App';

const meta = {
  title: 'App',
  component: App,
  tags: ['autodocs'],
} satisfies Meta<typeof App>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

Then you can see the App component being displayed inside Storybook, with the ability to interact with it by clicking on the counter button.

Now you are able to focus on the component development without the need to start the development server and navigate to the page where it is displayed.

Add a CI/CD pipeline

We’ve set up up code linting and unit testing to our application, now we can make sure that our code is properly written and still works as expected before pushing it to our remote repository.

But there is still an issue, as we have to keep in mind that we are not coding alone, other developers might be willing to contribute to your project by sending bugfixes, new features or enhancements with pull requests.

There is no guarantee that the code they will send will be linted and tested accordingly to your standards, which is why we need to automate this process by adding a CI/CD pipeline.

There are many CI/CD providers available, but I will use GitHub Actions for this article.

Create a new file .github/workflows/main.yml with the following content:

name: Main

on:
  push:
    branches:
      - '*'
  pull_request:
    branches:
      - '*'

This part of the file will tell GitHub Actions to run the pipeline on every push and pull request made to any branch of the repository.

Then we need to add jobs to the pipeline, which will be responsible for running the linting and unit testing tasks after preparing the testing environment.

This can be done by adding the following content to the file:

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 18

    - name: Install dependencies
      run: npm ci

    - name: Run linter
      run: npm run lint

    - name: Run unit tests
      run: npm test

Wrapping up

We’ve seen how to set up a React application from scratch, with the ability to write code in Typescript, lint it, test it through a CI/CD pipeline, which will run the linting and testing tasks on every push and pull request made to any branch of the repository.

This is a good starting point for any React application, but there are still many things to do, such as setting-up an automated release process, monitoring the errors in real-time, implementing visual regression testing, …

Unfortunately, these steps would have made this article too long, but I promise that I will write about them in the future.

Feel free to write a comment below if you have any question or suggestion, also subscribe to my Twitter, Instagram or LinkedIn accounts to get notified when I will publish a new post.

Updated:

Leave a comment