Day 1: React Fundamentals — Build an Auth MFE
Day 1: React Fundamentals — Build an Auth MFE
Audience: Beginner–Intermediate | Duration: ~4h demo + ~4h practice (Windows/PowerShell friendly)
/login
and /signup
. No backend today — we’ll use MSW to mock APIs so you focus on React, not servers.
What You’ll Build & Learn
- Vite + TypeScript scaffolding, project anatomy, dev server
- Routing with React Router (
<Link/>
,<Outlet/>
) - Mocked APIs with MSW (
/api/login
,/api/signup
,/api/users
) - Controlled forms, POST with
fetch
, UX states (loading/errors) - Validation & a11y basics (
aria-*
,role="alert"
) - Small refactor to a reusable
<Input/>
- React Query for a sample users list (cache + loading + error)
- Final assembly: everything working together
Table of Contents
- Windows/PowerShell Setup
- ex1 — Vite + React + TypeScript: Blank Canvas
- ex2 — Routing: /login & /signup
- ex3 — MSW: Mock Server Endpoints
- ex4 — Login Form: Controlled State + POST
- ex5 — Signup Form: Happy Path
- ex6 — UX: Loading, Disabled, Error Banner
- ex7 — Validation & A11y: Email +
aria-*
- ex8 — Refactor: Reusable <Input/>
- ex9 — React Query: Users List
- ex10 — Final Assembly (Sum of All Steps)
- Practice & Next Steps
- Summary
Windows/PowerShell Setup
# Check Node & npm
node -v # Check Node.js is installed
npm -v # Check npm is installed
# If missing, install Node LTS (Windows 10/11)
winget install -e --id OpenJS.NodeJS.LTS
# Install Git if needed
git --version
winget install -e --id Git.Git
# Create a workspace folder and enter it
New-Item -Path . -Name "auth-mfe-day1" -ItemType "Directory"
Set-Location .\auth-mfe-day1
ex1 — Vite + React + TypeScript: Blank Canvas
Goal: Create a minimal, working React app and understand the core files.
Scaffold (PowerShell)
# Create the project with Vite + TS template
npm create vite@latest ex1-basic-template -- --template react-ts # Scaffold React+TS app; when prompted: "Use rolldown-vite (Experimental)?" → No, "Install with npm and start now?" → No
Set-Location .\ex1-basic-template
# Install dependencies and start the dev server
npm install
npm run dev
File: src/main.tsx
import React from "react"; // Import React types (dev-time helpers)
import ReactDOM from "react-dom/client"; // Modern root API (createRoot)
import App from "./App.tsx"; // Root component that renders UI
import "./index.css"; // Global styles (optional)
ReactDOM.createRoot( // Create a root mounted on #root
document.getElementById("root")! // Non-null assertion for TypeScript
).render(
<React.StrictMode> {/* Extra checks/warnings in dev */}
<App /> {/* Render our App component */}
</React.StrictMode>
);
File: src/App.tsx
export default function App() { // A component is just a function
return ( // that returns JSX
<div style={{ maxWidth: 640, margin: "2rem auto", fontFamily: "system-ui" }}>
<h1>Auth MFE — Day 1</h1> {/* Title */}
<ul> {/* Basic content */}
<li>Vite + React + TypeScript</li>
<li>Components return JSX</li>
</ul>
</div>
);
}
ex2 — Routing: /login & /signup
Goal: Introduce React Router — navigation links and nested routes via <Outlet/>
.
Setup (PowerShell)
# Clone ex1 into ex2 folder to keep milestones
Set-Location ..
Copy-Item -Path .\ex1-basic-template -Destination .\ex2-routing -Recurse
Set-Location .\ex2-routing
# Install react-router-dom dependency (REQUIRED)
npm install react-router-dom
File: src/App.tsx
import { Link, Outlet } from "react-router-dom"; // Link for navigation, Outlet renders child route
export default function App() {
return (
<div style={{ maxWidth: 640, margin: "2rem auto", fontFamily: "system-ui" }}>
<h1>Auth MFE — Routing</h1>
<nav style={{ display: "flex", gap: "1rem", marginBottom: "1rem" }}>
<Link to="/login">Login</Link> {/* client-side nav to /login */}
<Link to="/signup">Signup</Link> {/* client-side nav to /signup */}
</nav>
<Outlet /> {/* child route content goes here */}
</div>
);
}
File: src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom"; // Router primitives
import App from "./App";
import Login from "./pages/Login"; // Route components (create files below)
import Signup from "./pages/Signup";
import "./index.css";
const router = createBrowserRouter([ // Route config array
{ path: "/", element: <App />, children: [
{ path: "login", element: <Login /> }, // /login
{ path: "signup", element: <Signup /> } // /signup
]}
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} /> {/* Provide router to the app */}
</React.StrictMode>
);
Files: src/pages/Login.tsx
& src/pages/Signup.tsx
// src/pages/Login.tsx
export default function Login() { // Temporary placeholder
return <p>Login page (form coming soon)</p>;
}
// src/pages/Signup.tsx
export default function Signup() { // Temporary placeholder
return <p>Signup page (form coming soon)</p>;
}
ex3 — MSW: Mock Server Endpoints
Goal: Intercept fetch
calls with MSW so you can develop offline and avoid CORS/config pain.
Setup (PowerShell)
Set-Location ..
Copy-Item -Path .\ex2-routing -Destination .\ex3-msw -Recurse
Set-Location .\ex3-msw
# Install & init MSW (creates public/mockServiceWorker.js)
npm install
npm install -D msw
npx msw init public/ --save --no-open
Files: src/mocks/handlers.ts
& src/mocks/browser.ts
// src/mocks/handlers.ts
import { http, HttpResponse } from "msw"; // Declarative handlers + response helpers
export const handlers = [
http.post("/api/login", async ({ request }) => { // Login endpoint
const body = await request.json() as { email: string; password: string };
if (body.email === "user@example.com" && body.password === "password") {
return HttpResponse.json({ token: "fake-jwt-123", user: { email: body.email } }, { status: 200 });
}
return HttpResponse.json({ message: "Invalid credentials" }, { status: 401 });
}),
http.post("/api/signup", async ({ request }) => { // Signup endpoint
const body = await request.json() as { email: string; password: string };
return HttpResponse.json({ id: "u_123", email: body.email }, { status: 201 });
}),
http.get("/api/users", () => { // Sample users list (for ex9)
return HttpResponse.json([{ id: 1, name: "John Doe" }]);
})
];
// src/mocks/browser.ts
import { setupWorker } from "msw/browser"; // Browser worker
import { handlers } from "./handlers"; // Our route handlers
export const worker = setupWorker(...handlers); // Start with all handlers
File: src/main.tsx
(enable MSW in dev)
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import "./index.css";
async function enableMocking() { // Gate MSW to dev only
if (import.meta.env.MODE !== "development") return; // Avoid in production build
const { worker } = await import("./mocks/browser"); // Lazy import to reduce bundle
await worker.start({ serviceWorker: { url: "/mockServiceWorker.js" } }); // Start worker
}
await enableMocking(); // Ensure MSW is ready
const router = createBrowserRouter([
{ path: "/", element: <App />, children: [
{ path: "login", element: <Login /> },
{ path: "signup", element: <Signup /> }
]}
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
ex4 — Login Form: Controlled State + POST
Goal: Controlled inputs with useState
, submit → fetch
→ UI update.
Setup (PowerShell)
Set-Location ..
Copy-Item -Path .\ex3-msw -Destination .\ex4-login-form -Recurse
Set-Location .\ex4-login-form
npm install
File: src/pages/Login.tsx
import { useState } from "react"; // Local state for inputs & UI
type LoginResponse = // Discriminated union for response
| { token: string; user: { email: string } }
| { message: string };
export default function Login() {
const [email, setEmail] = useState("user@example.com"); // Prefill demo creds for speed
const [password, setPassword] = useState("password");
const [msg, setMsg] = useState<string>(""); // Where we show result text
async function onSubmit(e: React.FormEvent) { // Form submit handler
e.preventDefault();
setMsg(""); // Reset any previous message
try {
const res = await fetch("/api/login", { // POST to MSW endpoint
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data: LoginResponse = await res.json(); // Parse JSON
if (res.ok && "token" in data) { // If success shape (has token)
setMsg(`Welcome, ${data.user.email} (token: ${data.token})`);
} else {
setMsg(("message" in data && data.message) || "Login failed");
}
} catch {
setMsg("Network error");
}
}
return (
<form onSubmit={onSubmit} style={{ display: "grid", gap: "0.75rem", maxWidth: 360 }}>
<h2>Login</h2>
<label>Email<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" /></label>
<label>Password<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" /></label>
<button type="submit">Sign in</button>
{msg && <p>{msg}</p>}
</form>
);
}
ex5 — Signup Form: Happy Path
Goal: Submit to /api/signup
, parse JSON, confirm success.
Setup (PowerShell)
Set-Location ..
Copy-Item -Path .\ex4-login-form -Destination .\ex5-signup-form -Recurse
Set-Location .\ex5-signup-form
npm install
File: src/pages/Signup.tsx
import { useState } from "react"; // Local form state
type SignupRes = { id: string; email: string } | { message: string }; // Union for success/error
export default function Signup() {
const [email, setEmail] = useState(""); // Controlled input
const [pwd, setPwd] = useState(""); // Controlled input
const [result, setResult] = useState(""); // Result message
async function onSubmit(e: React.FormEvent) { // Submit handler
e.preventDefault();
setResult("");
const res = await fetch("/api/signup", { // POST to mock endpoint
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password: pwd })
});
const data: SignupRes = await res.json(); // Parse JSON
if (res.ok && "id" in data) { // Success shape
setResult(`User created: ${data.email}`);
} else {
setResult(("message" in data && data.message) || "Signup failed");
}
}
return (
<form onSubmit={onSubmit} style={{ display: "grid", gap: "0.75rem", maxWidth: 360 }}>
<h2>Signup</h2>
<label>Email<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" /></label>
<label>Password<input value={pwd} onChange={(e) => setPwd(e.target.value)} type="password" /></label>
<button type="submit">Create account</button>
{result && <p>{result}</p>}
</form>
);
}
ex6 — UX: Loading, Disabled, Error Banner
Goal: Treat network issues as normal. Disable buttons during async. Show errors where eyes look.
Setup (PowerShell)
Set-Location ..
Copy-Item -Path .\ex5-signup-form -Destination .\ex6-ux-loading-errors -Recurse
Set-Location .\ex6-ux-loading-errors
npm install
File: src/pages/Login.tsx
(UX states)
import { useState } from "react";
export default function Login() {
const [email, setEmail] = useState("user@example.com"); // Prefilled demo creds
const [password, setPassword] = useState("password");
const [msg, setMsg] = useState(""); // Success text
const [loading, setLoading] = useState(false); // Disable button when true
const [err, setErr] = useState(""); // Error banner text
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setMsg(""); setErr(""); setLoading(true); // Reset UI state
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (res.ok && data?.token) { // success: has token
setMsg(`Welcome, ${data.user.email}`);
} else {
setErr(data?.message || "Login failed");
}
} catch {
setErr("Network error — please try again."); // fetch error
} finally {
setLoading(false); // re-enable
}
}
return (
<form onSubmit={onSubmit} style={{ display: "grid", gap: "0.75rem", maxWidth: 360 }}>
<h2>Login</h2>
{err && ( // Error banner
<div role="alert" style={{ background: "#fee", padding: "0.5rem", border: "1px solid #f99" }}>
{err}
</div>
)}
<label>Email<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" /></label>
<label>Password<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" /></label>
<button type="submit" disabled={loading}> {/* Prevent double submit */}
{loading ? "Signing in…" : "Sign in"}
</button>
{msg && <p>{msg}</p>}
</form>
);
}
Simulate a server failure (optional)
// In src/mocks/handlers.ts change login handler to:
http.post("/api/login", async () => {
return HttpResponse.json({ message: "Server down" }, { status: 500 });
});
ex7 — Validation & A11y
Goal: Gentle email validation + accessible hints and errors.
Setup (PowerShell)
Set-Location ..
Copy-Item -Path .\ex6-ux-loading-errors -Destination .\ex7-validation-a11y -Recurse
Set-Location .\ex7-validation-a11y
npm install
File: src/pages/Signup.tsx
(validation + a11y)
import { useMemo, useState } from "react";
const emailOk = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); // Simple email regex
export default function Signup() {
const [email, setEmail] = useState(""); // Controlled email
const [pwd, setPwd] = useState(""); // Controlled password
const [submitting, setSubmitting] = useState(false); // UX state
const [error, setError] = useState(""); // Error text
const emailValid = useMemo(() => emailOk(email), [email]); // Derived state (memoized)
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
if (!emailValid) { // Client-side guard
setError("Please enter a valid email address.");
return;
}
setSubmitting(true);
try {
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password: pwd })
});
const data = await res.json();
if (!res.ok) throw new Error(data?.message || "Signup failed");
alert(`User created: ${data.email}`); // Success path
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Signup failed");
} finally {
setSubmitting(false);
}
}
return (
<form onSubmit={onSubmit} aria-describedby="signup-help" style={{ display: "grid", gap: "0.75rem", maxWidth: 420 }}>
<h2>Signup</h2>
{error && ( // Accessible error region
<div role="alert" style={{ background: "#fee", padding: "0.5rem", border: "1px solid #f99" }}>
{error}
</div>
)}
<label>
Email
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
aria-invalid={email ? String(!emailValid) : undefined} // a11y invalid state
aria-describedby="email-hint" // a11y hint link
required
/>
</label>
<small id="email-hint">We’ll send a confirmation link. Use a valid email format.</small>
<label>
Password
<input value={pwd} onChange={(e) => setPwd(e.target.value)} type="password" required minLength={6} />
</label>
<button type="submit" disabled={submitting || !emailValid}>
{submitting ? "Creating…" : "Create account"}
</button>
<p id="signup-help" hidden>Fill the form to create an account.</p>
</form>
);
}
ex8 — Refactor: Reusable <Input/>
Goal: Reduce copy/paste; show how to pass props through a custom component.
Setup (PowerShell)
Set-Location ..
Copy-Item -Path .\ex7-validation-a11y -Destination .\ex8-refactor-input -Recurse
Set-Location .\ex8-refactor-input
npm install
File: src/components/Input.tsx
import { InputHTMLAttributes, forwardRef } from "react"; // Types + ref support
type Props = InputHTMLAttributes<HTMLInputElement> & { // Extend normal input props
label: string; // Visible label text
hintId?: string; // Optional hint association
error?: string; // Optional error text
};
const Input = forwardRef<HTMLInputElement, Props>(function Input(
{ label, hintId, error, ...rest }, // Unpack props
ref // Ref to the input element
) {
const invalid = Boolean(error) || rest["aria-invalid"] === "true"; // Compute invalid state
return (
<label style={{ display: "grid", gap: "0.25rem" }}>
<span>{label}</span> {/* Visible label */}
<input
{...rest} {/* Pass any other props (value, onChange, etc.) */}
ref={ref}
aria-invalid={invalid ? "true" : undefined}
aria-describedby={hintId}
/>
{error && ( // Inline error text
<span role="alert" style={{ color: "#b00" }}>
{error}
</span>
)}
</label>
);
});
export default Input;
ex9 — React Query: Users List
Goal: Show server-state best practices: caching, loading, error. (We’ll still use MSW.)
Install React Query (PowerShell)
npm install @tanstack/react-query
File: src/main.tsx
(wrap app with QueryClientProvider)
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // React Query
import App from "./App";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import "./index.css";
const queryClient = new QueryClient(); // One cache per app
const router = createBrowserRouter([
{ path: "/", element: <App />, children: [
{ path: "login", element: <Login /> },
{ path: "signup", element: <Signup /> }
]}
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}> {/* Provide cache */}
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>
);
File: src/App.tsx
(add UsersList)
import { Link, Outlet } from "react-router-dom";
import { useQuery } from "@tanstack/react-query"; // Hook to fetch/cache users
function UsersList() {
const { data, isLoading, error } = useQuery({
queryKey: ["users"], // Cache key
queryFn: async () => { // Fetcher
const res = await fetch("/api/users");
if (!res.ok) throw new Error("Failed to load users");
return res.json();
}
});
if (isLoading) return <p>Loading users…</p>; // Loading state
if (error) return <p role="alert">{String(error)}</p>; // Error state
return <ul>{data.map((u: any) => <li key={u.id}>{u.name}</li>)}</ul>; // Success
}
export default function App() {
return (
<div style={{ maxWidth: 720, margin: "2rem auto", fontFamily: "system-ui" }}>
<header style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<h1 style={{ margin: 0, fontSize: "1.25rem" }}>React Mocks Lab</h1>
<nav style={{ display: "flex", gap: "1rem" }}>
<Link to="/login">Login</Link>
<Link to="/signup">Signup</Link>
</nav>
</header>
<section style={{ marginTop: "1rem" }}>
<h3>Sample Users (React Query)</h3>
<UsersList /> {/* Show cached users list */}
</section>
<main style={{ marginTop: "1.5rem" }}>
<Outlet /> {/* Routes render here */}
</main>
</div>
);
}
ex10 — Final Assembly (Sum of All Steps)
Goal: A clean, minimal Auth MFE with Router, MSW, React Query, UX, a11y, and a reusable <Input/>
.
Create a fresh project (optional) or reuse ex9
# (Optional) Fresh final folder by copying ex9 forward:
Set-Location ..
Copy-Item -Path .\ex9 -Destination .\ex10-final -Recurse # If you named ex9 differently, adjust
File: package.json
(scripts + deps)
{
"name": "react-mocks-lab",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"msw:init": "msw init public/ --save --no-open"
},
"dependencies": {
"@tanstack/react-query": "^5.56.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"msw": "^2.4.9",
"typescript": "^5.6.2",
"vite": "^5.4.8"
}
}
File: index.html
(SEO meta + entry)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React Mocks Lab</title>
<meta name="description" content="React application with MSW mocking capabilities" />
<meta name="keywords" content="react, msw, mocking, testing, vite, typescript" />
<meta property="og:title" content="React Mocks Lab" />
<meta name="theme-color" content="#646cff" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
File: src/mocks/handlers.ts
import { http, HttpResponse } from "msw"; // MSW APIs
export const handlers = [
http.get("/api/users", () => { // Users list (ex9)
return HttpResponse.json([{ id: 1, name: "John Doe" }]);
}),
http.post("/api/login", async ({ request }) => { // Login (ex4/ex6)
const body = await request.json() as { email: string; password: string };
if (body.email === "user@example.com" && body.password === "password") {
return HttpResponse.json({ token: "fake-jwt-123", user: { email: body.email } }, { status: 200 });
}
return HttpResponse.json({ message: "Invalid credentials" }, { status: 401 });
}),
http.post("/api/signup", async ({ request }) => { // Signup (ex5/ex7)
const body = await request.json() as { email: string; password: string };
return HttpResponse.json({ id: "u_123", email: body.email }, { status: 201 });
})
];
File: src/mocks/browser.ts
import { setupWorker } from "msw/browser"; // Browser worker
import { handlers } from "./handlers"; // All our endpoints
export const worker = setupWorker(...handlers); // Configure worker
File: src/components/Input.tsx
import { InputHTMLAttributes, forwardRef } from "react";
type Props = InputHTMLAttributes<HTMLInputElement> & {
label: string;
hintId?: string;
error?: string;
};
const Input = forwardRef<HTMLInputElement, Props>(function Input(
{ label, hintId, error, ...rest },
ref
) {
const invalid = Boolean(error) || rest["aria-invalid"] === "true";
return (
<label style={{ display: "grid", gap: "0.25rem" }}>
<span>{label}</span>
<input
{...rest}
ref={ref}
aria-invalid={invalid ? "true" : undefined}
aria-describedby={hintId}
/>
{error && (
<span role="alert" style={{ color: "#b00" }}>{error}</span>
)}
</label>
);
});
export default Input;
File: src/pages/Login.tsx
import { useState } from "react";
export default function Login() {
const [email, setEmail] = useState("user@example.com");
const [password, setPassword] = useState("password");
const [msg, setMsg] = useState("");
const [loading, setLoading] = useState(false);
const [err, setErr] = useState("");
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setMsg(""); setErr(""); setLoading(true);
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (res.ok && data?.token) {
setMsg(`Welcome, ${data.user.email}`); // You could navigate to /welcome here
} else {
setErr(data?.message || "Login failed");
}
} catch {
setErr("Network error — please try again.");
} finally {
setLoading(false);
}
}
return (
<form onSubmit={onSubmit} style={{ display: "grid", gap: "0.75rem", maxWidth: 360 }}>
<h2>Login</h2>
{err && (<div role="alert" style={{ background: "#fee", padding: "0.5rem", border: "1px solid #f99" }}>{err}</div>)}
<label>Email<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" /></label>
<label>Password<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" /></label>
<button type="submit" disabled={loading}>{loading ? "Signing in…" : "Sign in"}</button>
{msg && <p>{msg}</p>}
</form>
);
}
File: src/pages/Signup.tsx
import { useMemo, useState } from "react";
const emailOk = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
export default function Signup() {
const [email, setEmail] = useState("");
const [pwd, setPwd] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const emailValid = useMemo(() => emailOk(email), [email]);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
if (!emailValid) { setError("Please enter a valid email address."); return; }
setSubmitting(true);
try {
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password: pwd })
});
const data = await res.json();
if (!res.ok) throw new Error(data?.message || "Signup failed");
alert(`User created: ${data.email}`);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Signup failed");
} finally {
setSubmitting(false);
}
}
return (
<form onSubmit={onSubmit} aria-describedby="signup-help" style={{ display: "grid", gap: "0.75rem", maxWidth: 420 }}>
<h2>Signup</h2>
{error && (<div role="alert" style={{ background: "#fee", padding: "0.5rem", border: "1px solid #f99" }}>{error}</div>)}
<label>
Email
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
aria-invalid={email ? String(!emailValid) : undefined}
aria-describedby="email-hint"
required
/>
</label>
<small id="email-hint">We’ll send a confirmation link. Use a valid email format.</small>
<label>Password<input value={pwd} onChange={(e) => setPwd(e.target.value)} type="password" minLength={6} required /></label>
<button type="submit" disabled={submitting || !emailValid}>{submitting ? "Creating…" : "Create account"}</button>
<p id="signup-help" hidden>Fill the form to create an account.</p>
</form>
);
}
File: src/App.tsx
(with UsersList)
import { Link, Outlet } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
function UsersList() {
const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: async () => {
const res = await fetch("/api/users");
if (!res.ok) throw new Error("Failed to load users");
return res.json();
}
});
if (isLoading) return <p>Loading users…</p>;
if (error) return <p role="alert">{String(error)}</p>;
return <ul>{data.map((u: any) => <li key={u.id}>{u.name}</li>)}</ul>;
}
export default function App() {
return (
<div style={{ maxWidth: 720, margin: "2rem auto", fontFamily: "system-ui" }}>
<header style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<h1 style={{ margin: 0, fontSize: "1.25rem" }}>React Mocks Lab</h1>
<nav style={{ display: "flex", gap: "1rem" }}>
<Link to="/login">Login</Link>
<Link to="/signup">Signup</Link>
</nav>
</header>
<section style={{ marginTop: "1rem" }}>
<h3>Sample Users (React Query)</h3>
<UsersList />
</section>
<main style={{ marginTop: "1.5rem" }}>
<Outlet />
</main>
</div>
);
}
File: src/main.tsx
(final)
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import "./index.css";
const queryClient = new QueryClient();
async function enableMocks() { // Enable MSW in dev
if (import.meta.env.DEV) {
const { worker } = await import("./mocks/browser");
await worker.start({ onUnhandledRequest: "bypass", serviceWorker: { url: "/mockServiceWorker.js" } });
}
}
await enableMocks();
const router = createBrowserRouter([
{ path: "/", element: <App />, children: [
{ path: "login", element: <Login /> },
{ path: "signup", element: <Signup /> }
]}
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>
);
Run it
npm run dev
# Open the URL shown (usually http://localhost:5173 or http://localhost:3000)
Practice & Next Steps
- Rebuild ex1 → ex6 without looking.
- Add a “Remember me” checkbox (no storage yet).
- Simulate server 500 (MSW) and surface a friendly message.
- Create a
PasswordInput
with show/hide toggle. - Bonus: After a successful login, navigate to
/welcome
and display the email.
Summary
- Scaffolded a React app via Vite + TS.
- Added React Router for
/login
&/signup
. - Mocked APIs with MSW.
- Built forms with controlled state and
fetch
. - Improved UX (loading, disabled, error banner) and basic a11y.
- Refactored a reusable <Input/> and used React Query for server state.
- Final assembly shows how these parts integrate in a simple, scalable structure.
Comments
Post a Comment