Back to Machine Coding
Machine Coding
hard
mid

How would you build a full React application from scratch covering state management, forms, and component architecture?

End-to-end recipe: Vite or Next.js scaffold; TypeScript strict; ESLint+Prettier; Tailwind+Radix or design system; React Query for server state; Zustand for client state; React Hook Form + Zod for forms; React Router or Next router; Sentry+web-vitals for observability; Vitest+Testing Library+MSW for tests; CI with type-check+test+bundle budget. Feature folders; codeowners; ADRs.

6 min read·~60 min to think through

End-to-end recipe for a credible production app.

Scaffold

bash
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
# or
npm create vite@latest my-app -- --template react-ts

Pick Next.js for SSR/SEO/RSC; Vite for pure CSR / library work.

TS config

json
{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "Bundler",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Stack

ConcernTool
StylingTailwind + tokens via CSS vars
UI primitivesRadix / Headless UI + CVA variants
Server stateReact Query (or RTK Query)
Client stateZustand
FormsReact Hook Form + Zod
ValidationZod schemas shared client/server
RoutingNext App Router / React Router
AuthAuth provider + httpOnly cookies
TestingVitest + Testing Library + MSW + Playwright
Lint/formatESLint + Prettier (or Biome)
ObservabilitySentry + web-vitals
Bundle budgetsize-limit

Folder structure

ts
src/
├ app/                  (Next routes or App.tsx)
├ features/
│  ├ cart/
│  ├ checkout/
│  └ profile/
├ shared/
│  ├ ui/                (design system primitives)
│  ├ hooks/
│  └ utils/
├ lib/                  (http, auth, storage)
└ types/

Component example

tsx
// features/profile/ProfileForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const Schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});
type FormData = z.infer<typeof Schema>;

export function ProfileForm({ defaults, onSubmit }: { defaults: FormData; onSubmit: (data: FormData) => Promise<void> }) {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({
    resolver: zodResolver(Schema),
    defaultValues: defaults,
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <label>
        Name
        <input {...register('name')} className="..." />
        {errors.name && <span>{errors.name.message}</span>}
      </label>
      <label>
        Email
        <input {...register('email')} type="email" className="..." />
        {errors.email && <span>{errors.email.message}</span>}
      </label>
      <button type="submit" disabled={isSubmitting}>Save</button>
    </form>
  );
}

Data layer

tsx
const useProfile = (id: string) => useQuery({
  queryKey: ['profile', id],
  queryFn: () => api.get(`/users/${id}`).then((r) => r.json()),
});

const useUpdateProfile = () => useMutation({
  mutationFn: (data: ProfileData) => api.patch('/profile', data),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['profile'] }),
});

Client state

tsx
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCart = create(persist((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
}), { name: 'cart' }));

Testing

tsx
test('saves profile', async () => {
  const onSubmit = vi.fn();
  render(<ProfileForm defaults={{ name: 'A', email: 'a@b.c' }} onSubmit={onSubmit} />);
  await userEvent.clear(screen.getByLabelText(/name/i));
  await userEvent.type(screen.getByLabelText(/name/i), 'B');
  await userEvent.click(screen.getByRole('button', { name: /save/i }));
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ name: 'B' }));
});

CI

  • Type check.
  • Lint.
  • Unit + integration tests.
  • Build.
  • size-limit per route.
  • Playwright smoke on critical paths.
  • Visual regression for design system.

Observability

ts
import * as Sentry from '@sentry/react';
import { onLCP, onINP, onCLS } from 'web-vitals';

Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN });
onLCP(({ value }) => analytics.track('LCP', { value }));
onINP(({ value }) => analytics.track('INP', { value }));
onCLS(({ value }) => analytics.track('CLS', { value }));

Conventions

  • ADRs in docs/adr/.
  • CODEOWNERS per feature.
  • Commit conventions (Conventional Commits).
  • Feature flags via LaunchDarkly / GrowthBook / similar.

Interview framing

"Next.js (or Vite) with TypeScript strict. Tailwind + Radix primitives + CVA variants for UI. React Query for server state, Zustand for client state, React Hook Form + Zod for forms (with shared schemas for server validation). Feature folders with strict dependency direction. Vitest + Testing Library + MSW + Playwright for tests. CI gates: type check, lint, tests, build, bundle budget per route, visual regression. Sentry + web-vitals for observability. ADRs and CODEOWNERS for governance. The recipe matters less than the discipline — measurement, ownership, gates."

Follow-up questions

  • How would you handle auth in this stack?
  • What does your CI look like?
  • How do you organize features at scale?

Common mistakes

  • Skipping Zod / shared schemas.
  • No CI bundle budget.
  • Mixing server state into client store.
  • Type checking only locally.

Performance considerations

  • Stack defaults are good; budgets in CI prevent regression.

Edge cases

  • SSR + Zustand store-per-request.
  • Auth with edge runtime.
  • Form schemas shared with server validators.

Real-world examples

  • Vercel templates, T3 stack, Next.js examples.

Senior engineer discussion

Seniors articulate the choices and tradeoffs of each piece — and the discipline (CI, ADRs, ownership) matters more than the picks.

Related questions