Day 1: React Fundamentals — Build an Auth MFE

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

Day 1: React Fundamentals — Build an Auth MFE

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>
  );
}

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

  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.

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

Building Scalable AWS VPC Infrastructure with Terraform Modules for SaaS Applications

CodeForge Full-Stack Guide with AI: Spring Boot, React, AWS using ChatGPT-5, Amazon Q & GitHub Copilot