From programmer
Write React frontend code following modern patterns. Use when creating features, adding routes, or writing components. Activates for phrases like "create a feature", "add a route", "new component", "frontend", or when working on React/TypeScript code with React Router.
npx claudepluginhub nicolaei/claude-plugins --plugin programmerThis skill uses the workspace's default tool permissions.
- Project Structure
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Follow Bulletproof React's feature-based architecture:
src/
├── app/ # Application layer (routes, providers, router)
├── components/ # Shared components
├── config/ # Global configuration
├── features/ # Feature modules (primary organization)
├── hooks/ # Shared hooks
├── lib/ # Preconfigured libraries
├── types/ # Shared TypeScript types
└── utils/ # Shared utilities
Each feature is self-contained:
features/user-profile/
├── api/ # API requests and hooks
├── components/ # Feature-specific components
├── hooks/ # Feature-specific hooks
├── types/ # Feature types
└── utils/ # Feature utilities
Only include folders needed for the feature. Don't create empty folders.
kebab-case (e.g., user-profile/)PascalCase.tsx (e.g., UserCard.tsx)use-kebab-case.ts (e.g., use-user-data.ts)kebab-case.ts (e.g., user-types.ts)Avoid index.ts files that re-export everything. They hurt Vite tree-shaking.
// Bad - barrel file
export * from './UserCard';
export * from './UserList';
// Good - direct imports
import { UserCard } from '@/features/user-profile/components/UserCard';
React Router v7 route modules export three main pieces:
import type { Route } from "./+types/my-route";
// Server-side data loading
export async function loader({ params, request }: Route.LoaderArgs) {
const data = await fetchData(params.id);
return { data };
}
// Server-side mutations
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
await saveData(formData);
return { success: true };
}
// Component receives loader data as props
export default function MyRoute({ loaderData }: Route.ComponentProps) {
return <div>{loaderData.data.title}</div>;
}
For non-critical data, return promises without awaiting:
export async function loader({ params }: Route.LoaderArgs) {
// Critical - await this
const user = await getUser(params.id);
// Non-critical - don't await, stream later
const activity = getRecentActivity(params.id);
return { user, activity };
}
export default function Profile({ loaderData }: Route.ComponentProps) {
const { user, activity } = loaderData;
return (
<div>
<h1>{user.name}</h1>
<Suspense fallback={<ActivitySkeleton />}>
<Await resolve={activity}>
{(data) => <ActivityList items={data} />}
</Await>
</Suspense>
</div>
);
}
Handle route errors gracefully:
import { isRouteErrorResponse, useRouteError } from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <div>{error.status}: {error.statusText}</div>;
}
return <div>Something went wrong</div>;
}
Use flatRoutes from @react-router/fs-routes for file-based routing:
// app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;
| Filename | URL |
|---|---|
_index.tsx | / |
about.tsx | /about |
teams.tsx | /teams (layout) |
teams._index.tsx | /teams (index) |
teams.$id.tsx | /teams/:id |
teams_.$id.tsx | /teams/:id (no layout) |
Use $ prefix for dynamic parameters:
app/routes/
├── users.$userId.tsx → /users/:userId
├── posts.$postId.edit.tsx → /posts/:postId/edit
└── files.$.tsx → /files/* (splat)
Access params in loader:
export async function loader({ params }: Route.LoaderArgs) {
const user = await getUser(params.userId);
return { user };
}
Dots create nesting. The parent renders children via <Outlet />:
app/routes/
├── concerts.tsx → Layout for /concerts/*
├── concerts._index.tsx → /concerts
├── concerts.$city.tsx → /concerts/:city
└── concerts.trending.tsx → /concerts/trending
Underscore prefix creates layouts without URL segments:
app/routes/
├── _auth.tsx → Layout (no URL segment)
├── _auth.login.tsx → /login
└── _auth.register.tsx → /register
Always define explicit prop types:
type UserCardProps = {
user: User;
onSelect?: (id: string) => void;
variant?: 'compact' | 'full';
};
export function UserCard({ user, onSelect, variant = 'full' }: UserCardProps) {
return (
<div className="rounded-lg border p-4">
<h3 className="font-semibold">{user.name}</h3>
{variant === 'full' && <p className="text-gray-600">{user.email}</p>}
</div>
);
}
Use utility classes directly. Compose with template literals for conditionals:
type ButtonProps = {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = 'primary',
size = 'md',
children,
className,
...props
}: ButtonProps) {
const baseStyles = 'rounded font-medium transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className ?? ''}`}
{...props}
>
{children}
</button>
);
}
Create skeleton components for loading states:
export function UserCardSkeleton() {
return (
<div className="animate-pulse rounded-lg border p-4">
<div className="h-5 w-32 rounded bg-gray-200" />
<div className="mt-2 h-4 w-48 rounded bg-gray-200" />
</div>
);
}
// Usage with Suspense
<Suspense fallback={<UserCardSkeleton />}>
<Await resolve={userData}>
{(user) => <UserCard user={user} />}
</Await>
</Suspense>
| Type | Location |
|---|---|
| Shared (used across features) | src/components/ |
| Feature-specific | src/features/[name]/components/ |
| Route-specific | Same file or folder as route |
Enforce unidirectional dependencies to keep features independent:
shared → features → app
| Module | Can Import From |
|---|---|
components/, hooks/, utils/, types/ | Each other (shared layer) |
features/* | Shared layer only |
app/ | Shared layer + features |
Features never import from other features
// Bad - cross-feature import
import { UserCard } from '@/features/users/components/UserCard';
// in features/dashboard/...
// Good - use shared components or lift to app layer
import { UserCard } from '@/components/UserCard';
Features never import from app
// Bad - importing from app layer
import { router } from '@/app/router';
// Good - pass dependencies via props or context
Compose features at app layer
// app/routes/dashboard.tsx
import { UserStats } from '@/features/users/components/UserStats';
import { RecentActivity } from '@/features/activity/components/RecentActivity';
export default function Dashboard() {
return (
<div>
<UserStats />
<RecentActivity />
</div>
);
}
Use eslint-plugin-import to enforce boundaries:
// .eslintrc.js
rules: {
'import/no-restricted-paths': ['error', {
zones: [
// features can't import from other features
{
target: './src/features',
from: './src/features',
except: ['./'],
},
// features can't import from app
{
target: './src/features',
from: './src/app',
},
],
}],
}
For data fetching decisions (loaders vs React Query), see: Data Fetching Guide