Build React Single Page Applications with TypeScript, Vite, Tailwind CSS v4+, and lucide-react. Follows a structured architecture with theme components, utilities, and strict TypeScript patterns. Optimized for small demo applications with best practices for performance, code organization, and maintainability.
This skill provides guidelines for developing React Single Page Applications following a specific architecture pattern, primarily used for small demo applications.
Use this skill when:
When scaffolding a new React SPA project, refer to PROJECT_SETUP.md for:
src/
├── components/ # App-specific components with logic
├── theme/
│ └── index.ts # Central export for all theme components
├── utils/
│ └── classnames.ts # CN utility for conditional classes
├── App.tsx # Root component
├── index.css # Tailwind v4 theme + animations
├── main.tsx # React entry point
└── vite-env.d.ts # Vite type declarations
src/components/)App-specific components with business logic:
components/auth/, components/dashboard/)Example structure:
components/
├── auth/
│ ├── LoginForm.tsx
│ └── RegisterForm.tsx
├── dashboard/
│ └── MetricCard.tsx
└── LanguageSelector.tsx
src/theme/)Reusable UI components with minimal logic:
src/theme/index.tsExample theme component:
// src/theme/button/Button.tsx
import type { ButtonHTMLAttributes } from "react";
import cn from "../../utils/classnames";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
className?: string;
}
export default function Button({
variant = "primary",
className = "",
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
"px-4 py-2 rounded",
{
"bg-blue-500 text-white": variant === "primary",
"bg-gray-200 text-black": variant === "secondary",
},
className
)}
{...props}
>
{children}
</button>
);
}
Central theme export:
// src/theme/index.ts
export { default as Button } from "./button/Button";
export { default as InputText } from "./form/InputText";
Usage in components:
import { Button, InputText } from "./theme";
export default function MyComponent() {
return (
<div>
<InputText placeholder="Enter name" />
<Button variant="primary">Submit</Button>
</div>
);
}
src/utils/)Utility files export multiple helper functions:
Examples:
utils/classnames.ts - CN utility for conditional classes (required)utils/format.ts - formatDate(), formatNumber(), formatCurrency()utils/validation.ts - isEmail(), isValidUrl(), validateForm()Standard structure for all React components:
import { useState } from "react";
import cn from "./utils/classnames";
import { Button } from "./theme";
interface MyComponentProps {
title: string;
count?: number;
className?: string;
onSubmit?: (value: string) => void;
}
export default function MyComponent({
title,
count = 0,
className = "",
onSubmit,
}: MyComponentProps) {
const [value, setValue] = useState<string>("");
const handleSubmit = () => {
if (onSubmit) {
onSubmit(value);
}
};
return (
<div className={cn("container", className)}>
<h2>{title}</h2>
<p>Count: {count}</p>
<Button onClick={handleSubmit}>Submit</Button>
</div>
);
}
Key patterns:
Props)className?: string in props for styling flexibilitycn() utility for combining classnames"./theme"any - Use proper types or unknown with type guardsimport type { MyType } from './types'as const for literal types instead of enumsinterface for object shapesIMPORTANT: Never use React. prefix for types. Always import types directly from React.
import type {
ButtonHTMLAttributes,
InputHTMLAttributes,
MouseEvent,
ReactNode
} from "react";
// ✓ Correct: Import types and extend without React. prefix
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
}
// ✗ Wrong: Don't use React. prefix
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
}
// ✓ Event handlers
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
// ...
};
// ✓ Refs
const inputRef = useRef<HTMLInputElement>(null);
// ✓ Children prop
interface ContainerProps {
children: ReactNode;
}
// ✓ Generic components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}
useState, useEffect, useRef, useMemo, useCallbackuseState for component-specific statereact-hook-form for complex forms// State with derived values
const [items, setItems] = useState<Item[]>([]);
const activeItems = items.filter((item) => item.active);
// Effect with cleanup
useEffect(() => {
const subscription = api.subscribe();
return () => subscription.unsubscribe();
}, []);
// Memoized expensive computation
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Memoized callback
const handleClick = useCallback(() => {
console.log(value);
}, [value]);
// Refs for DOM access
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
LanguageSelector.tsx, MetricCard.tsx)classnames.ts, format.ts, validation.ts)types.ts or index.ts in dedicated foldersconstants.ts with SCREAMING_SNAKE_CASE exportsExamples:
src/
├── components/
│ ├── UserProfile.tsx ✓ PascalCase
│ └── DashboardMetrics.tsx ✓ PascalCase
├── theme/
│ └── button/
│ └── Button.tsx ✓ PascalCase
├── utils/
│ ├── classnames.ts ✓ camelCase
│ ├── format.ts ✓ camelCase
│ └── validation.ts ✓ camelCase
└── constants.ts ✓ camelCase
.prettierrc rulesmap/filter over loopsImports are automatically sorted using @trivago/prettier-plugin-sort-imports:
Configuration in .prettierrc:
{
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
Result: Two groups separated by blank line:
// Group 1: Third-party modules (alphabetically sorted)
import { useState, useEffect } from "react";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { User, Settings } from "lucide-react";
// Group 2: Local imports (alphabetically sorted)
import { Button, InputText } from "./theme";
import UserCard from "./components/UserCard";
import type { UserData } from "./types";
import cn from "./utils/classnames";
import { formatDate } from "./utils/format";
Do not manually organize imports - let Prettier handle it automatically.
// Early return for loading/error states
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
// Ternary for simple conditions
{isActive ? <ActiveIcon /> : <InactiveIcon />}
// Logical AND for conditional display
{showTitle && <h1>{title}</h1>}
// Complex conditions - extract to variable
const canSubmit = isValid && !isSubmitting && hasChanges;
return <Button disabled={!canSubmit}>Submit</Button>;
All Tailwind configuration uses the new CSS-first approach in src/index.css:
@import "tailwindcss";
@theme {
/* Custom theme variables */
--color-primary: #3b82f6;
--color-secondary: #64748b;
/* Custom breakpoints, spacing, etc. */
}
// Use cn() for conditional classes
<div className={cn(
"px-4 py-2 rounded",
{ "bg-blue-500": isPrimary },
{ "bg-gray-200": !isPrimary },
className
)} />
// Responsive design
<div className="text-sm md:text-base lg:text-lg" />
// State variants
<button className="hover:bg-blue-600 focus:ring-2 active:scale-95" />
useMemo/useCallback for expensive renders// Lazy loading components
const HeavyComponent = lazy(() => import("./HeavyComponent"));
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
// Debouncing input
const [search, setSearch] = useState("");
const debouncedSearch = useMemo(
() => debounce((value: string) => performSearch(value), 300),
[]
);
// Memoize expensive calculations
const filteredData = useMemo(
() => data.filter((item) => item.category === category),
[data, category]
);
npm run dev for rapid iterationnpm run lint and npm run format before commitsnpm run build to catch build issuesThis skill enables building maintainable, performant React SPAs with a consistent architecture pattern and best practices.