For a long time, the Create React App environment was the most popular way of starting new React projects. It does not seem to be maintained anymore, and the new React documentation does not even mention it. Fortunately, there are alternatives.
The documentation mentions solutions such as Next.js. While it is a good solution, it includes a Node.js component that implements Server-Side Rendering. SSR pre-renders pages on the server before sending them to the client. If you want to avoid that and create applications similar to the ones built with the Create React App, another alternative exists.
Introducing Vite
Vite is a build tool that aims to deliver a reliable and fast development experience by giving up on Webpack in favor of Rollup and esbuild. One of the advantages of this solution is a significantly shorter build time.
Starting a React project with TypeScript
To start a new project with Vite, we need to run the following command:
1 |
npm create vite@latest |
Once we do that, Vite asks us to make a few choices, such as the framework we want to use. Vite works not only with React but also with many other frameworks, such as Svelte and Vue. In fact, Vite is maintained by the Vue team.
The next significant question Vite asks us is whether or not we want to use the Speedy Web Compiler. SWC is a JavaScript and TypeScript compiler written in Rust. It aims to replace Babel and the original TypeScript compiler and can provide a significant speed boost.
SWC only transpiles the TypeScript code. Vite still uses the original TypeScript compiler to perform type checking.
The only thing left to start our project is to install the dependencies using npm install and run npm run dev afterward. By default, this makes our application available at http://localhost:5173/
Using environment variables
Through environment variables, we can store values that can differ across various environments our application runs in, such as the testing environment and production. Another important advantage of environment variables is that we don’t include them in the source code pushed to our Git repository. Thanks to that, we can keep them secret.
A good example of an environment variable is the URL of our API because it will be different when testing and on production. The most straightforward way of adding an environment variable is through the .env file.
.env
1 |
VITE_API_URL=https://jsonplaceholder.typicode.com |
All environment variables that don’t have the VITE_ prefix are not available to our frontend application to prevent leaking them accidentally.
Let’s include the .env file in our .gitignore to ensure we don’t commit it to the repository by accident.
.gitignore
1 2 |
.env # ... |
Vite provides some TypeScript definitions for environment variables under the hood by default.
node_modules/vite/types/importMeta.d.ts
1 2 3 4 5 6 7 8 |
interface ImportMetaEnv { [key: string]: any BASE_URL: string MODE: string DEV: boolean PROD: boolean SSR: boolean } |
We can improve the above type by modifying the vite-env.d.ts file in our project.
vite-env.d.ts
1 2 3 4 5 |
/// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_URL: string; } |
With the <reference types="vite/client" /> triple slash directive, we are adjusting the types built into Vite and adding the VITE_API_URL property.
When we look at the code Vite serves to the browser, we can see that the environment variables are available through the import.meta.env object.
1 2 3 4 5 6 7 8 |
import.meta.env = { "VITE_API_URL": "https://jsonplaceholder.typicode.com", "BASE_URL": "/", "MODE": "development", "DEV": true, "PROD": false, "SSR": false }; |
You might notice that it is slightly different from Create React App where we need to use the process.env object instead.
Let’s create a simple function that fetches a list of posts using our environment variable.
fetchPosts.tsx
1 2 3 4 5 6 7 |
export async function fetchPosts() { const response = await fetch(`${import.meta.env.VITE_API_URL}/posts`); if (!response.ok) { throw new Error(`Request failed with status ${response.statusText}`); } return response.json(); } |
Testing using the React Testing Library and Vitest
Jest is currently the most popular JavaScript testing framework. While it can work with Vite, there is an alternative worth considering.
Vitest is a tool that aims to be a drop-in replacement for Jest. Since Vitest was built with Vite in mind, it was adjusted to its architecture. This allows it to be fast and reliable.
Let’s create a straightforward component that renders a list of posts so that we can test it.
PostsList.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { usePostsList } from './usePostsList'; export const PostsList = () => { const { didErrorHappen, posts } = usePostsList(); if (didErrorHappen) { return <div>An error happened when loading the list of posts.</div>; } return ( <div> {posts.map((post) => { return ( <div key={post.id} data-testid="post"> <h2>{post.title}</h2> <p>{post.body}</p> </div> ); })} </div> ); }; |
Under the hood, we use the fetchPosts function we wrote before.
usePostsList.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { useEffect, useState } from 'react'; import { fetchPosts } from './fetchPosts'; export interface Post { id: number; title: string; body: string; } export function usePostsList() { const [posts, setPosts] = useState<Post[]>([]); const [didErrorHappen, setDidErrorHappen] = useState(false); useEffect(() => { setPosts([]); setDidErrorHappen(false); fetchPosts() .then((responseData) => { setPosts(responseData); }) .catch(() => { setDidErrorHappen(true); }); }, []); return { posts, didErrorHappen, }; } |
Setting up the testing environment
To be able to write unit tests for our component, we will need a set of libraries.
1 |
npm install vitest jsdom @testing-library/react |
The first step is to adjust the configuration of Vite.
vite.config.ts
1 2 3 4 5 6 7 8 9 |
import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react-swc'; export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', }, }); |
The first thing to notice is that we are using the defineConfig function from vitest/config, not vite. This is because we need to provide the environment property to our configuration.
By specifying environment: 'jsdom', we are adding jsdom to our unit tests. It is a JavaScript implementation of DOM and browser APIs that allows us to test how our React components would interact with a real browser.
We also have to add a script that runs Vitest to our package.json.
package.json
1 2 3 4 5 6 7 |
{ "scripts": { // ... "test": "vitest" }, // ... } |
Creating a basic test
Thanks to our setup, we can now create our first simple test.
PostsList.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { fetchPosts } from './fetchPosts'; import { render } from '@testing-library/react'; import { PostsList } from './PostsList'; import { Post } from './usePostsList'; vi.mock('./fetchPosts', () => ({ fetchPosts: vi.fn(), })); describe('The PostsList component', () => { describe('when the fetchPosts function returns a valid list of posts', () => { let posts: Post[]; beforeEach(() => { posts = [ { id: 1, title: 'The first article', body: 'The body of the first article', }, { id: 2, title: 'The second article', body: 'The body of the second article', }, ]; (fetchPosts as Mock).mockResolvedValue(posts); }); it('should render the posts', async () => { const { findByText } = render(<PostsList />); for (const post of posts) { const findTitleResult = await findByText(post.title); const findBodyResult = await findByText(post.body); expect(findTitleResult).toBeDefined(); expect(findBodyResult).toBeDefined(); } }); }); }); |
✓ The PostsList component
✓ when the fetchPosts function returns a valid list of posts
✓ should render the posts
If we’re used to writing tests with Jest, we might notice that we have to import functions such as describe and beforeEach explicitly. While this is a default behavior or Vitest, we could change that by adding globals: true to the testing configuration in the vite.config.ts file.
Adding more test cases
Let’s test what happens when fetching the posts fails.
PostsList.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { fetchPosts } from './fetchPosts'; import { render } from '@testing-library/react'; import { PostsList } from './PostsList'; import { Post } from './usePostsList'; vi.mock('./fetchPosts', () => ({ fetchPosts: vi.fn(), })); describe('The PostsList component', () => { describe('when the fetchPosts function returns a valid list of posts', () => { // ... }); describe('when the fetchPosts function throw an error', () => { beforeEach(() => { (fetchPosts as Mock).mockRejectedValue(Error()); }); it('should render the error message', async () => { const { findByText } = render(<PostsList />); const findErrorMessageResult = findByText( 'An error happened when loading the list of posts.', ); expect(findErrorMessageResult).toBeDefined(); }); it('should not render any posts', () => { const { queryAllByTestId } = render(<PostsList />); const result = queryAllByTestId('post'); expect(result.length).toBe(0); }); }); }); |
The PostsList component
✓ when the fetchPosts function returns a valid list of posts
✓ should render the posts
when the fetchPosts function throw an error
✓ should render the error message
× should not render any posts
Unfortunately, the test that ensures no posts are rendered when there is an error fails. Even worse, it fails only when we run all our tests in the sequence. It passes without problems when we’re running it in isolation.
We are experiencing this problem because we rendered the posts in our previous tests, and they are still present in the DOM tree. We must explicitly configure Vitest to reset the DOM tree after each test. To do that, let’s create a setup file.
tests-setup.tsx
1 2 3 4 |
import { afterEach } from 'vitest'; import { cleanup } from '@testing-library/react'; afterEach(cleanup); |
We also have to point to it in our Vite configuration.
vite.config.ts
1 2 3 4 5 6 7 8 9 10 |
import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react-swc'; export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./tests-setup.tsx'], }, }); |
Thanks to that, we can keep our tests independent of each other when we use the jsdom library and simulate the DOM tree.
Summary
In this article, we’ve reviewed how to set up React with Vite and TypeScript. We started by explaining the basic setup steps but also dealt with some potential challenges. One example was using environment variables while ensuring they were typed with TypeScript. Another important aspect was testing with the Vitest framework. Even though it is meant to be a replacement for Jest, it might require some slight adjustments.
With all that knowledge, we are ready to build projects with Vite. It provides a fast and reliable environment focused on the overall development experience.
i followed you in linkedIn as well. keep it up. I learned a lot from you. i really appreciate your more content.
Thanks a lot!