Chapter 7: Sample React.js App with Best Practices

Overview

This chapter walks through a small, realistic React app that applies the best practices from earlier chapters:

You can adapt this into any stack (Next.js, Remix), but the principles stay the same.


Project Structure

src/
  app/
    App.tsx
    router.tsx
    queryClient.ts
  features/
    auth/
      api.ts
      store.ts
      components/
        LoginForm.tsx
    todos/
      api.ts
      components/
        TodoList.tsx
        AddTodoForm.tsx
  shared/
    components/
      Button.tsx
      TextField.tsx
      PageLayout.tsx
    hooks/
      useTitle.ts
    lib/
      axiosClient.ts
  main.tsx

Key ideas:


App Shell with Providers

// src/app/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      refetchOnWindowFocus: true,
      retry: 2,
    },
  },
});
// src/app/App.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from './queryClient';
import { AppRouter } from './router';

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <AppRouter />
      </BrowserRouter>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Best practices covered:


Routing and Code Splitting

// src/app/router.tsx
import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { PageLayout } from '@/shared/components/PageLayout';

const TodosPage = lazy(() => import('@/features/todos/components/TodosPage'));
const LoginPage = lazy(() => import('@/features/auth/components/LoginPage'));

export function AppRouter() {
  return (
    <PageLayout>
      <Suspense fallback={<div>Loading page…</div>}>
        <Routes>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/todos" element={<TodosPage />} />
          <Route path="*" element={<Navigate to="/todos" replace />} />
        </Routes>
      </Suspense>
    </PageLayout>
  );
}

Best practices:


Shared UI Components

// src/shared/components/Button.tsx
import type { ButtonHTMLAttributes, ReactNode } from 'react';

type Variant = 'primary' | 'secondary' | 'ghost';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  children: ReactNode;
  variant?: Variant;
  isLoading?: boolean;
}

export function Button({ children, variant = 'primary', isLoading, disabled, ...rest }: ButtonProps) {
  const base =
    'inline-flex items-center justify-center rounded px-3 py-2 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed';
  const variants: Record<Variant, string> = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-200',
    ghost: 'bg-transparent text-slate-900 hover:bg-slate-100',
  };

  return (
    <button
      type={rest.type ?? 'button'}
      className={`${base} ${variants[variant]}`}
      disabled={disabled || isLoading}
      {...rest}
    >
      {isLoading ? 'Please wait…' : children}
    </button>
  );
}
// src/shared/components/TextField.tsx
import type { InputHTMLAttributes } from 'react';

interface TextFieldProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

export function TextField({ id, label, error, ...rest }: TextFieldProps) {
  const inputId = id ?? rest.name;

  return (
    <div className="flex flex-col gap-1">
      <label htmlFor={inputId} className="text-sm font-medium text-slate-800">
        {label}
      </label>
      <input
        id={inputId}
        className={`rounded border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-500 ${
          error ? 'border-red-500' : 'border-slate-300'
        }`}
        {...rest}
      />
      {error && <p className="text-xs text-red-600">{error}</p>}
    </div>
  );
}

Best practices:


Auth Feature: Zustand + React Query

// src/features/auth/store.ts
import { create } from 'zustand';

interface User {
  id: string;
  email: string;
}

interface AuthState {
  user: User | null;
  accessToken: string | null;
  setSession: (user: User, accessToken: string) => void;
  clearSession: () => void;
}

export const useAuthStore = create<AuthState>(set => ({
  user: null,
  accessToken: null,
  setSession: (user, accessToken) => set({ user, accessToken }),
  clearSession: () => set({ user: null, accessToken: null }),
}));
// src/shared/lib/axiosClient.ts
import axios from 'axios';
import { useAuthStore } from '@/features/auth/store';

export const axiosClient = axios.create({
  baseURL: '/api',
});

axiosClient.interceptors.request.use(config => {
  const token = useAuthStore.getState().accessToken;
  if (token) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${token}`,
    };
  }
  return config;
});
// src/features/auth/api.ts
import { axiosClient } from '@/shared/lib/axiosClient';

export interface LoginPayload {
  email: string;
  password: string;
}

export interface LoginResponse {
  user: { id: string; email: string };
  accessToken: string;
}

export async function login(payload: LoginPayload): Promise<LoginResponse> {
  const { data } = await axiosClient.post<LoginResponse>('/auth/login', payload);
  return data;
}

Best practices:


Auth Form: React Hook Form + Zod

// src/features/auth/components/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { login, type LoginPayload } from '../api';
import { useAuthStore } from '../store';
import { Button } from '@/shared/components/Button';
import { TextField } from '@/shared/components/TextField';

const schema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

type FormValues = z.infer<typeof schema>;

export function LoginForm() {
  const setSession = useAuthStore(state => state.setSession);

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: {
      email: '',
      password: '',
    },
  });

  const { mutate, isPending } = useMutation({
    mutationFn: (values: LoginPayload) => login(values),
    onSuccess: ({ user, accessToken }) => {
      setSession(user, accessToken);
      // navigate to /todos (using React Router's useNavigate in the page component)
    },
  });

  const onSubmit = (values: FormValues) => {
    mutate(values);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex max-w-sm flex-col gap-4">
      <TextField
        label="Email"
        type="email"
        autoComplete="email"
        {...register('email')}
        error={errors.email?.message}
      />
      <TextField
        label="Password"
        type="password"
        autoComplete="current-password"
        {...register('password')}
        error={errors.password?.message}
      />
      <Button type="submit" variant="primary" isLoading={isPending}>
        Log in
      </Button>
    </form>
  );
}

Best practices:


Todos Feature: React Query + Optimistic UI

// src/features/todos/api.ts
import { axiosClient } from '@/shared/lib/axiosClient';

export interface Todo {
  id: string;
  title: string;
  completed: boolean;
}

export async function fetchTodos(): Promise<Todo[]> {
  const { data } = await axiosClient.get<Todo[]>('/todos');
  return data;
}

export async function createTodo(title: string): Promise<Todo> {
  const { data } = await axiosClient.post<Todo>('/todos', { title });
  return data;
}
// src/features/todos/components/TodoList.tsx
import { useQuery } from '@tanstack/react-query';
import { fetchTodos } from '../api';

export function TodoList() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  if (isLoading) return <p>Loading todos…</p>;
  if (isError) return <p>Failed to load todos</p>;

  if (!data?.length) {
    return <p className="text-sm text-slate-500">You have no todos yet. Add one above.</p>;
  }

  return (
    <ul className="space-y-2">
      {data.map(todo => (
        <li key={todo.id} className="flex items-center gap-2">
          <input type="checkbox" checked={todo.completed} readOnly aria-label={todo.title} />
          <span className={todo.completed ? 'line-through text-slate-400' : ''}>{todo.title}</span>
        </li>
      ))}
    </ul>
  );
}
// src/features/todos/components/AddTodoForm.tsx
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTodo, type Todo } from '../api';
import { Button } from '@/shared/components/Button';

export function AddTodoForm() {
  const [title, setTitle] = useState('');
  const queryClient = useQueryClient();

  const { mutate, isPending } = useMutation({
    mutationFn: (t: string) => createTodo(t),
    onMutate: async newTitle => {
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      const previous = queryClient.getQueryData<Todo[]>(['todos']) ?? [];

      const optimistic: Todo = {
        id: `temp-${Date.now()}`,
        title: newTitle,
        completed: false,
      };

      queryClient.setQueryData<Todo[]>(['todos'], [...previous, optimistic]);
      setTitle('');

      return { previous };
    },
    onError: (_error, _newTitle, context) => {
      if (context?.previous) {
        queryClient.setQueryData(['todos'], context.previous);
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!title.trim()) return;
    mutate(title.trim());
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="Add a new todo"
        className="flex-1 rounded border border-slate-300 px-3 py-2 text-sm"
      />
      <Button type="submit" variant="primary" isLoading={isPending}>
        Add
      </Button>
    </form>
  );
}
// src/features/todos/components/TodosPage.tsx
import { TodoList } from './TodoList';
import { AddTodoForm } from './AddTodoForm';

export default function TodosPage() {
  return (
    <main className="mx-auto flex max-w-xl flex-col gap-4 p-4">
      <header>
        <h1 className="text-xl font-semibold text-slate-900">Todos</h1>
        <p className="text-sm text-slate-600">A simple list powered by React Query.</p>
      </header>
      <AddTodoForm />
      <TodoList />
    </main>
  );
}

Best practices:


Putting It All Together

This small app demonstrates how to combine:

From here you can grow the app by:

The patterns stay the same — only the rendering environment (CSR/SSR/RSC) and routing layer change.