Noh | エンジニア向け情報共有コミュニティ
Signup / Login

Create a React SPA site with Vite

javascript
typescript
vite
react

投稿日: 2024/03/12

更新日: 2024/07/02

This article is a translation of the following article.

https://noh.ink/articles/m1SpHhdyxoExLxyFHEdM

Introduction

The last release of create-react-app was in 2022 and it has been removed from the official documentation. While frameworks like Next.js are generally recommended, there are times when you want a more lightweight setup. So, we will create an environment with Vite + React.

Basically, we will proceed according to the official guide, so it is recommended to also refer to the official guide.

This is the repository I created.

I used node version 20.11.0.
I used anyenv for installing node, but any environment where node can run is acceptable around here.

$ nodenv local 20.11.0 $ nodenv -v nodenv 1.4.1+79.15375bb $ node -v v20.11.0 $ npm -v 10.2.4

Here is the list of versions of the packages being used this time.

package.json

"dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.3" }, "devDependencies": { "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", "@types/node": "^20.11.26", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "jsdom": "^24.0.0", "typescript": "^5.2.2", "vite": "^5.1.6", "vitest": "^1.3.1" }

Setting Up Vite and React Environment

I will create a project using a template. This time, I used react-ts, but there are also various other options available.
It seems that when using npm 7 or higher, four hyphens are required as follows.

$ npm create vite@latest vite-project -- --template react-ts

Run npm install, and start the development server by running npm run dev.

$ cd vite-project $ npm i $ npm run dev > [email protected] dev > vite VITE v5.1.6 ready in 96 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help

When you access http://localhost:5173/, you can see the page. (It might be http://localhost:3000/.)

Setting up prettier code formatter

I want to enable automatic formatting when saving, so I will introduce Prettier.

Please follow the instructions in the documentation if you want to format using a command.

I will install extensions because I automatically format when saving in VSCode.

I will change the settings of VSCode so that the file is formatted when saved after adding extensions.

Select Settings icon.

Search for defaultformatter and change the Default Formatter to Prettier.

Search for format on save and check the box for Format On Save.

Add .prettierrc.json at the end. Please configure it as you like.

.prettierrc.json

{ "printWidth": 80, "tabWidth": 2, "useTabs": false, "semi": false }

Path alias when importing

By adding the following settings

tsconfig.json

{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } },

vite.config.ts

export default defineConfig({ resolve: { alias: [{ find: "@/", replacement: `${__dirname}/src/` }], },

When you add a Example component like the following, you will be able to write import { Example } from "@/components/Example" in App.tsx.

src ├── App.tsx ├── components │   └── Example.tsx

I will install @types/node because a warning Cannot find name '__dirname'.ts(2304) is displayed.

npm i -D @types/node

By the way, separate settings are required to run it for testing, and they will be described later in the testing section. If you find these settings troublesome, using vite-tsconfig-paths seems like a good option.

Test

Introduction of Vitest and React Testing Library

This time, we will be using Vitest. Jest is also fine, but I really like Vitest because it works with concise configuration.
In order to write tests for React, we will also introduce React Testing Library. jsdom is installed as it is necessary for testing components.

npm install -D vitest @testing-library/react @testing-library/user-event jsdom

Next, we will add configurations to the package.json so that you can run the tests using the npm run test command.

package.json

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

fireEvent and userEvent

There are fireEvent and userEvent methods that can be used to test events such as clicking a button within a component. It is generally recommended to use userEvent.

fireEvent is a low-level event trigger function that dispatches DOM events, while userEvent provides a higher-level simulation to more accurately replicate actual user interactions.

Write actual tests

First, I will add the necessary settings for the test.
The contents written in vitest.config.ts can also be written in vite.config.ts, but I believe that separating the test settings into a separate file makes it look cleaner, so I have separated them.

vitest.config.ts

import * as path from "path" import { defineConfig } from "vitest/config" export default defineConfig({ test: { globals: true, environment: "jsdom", alias: { "@": path.resolve(__dirname, "./src"), }, }, })

tsconfig.json

"paths": { "@/*": ["./src/*"] - } + }, + + /* For Vitest*/ + "types": ["vitest/globals"] }, "include": ["src"],

By adding globals: true and "types": ["vitest/globals"], you will be able to use describe and test functions in each test file without explicitly importing them, similar to Jest.

I am setting up alias: { "@": path.resolve(__dirname, "./src") } to make path aliases work even during testing.

First, we will write a test for a simple function. Add src/helpers/example.ts and src/helpers/example.test.ts.

src/helpers/example.ts

export const example = () => { return "example" }

src/helpers/example.test.ts

import { example } from "./example" describe("example", () => { test("文字列を返す", () => { expect(example()).toEqual("example") }) })

Next, I will write tests for the components. I will add src/App.test.tsx.
This time, I am testing whether a specific string is displayed when a component is rendered and whether the count of characters increases when a button is pressed.

src/App.test.tsx

import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import App from "./App" describe("App", () => { test("renders App component", () => { render(<App />) screen.getByText("Click on the Vite and React logos to learn more") }) test("change count", async () => { const user = userEvent.setup() render(<App />) await user.click(screen.getByRole("button", { name: /count is 0/i })) screen.getByText("count is 1") }) })

I will execute the test (by the way, it is very convenient because the test will be automatically run again when you save the test file).

$ npm run test > [email protected] test > vitest DEV v1.3.1 /home/user_name/projects/github.com/yosipy/mui5_example ✓ src/helpers/example.test.ts (1) ✓ src/App.test.tsx (2) Test Files 2 passed (2) Tests 3 passed (3) Start at 19:23:03 Duration 751ms (transform 166ms, setup 0ms, collect 214ms, tests 45ms, environment 313ms, prepare 251ms) PASS Waiting for file changes... press h to show help, press q to quit

Page Routing with React Router

Since there is only a homepage, I will enable the ability to display other pages.

Installing React Router

Install React Router.

npm i react-router-dom

Introduction

You just need to write as follows. You can add routing inside createBrowserRouter.

src/main.tsx

import React from "react" import ReactDOM from "react-dom/client" import { createBrowserRouter, RouterProvider } from "react-router-dom" import "./index.css" const router = createBrowserRouter([ { path: "/", element: <div>Hello world!</div>, }, ]) ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> )

Example

Let's try displaying the App component that was initially used.

src/main.tsx

import React from "react" import ReactDOM from "react-dom/client" import { createBrowserRouter, RouterProvider } from "react-router-dom" import "./index.css" import App from "./App" const router = createBrowserRouter([ { path: "/", element: <div>Hello world!</div>, }, { path: "/app", element: <App />, }, ]) ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> )

You can view it at http://localhost:5173/app.

CI

Set up to automatically run tests when pushing using Github Actions.

.github/workflows/ci.yml

name: CI on: push: jobs: frontend-test: name: Frontend test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - name: npm ci run: npm ci - name: Lint run: npm run lint - name: Run test run: npm run test

It feels good.

Here we are not currently doing it, but it is a good idea to cache the result of npm install using actions/cache. Additionally, it is recommended to set up rules so that you cannot merge into the main branch when tests fail (by the way, you can set up branch protection rules for free in public repositories, but not in private repositories for free).

Table of Contents