Day 1: React Fundamentals - Building Your First Modern React App
Day 1: Build a React Auth Micro-Frontend (Login & Signup) with Vite, TypeScript, React Router & MSW
Audience: Beginner–intermediate React developers | Time: ~4 hours demo + ~4 hours practice
Technical & Functional Goals
- Functional: Users can “log in” and “sign up” with friendly success/error messages.
- Technical: Learn React app structure, client routing, controlled forms, async requests, loading states, error handling, basic a11y, and a tiny refactor for reusable inputs.
Teaching style: We learn by doing. Each step is runnable. Copy → paste → run → understand the why.
Table of Contents
- Prereqs & Workspace
- Day 1 Folder Layout
- ex1 — Vite + React + TS: Blank Canvas
- ex2 — React Router: /login & /signup
- ex3 — MSW: Friendly Fake APIs
- ex4 — React Login Form (useState + fetch)
- ex5 — React Signup Success Flow
- ex6 — UX: Loading, Disabled Button, Error Banner
- ex7 — Validation & Accessibility (a11y)
- ex8 — Refactor: Reusable <Input/>
- Practice Lab & Next Steps
- FAQ: Full code in blog vs Git repo?
- Summary
Prereqs & Workspace
- Node: v18+ recommended (node -v)
- Package manager: npm (comes with Node)
- Editor: VS Code or IntelliJ IDEA Community
# Create a clean workspace
mkdir auth-mfe-day1 && cd auth-mfe-day1
Day 1 Folder Layout
auth-mfe-day1/
ex1-basic-template/
ex2-routing/
ex3-msw/
ex4-login-form/
ex5-signup-form/
ex6-ux-loading-errors/
ex7-validation-a11y/
ex8-refactor-input/
Why separate folders? Each example is a small, runnable milestone. If you break something, you can compare with the previous step and recover fast.
Common package.json (copy into each example)
After scaffolding with Vite, replace your generated package.json
with this one and run npm i
.
{
"name": "auth-mfe-day1",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"msw:init": "msw init public/ --save --no-open"
},
"dependencies": {
"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"
}
}
ex1 — Vite + React + TypeScript: Blank Canvas
Concept: Components are just functions that return JSX. Vite gives a fast dev server and hot reloading.
# Scaffold
npm create vite@latest ex1-basic-template -- --template react-ts
cd ex1-basic-template
# Replace package.json with the one above, then:
npm i
npm run dev
src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/App.tsx
export default function App() {
return (
<div style={{ maxWidth: 640, margin: "2rem auto", fontFamily: "system-ui" }}>
<h1>Auth MFE — Day 1</h1>
<ul>
<li>This is a Vite + React + TS setup.</li>
<li>Components are functions that return JSX.</li>
</ul>
</div>
);
}
ex2 — React Router: /login & /signup
Concept: Client-side routing avoids full page reloads. <Link>
updates the URL, and <Outlet>
renders the nested route.
# Copy ex1 to ex2
cd ..
cp -r ex1-basic-template ex2-routing
cd ex2-routing && npm i
src/main.tsx
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";
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>
);
src/App.tsx
import { Link, Outlet } from "react-router-dom";
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>
<Link to="/signup">Signup</Link>
</nav>
<Outlet />
</div>
);
}
src/pages/Login.tsx
export default function Login() {
return <p>Login page (form coming soon)</p>;
}
src/pages/Signup.tsx
export default function Signup() {
return <p>Signup page (form coming soon)</p>;
}
ex3 — MSW: Friendly Fake APIs
Concept: MSW intercepts fetch
and returns responses locally. No CORS, no cookies, no DNS.
# Copy ex2 to ex3
cd ..
cp -r ex2-routing ex3-msw
cd ex3-msw
npm i
npm run msw:init # creates public/mockServiceWorker.js
src/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.post("/api/login", async ({ request }) => {
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 }) => {
const body = (await request.json()) as { email: string; password: string };
return HttpResponse.json({ id: "u_123", email: body.email }, { status: 201 });
})
];
src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
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() {
if (import.meta.env.MODE !== "development") return;
const { worker } = await import("./mocks/browser");
await worker.start({ serviceWorker: { url: "/mockServiceWorker.js" } });
}
await enableMocking();
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>
);
Troubleshoot: If the worker isn’t found, make sure public/mockServiceWorker.js
exists (created by msw init
). Hard refresh with Ctrl/Cmd+Shift+R.
ex4 — React Login Form (useState + fetch)
Concept: Forms follow the loop state → event → side-effect → UI update. We post to /api/login
and render the result.
# Copy ex3 to ex4
cd ..
cp -r ex3-msw ex4-login-form
cd ex4-login-form && npm i
src/pages/Login.tsx
import { useState } from "react";
type LoginResponse =
| { token: string; user: { email: string } }
| { message: string };
export default function Login() {
const [email, setEmail] = useState("user@example.com");
const [password, setPassword] = useState("password");
const [msg, setMsg] = useState<string>("");
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setMsg("");
try {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
const data: LoginResponse = await res.json();
if (res.ok && "token" in data) {
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"
placeholder="you@example.com"
/>
</label>
<label>
Password
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="password"
/>
</label>
<button type="submit">Sign in</button>
{msg && <p>{msg}</p>}
</form>
);
}
ex5 — React Signup Success Flow
Concept: Same loop as login. We post to /api/signup
and show a success message.
# Copy ex4 to ex5
cd ..
cp -r ex4-login-form ex5-signup-form
cd ex5-signup-form && npm i
src/pages/Signup.tsx
import { useState } from "react";
type SignupRes = { id: string; email: string } | { message: string };
export default function Signup() {
const [email, setEmail] = useState("");
const [pwd, setPwd] = useState("");
const [result, setResult] = useState("");
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setResult("");
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password: pwd })
});
const data: SignupRes = await res.json();
if (res.ok && "id" in data) {
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 Button, Error Banner
Concept: Good apps handle waiting and failure gracefully.
# Copy ex5 to ex6
cd ..
cp -r ex5-signup-form ex6-ux-loading-errors
cd ex6-ux-loading-errors && npm i
src/pages/Login.tsx (snippet)
/* inside component JSX */
<button type="submit" disabled={loading}>
{loading ? "Signing in…" : "Sign in"}
</button>
{err && (
<div role="alert" style={{ background: "#fee", padding: "0.5rem", border: "1px solid #f99" }}>
{err}
</div>
)}
Simulate a failure in MSW
http.post("/api/login", async () => {
return HttpResponse.json({ message: "Server down" }, { status: 500 });
}),
ex7 — Validation & Accessibility (a11y)
Concept: Light email validation + a11y attributes (aria-invalid
, role="alert"
) to help everyone.
# Copy ex6 to ex7
cd ..
cp -r ex6-ux-loading-errors ex7-validation-a11y
cd ex7-validation-a11y && npm i
src/pages/Signup.tsx (snippet)
const emailOk = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
/* inside JSX */
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
aria-invalid={email ? String(!emailValid) : undefined}
aria-describedby="email-hint"
required
/>
<small id="email-hint">We’ll send a confirmation link. Use a valid email format.</small>
{error && <div role="alert">{error}</div>}
ex8 — Refactor: Reusable <Input/>
Concept: Reduce copy/paste and make forms consistent.
# Copy ex7 to ex8
cd ..
cp -r ex7-validation-a11y ex8-refactor-input
cd ex8-refactor-input && npm i
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;
Using <Input/> in Signup
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
aria-invalid={!emailValid && email ? "true" : undefined}
error={emailError}
/>
<Input
label="Password"
type="password"
value={pwd}
onChange={(e) => setPwd(e.currentTarget.value)}
minLength={6}
required
/>
Practice Lab (4 hours)
- Rebuild ex1 → ex6 without peeking.
- Add a “Remember me” checkbox (no storage needed yet).
- Show a friendly message when the network is down (MSW 500).
- Create a PasswordInput with a show/hide toggle.
- Bonus: Add a
/welcome
route after successful login.
FAQ: Is it professional to include full code in the blog, or should I link a Git repo?
Best practice: Do both.
- In the post: Include the essential, copy-paste snippets for each step (like above). This keeps the tutorial self-contained and great for SEO.
- In GitHub: Provide the full working repo with
ex1…ex8
folders so readers can clone, run, and compare quickly.
This combination maximizes reader success, search visibility, and monetization (people stay longer, share more, and come back for Day 2).
Summary
- Scaffolded a React app with Vite + TypeScript.
- Added React Router for
/login
and/signup
. - Mocked APIs using MSW to avoid backend blockers.
- Built Login and Signup forms (controlled inputs +
fetch
). - Improved UX with loading state, disabled button, and error banner.
- Added validation & a11y (email check,
aria-invalid
,role="alert"
). - Refactored a reusable <Input/> component.
Nice work! You’ve built a clean, beginner-friendly Auth MFE foundation. Next up: Day 2 — Auth Context, Protected Routes & Logout.
Comments
Post a Comment