Day 1: React Fundamentals — Build an Auth MFE

React Day 1: Auth MFE (Vite + TS + Router + MSW) — End-to-End, Line-by-Line
<<<<<<< HEAD

Day 1: React Fundamentals — Build an Auth MFE

=======

Day 1: React Fundamentals — Build an Auth MFE (Mentor’s Perspective)

>>>>>>> a42757d (Add React auth MFE blog post)

Audience: Beginner–Intermediate | Duration: ~4h demo + ~4h practice (Windows/PowerShell friendly)

React is a shift from imperative DOM operations to declarative UI. We’ll learn by building: a tiny Auth micro-frontend with /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

  1. Windows/PowerShell Setup
  2. ex1 — Vite + React + TypeScript: Blank Canvas
  3. ex2 — Routing: /login & /signup
  4. ex3 — MSW: Mock Server Endpoints
  5. ex4 — Login Form: Controlled State + POST
  6. ex5 — Signup Form: Happy Path
  7. ex6 — UX: Loading, Disabled, Error Banner
  8. ex7 — Validation & A11y: Email + aria-*
  9. ex8 — Refactor: Reusable <Input/>
  10. ex9 — React Query: Users List
  11. ex10 — Final Assembly (Sum of All Steps)
  12. Practice & Next Steps
  13. 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>
  );
}
<<<<<<< HEAD
Key Insights (25+ years experience):
  • Entry Points: src/main.tsx bootstraps app, src/App.tsx is root component, index.html provides DOM mount point
  • Critical Line: ReactDOM.createRoot(...).render(...) — this mounts your React tree to #root
  • Why TypeScript + Vite: Fast dev server, type safety for components and props, catches bugs before runtime
  • Common Pitfalls: Forgetting document.getElementById("root")! non-null assertion or wrong type="module" in index.html
  • Quick Test: Change App content, run npm run dev, confirm hot-reload works
======= >>>>>>> a42757d (Add React auth MFE blog post)

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

<<<<<<< HEAD
# Install react-router-dom dependency (REQUIRED)
npm install react-router-dom

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: , children: [
    { path: "login", element:  },  // /login
    { path: "signup", element:  } // /signup
  ]}
]);

ReactDOM.createRoot(document.getElementById("root")!).render(
  
      {/* Provide router to the app */}
  
);

File: src/App.tsx

import { Link, Outlet } from "react-router-dom";  // Link for navigation, Outlet renders child route

export default function App() {
  return (
    

Auth MFE — Routing

{/* child route content goes here */}
); }

Files: src/pages/Login.tsx & src/pages/Signup.tsx

// src/pages/Login.tsx
export default function Login() {             // Temporary placeholder
  return 

Login page (form coming soon)

; } // src/pages/Signup.tsx export default function Signup() { // Temporary placeholder return

Signup page (form coming soon)

; }
Key Insights (25+ years experience):
  • Key Files: src/main.tsx (router setup with createBrowserRouter), src/App.tsx (navigation + <Outlet/>), src/pages/*
  • Critical Pattern: Route config array and how Outlet receives children routes
  • Why Nested Routes: Keep shared layout (header/nav) in App and swap page content inside <Outlet/>
  • Common Pitfalls: Using <a> instead of Link (causes full page reload), forgetting to wrap app with router
  • Quick Test: Click nav links — URL updates and correct page renders without reload

Create the pages folder

# Create pages directory and files
New-Item -Path .\src -Name "pages" -ItemType "Directory"
New-Item -Path .\src\pages -Name "Login.tsx" -ItemType "File"
New-Item -Path .\src\pages -Name "Signup.tsx" -ItemType "File"outer-dom
=======
# Install (if node_modules wasn't copied)
npm install
>>>>>>> a42757d (Add React auth MFE blog post)

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

  1. Rebuild ex1 → ex6 without looking.
  2. Add a “Remember me” checkbox (no storage yet).
  3. Simulate server 500 (MSW) and surface a friendly message.
  4. Create a PasswordInput with show/hide toggle.
  5. Bonus: After a successful login, navigate to /welcome and display the email.
Day 2 preview: Auth Context, Protected Routes, and Logout. Keep MSW for speed, then switch to a real backend + JWT.
<<<<<<< HEAD

Detailed Insights: What Each Example Teaches (25+ Years Experience)

ex3 — MSW: Mock Server Endpoints

Key Insights:
  • Key Files: src/mocks/handlers.ts (declare endpoints), src/mocks/browser.ts (create worker), src/main.tsx (start worker in dev)
  • Critical Pattern: Handlers that return JSON and status codes; worker.start(...)
  • Why MSW: Realistic request/response behavior, testable error states, no CORS hassles
  • Common Pitfalls: Not running npx msw init public/ so mockServiceWorker.js missing; starting MSW in production
  • Quick Test: Call /api/login from the app and see MSW responses in DevTools network (requests are fulfilled by the worker)

ex4 — Login Form: Controlled State + POST

Key Insights:
  • Key File: src/pages/Login.tsx
  • Critical Pattern: useState for email, password, msg; onSubmit uses fetch("/api/login", { method: "POST", body: JSON.stringify(...) })
  • Why Controlled Inputs: Immediate access to values, validation, predictable state
  • Common Pitfalls: Forgetting e.preventDefault() on submit, not setting Content-Type: application/json, or assuming res.ok without parsing
  • Quick Test: Submit demo creds and confirm success message from MSW

ex5 — Signup Form: Happy Path

Key Insights:
  • Key File: src/pages/Signup.tsx
  • Critical Pattern: onSubmit building the request body and checking for id in response
  • Why Separate Flows: Different success shapes, different UX outcomes (redirect, message)
  • Common Pitfalls: Not handling non-2xx response shapes or showing no feedback to user
  • Quick Test: Create a user and check the success message (User created)

ex6 — UX: Loading, Disabled, Error Banner

Key Insights:
  • Key File: src/pages/Login.tsx (updated)
  • Critical Pattern: loading, err states; button disabled={loading}; role="alert" for errors
  • Why Essential: Prevent double submits, show clear feedback, ensure screen readers announce errors
  • Common Pitfalls: Leaving UI stuck if finally block is omitted; swallowing server errors
  • Quick Test: Toggle MSW to return 500 and confirm error banner shows and button re-enables

ex7 — Validation & A11y

Key Insights:
  • Key File: src/pages/Signup.tsx (with emailOk regex and aria attributes)
  • Critical Pattern: aria-invalid, aria-describedby, role="alert", and disabling submit when invalid
  • Why A11y Matters: Validate early, provide programmatic hints for assistive tech, reduce server round-trips
  • Common Pitfalls: Overcomplicated regex (avoid strict RFC regex), relying only on UI disabled state for validation (also validate server-side)
  • Quick Test: Type invalid email and confirm inline error and button disabled

ex8 — Refactor: Reusable Input

Key Insights:
  • Key File: src/components/Input.tsx
  • Critical Pattern: forwardRef, spreading ...rest to pass props, computing aria-invalid from error
  • Why Forward Refs: Allow parent code to focus inputs or integrate with form libs
  • Common Pitfalls: Blindly spreading props that conflict (e.g., multiple id), forgetting to forward ref
  • Quick Test: Replace manual <label><input/></label> with Input and verify accessibility attributes still work

ex9 — React Query: Users List

Key Insights:
  • Key Files: src/main.tsx (wrap app with QueryClientProvider), src/App.tsx (uses useQuery)
  • Critical Pattern: useQuery({ queryKey: ["users"], queryFn: ... }), isLoading, error
  • Why React Query: Separate cache and fetching logic, automatic background refetch, simpler UX patterns
  • Common Pitfalls: Not providing a stable queryKey, fetching raw JSON without checking res.ok, not wrapping app with provider
  • Quick Test: Open app, see "Loading users…" then the list; modify MSW to return error and see error UI

ex10 — Final Assembly (Sum of All Steps)

Key Insights:
  • What to Read: package.json scripts (dev/build), src/main.tsx startup sequence (enable mocks, provide QueryClient & Router), composed App, Login, Signup, and Input
  • Why This Structure: Layered responsibilities — router for navigation, query client for server state, MSW for predictable responses, components for reuse
  • Common Pitfalls: Enabling MSW in production, not handling unhandled requests (use onUnhandledRequest: "bypass" during dev), stale cache issues if you mutate server state without invalidating queries
  • Quick Test: Run npm run dev, exercise login/signup, check users list, and simulate failures via MSW

Developer Checklist (From 25+ Years of Teaching)

Essential Habits for New React Developers:
  1. Always run the dev server and read console + network panel
  2. Keep API shapes consistent between MSW and client code
  3. Use accessibility attributes: role="alert", aria-describedby, aria-invalid for form accessibility
  4. Prefer small components and lift state only when needed
  5. Write one small manual test after each change (form submit, nav click, error case)
  6. Never trust network requests — always handle loading and error states
  7. Validate on both client and server — client for UX, server for security
  8. Use TypeScript strictly — it catches bugs before users do
======= >>>>>>> a42757d (Add React auth MFE blog post)

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.

Written by: Your React Mentor — 25+ years building web apps and mentoring developers.

Comments

Popular posts from this blog

2025 Auth Mfe Blog 1 Intro

Auth MFE Blog #1 — Introduction, Functional Scope, Technical Scope & React Primer