Testing & Deploying a React App
Testing Philosophy
Test components the way users use them: find things by their visible role/text, interact, assert on the result. Don’t test implementation details (state values, internal calls) — those tests break on every refactor while catching nothing.
npm install -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom// vite.config.js — add a test block
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: "./src/test/setup.js", // imports jest-dom matchers
},
});A Component Test
// Counter.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";
test("increments when clicked", async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole("button", { name: /clicked 0 times/i });
await user.click(button);
expect(screen.getByRole("button", { name: /clicked 1 time/i })).toBeInTheDocument();
});Query priority: getByRole (also enforces accessibility!) → getByLabelText (forms) → getByText → getByTestId (last resort).
Testing a Form
test("submits the email", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<SignupForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), "[email protected]");
await user.click(screen.getByRole("button", { name: /sign up/i }));
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ email: "[email protected]" })
);
});Testing Async Components
test("shows courses after loading", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => [{ id: 1, title: "Django" }],
});
render(<CourseList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText("Django")).toBeInTheDocument(); // findBy = waits
});(Bigger apps mock the network with MSW — Mock Service Worker — instead of patching fetch per test.)
Production Build
npm run build # → dist/ — minified, hashed, tree-shaken
npm run preview # serve the build locally to sanity-checkDeploying
A Vite SPA is static files — any static host works:
- Vercel / Netlify — connect the Git repo; build command
npm run build, outputdist. Every push deploys. Add a rewrite of/*→/index.htmlso React Router handles deep links. - Nginx (self-hosted, e.g. alongside a Django API):
location / {
root /srv/myapp/dist;
try_files $uri /index.html; # SPA fallback
}
location /api/ {
proxy_pass http://127.0.0.1:8000; # your backend
}Environment config: Vite exposes import.meta.env.VITE_API_URL from .env files — set per environment, never commit secrets (anything VITE_ is public in the bundle).
You Now Have the Full React Toolkit
Components, state, effects, hooks, context, reducers, routing, server data, performance, tests, deployment. Two natural next steps on this site: the Next.js series (React with server rendering and file-based routing) and wiring a React frontend to the Django REST API you built in the Django series. Build something real — that’s where it all clicks.