React 19 standalone conventions and patterns with Vite, React Router v7, TypeScript strict mode
From beenpx claudepluginhub george-popescu/bee-dev --plugin beeThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
These standards apply when the project stack is react. All agents and implementations must follow these conventions.
Also read skills/standards/frontend/SKILL.md for universal frontend standards (component architecture, accessibility, responsive design, CSS methodology, design quality) that apply alongside these React-specific conventions.
children prop for wrapper components (layouts, providers, modals).Tabs, Tabs.List, Tabs.Panel) using dot notation exports or a parent object.// Pattern: compound component with context
interface TabsProps { defaultTab: string; children: React.ReactNode; }
interface TabsContextValue { activeTab: string; setActiveTab: (tab: string) => void; }
const TabsContext = createContext<TabsContextValue | null>(null);
function Tabs({ defaultTab, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return <TabsContext value={{ activeTab, setActiveTab }}>{children}</TabsContext>;
}
function TabsList({ children }: { children: React.ReactNode }) {
return <div role="tablist">{children}</div>;
}
function TabsPanel({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab } = use(TabsContext)!;
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.List = TabsList;
Tabs.Panel = TabsPanel;
useState for local reactive state. Always use the setter function, never mutate directly.useEffect for side effects (subscriptions, timers, DOM manipulation). Always return a cleanup function when needed.useContext for consuming context values. Pair with a custom hook: useAuth() wraps useContext(AuthContext).useReducer for complex state with multiple sub-values or when next state depends on previous.useMemo for expensive derived values. Use instead of storing derived state in useState.useCallback for stable function references passed to child components.useRef for mutable values that do not trigger re-renders (DOM refs, timers, previous values).use() — read promises and context directly in render. Replaces useContext for context reading and enables Suspense-based async data.useActionState(fn, initialState) — manage form action state with automatic pending tracking. Replaces useFormState. Returns [state, formAction, isPending].useOptimistic(state, updateFn) — optimistic UI updates during async actions. Show the expected result immediately, roll back on failure.useFormStatus() (from react-dom) — read parent form submission status. Use in submit buttons to show loading state.// Pattern: form with useActionState + useOptimistic
import { useActionState, useOptimistic } from 'react';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
function TodoForm({ todos, addTodo }: { todos: Todo[]; addTodo: (text: string) => Promise<Todo[]> }) {
const [optimisticTodos, addOptimistic] = useOptimistic(todos, (state, newText: string) => [
...state, { id: crypto.randomUUID(), text: newText, pending: true },
]);
const [state, formAction] = useActionState(async (_prev: Todo[], formData: FormData) => {
const text = formData.get('text') as string;
addOptimistic(text);
return await addTodo(text);
}, todos);
return (
<form action={formAction}>
<input name="text" required />
<SubmitButton />
<ul>{optimisticTodos.map(t => <li key={t.id} style={{ opacity: t.pending ? 0.5 : 1 }}>{t.text}</li>)}</ul>
</form>
);
}
use() is the exception — it CAN be called conditionally (inside if/else, try/catch).useFilters(), useDebounce(), usePagination(), useLocalStorage().use* naming convention and live in src/hooks/ directory.// Pattern: custom hook with cleanup and abort
function useFetch<T>(url: string): { data: T | null; loading: boolean; error: Error | null } {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => { if (err.name !== 'AbortError') setError(err); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
useState for component-level state. Keep state as close to where it is used as possible.useReducer when state has multiple sub-values or transitions follow a defined pattern.type Action = { type: 'add'; item: Item } | { type: 'remove'; id: string }.Detect what the project uses — check package.json for installed state management libraries and follow THAT library's conventions. Do NOT introduce a different state library than what the project already uses.
Common libraries and their best practices:
createAsyncThunk for async, RTK Query for server state. Never mutate state outside Immer. Use useSelector with narrow selectors, useDispatch with typed hooks (useAppDispatch, useAppSelector).useStore(selector) over useStore().atom for derived state.observer() HOC, keep stores class-based if project convention.If no external store is installed: Use Context API + useReducer for shared state. Split read/write contexts for performance.
Key rule: Match the project. If the project uses Redux, write Redux. If it uses Zustand, write Zustand. Never mix state libraries without explicit user direction.
// Pattern: context with split read/write to prevent unnecessary re-renders
const StateContext = createContext<AppState | null>(null);
const DispatchContext = createContext<Dispatch<Action> | null>(null);
export function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext value={dispatch}>
<StateContext value={state}>
{children}
</StateContext>
</DispatchContext>
);
}
export function useAppState() {
const ctx = useContext(StateContext);
if (!ctx) throw new Error('useAppState must be used within AppProvider');
return ctx;
}
export function useAppDispatch() {
const ctx = useContext(DispatchContext);
if (!ctx) throw new Error('useAppDispatch must be used within AppProvider');
return ctx;
}
Use React Router v7 in framework mode with file-based route modules for type-safe data loading:
// routes/products.$pid.tsx — route module with typed loader
import type { Route } from "./+types/products.$pid";
export async function loader({ params }: Route.LoaderArgs) {
const product = await getProduct(params.pid);
if (!product) throw new Response("Not found", { status: 404 });
return { product };
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
await updateProduct(params.pid, Object.fromEntries(formData));
return { success: true };
}
export default function Product({ loaderData }: Route.ComponentProps) {
return <div>{loaderData.product.name}</div>;
}
Use createBrowserRouter with typed loaders/actions when not using framework mode:
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home />, loader: homeLoader },
{ path: 'orders', element: <Orders />, loader: ordersLoader },
{ path: 'orders/:id', element: <OrderDetail />, loader: orderLoader, action: orderAction },
],
},
]);
useFetcher<typeof loader>() for non-navigating data mutations (inline forms, like buttons). Note v7 generic syntax uses typeof loader, not LoaderData.useNavigation() for global pending UI (loading bar, spinner) during page transitions.errorElement on routes for route-level error handling.// Pattern: pending UI with useNavigation
function GlobalNav() {
const navigation = useNavigation();
return (
<nav>
{navigation.state === 'loading' && <ProgressBar />}
<Outlet />
</nav>
);
}
// Pattern: useFetcher for inline mutation
function LikeButton({ postId }: { postId: string }) {
const fetcher = useFetcher<typeof action>();
const isLiking = fetcher.state !== 'idle';
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<button disabled={isLiking}>{isLiking ? '...' : '♥'}</button>
</fetcher.Form>
);
}
<Suspense fallback={<Loading />}> for loading states.React.lazy() for route-level code splitting.use() hook for promise-based data reading.// Pattern: nested Suspense boundaries
function Dashboard() {
return (
<Suspense fallback={<PageSkeleton />}>
<DashboardHeader />
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<CardSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<RecentOrders />
</Suspense>
</div>
</Suspense>
);
}
react-error-boundary library or write a class-based error boundary.import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Usage: wrap feature sections
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => queryClient.clear()}>
<OrdersFeature />
</ErrorBoundary>
action prop on <form> with async functions for progressive enhancement.useState for real-time validation.FormData for simple forms (prefer when no real-time validation needed).// Pattern: Zod schema with React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
});
type FormData = z.infer<typeof schema>;
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Log in</button>
</form>
);
}
VITE_ prefix. Access via import.meta.env.VITE_API_URL. Never expose secrets without prefix.vite.config.ts under resolve.alias (e.g., @/ maps to src/). Mirror in tsconfig.json paths.server.proxy in Vite config for API calls to avoid CORS during development.React.lazy() for route-level splitting.build.rollupOptions.output.manualChunks for vendor splitting when needed. Default build.minify uses oxc (fastest)..env, .env.local, .env.production, .env.staging — loaded based on --mode flag.// vite.config.ts — typical React project configuration
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
});
render(), screen, and userEvent from @testing-library/react.vi.mock() for fetch/axios.renderHook() from @testing-library/react.import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('submits form and shows success message', async () => {
const user = userEvent.setup();
render(<CreateOrderForm />);
await user.type(screen.getByLabelText('Customer'), 'Acme Corp');
await user.selectOptions(screen.getByLabelText('Priority'), 'high');
await user.click(screen.getByRole('button', { name: 'Create Order' }));
await waitFor(() => {
expect(screen.getByText('Order created successfully')).toBeInTheDocument();
});
});
// Pattern: testing loading states
test('shows skeleton while loading', async () => {
render(
<Suspense fallback={<Skeleton />}>
<AsyncComponent />
</Suspense>
);
expect(screen.getByTestId('skeleton')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument();
});
});
use() can be conditional).any type in TypeScript -- define proper interfaces and types.import.meta.env without VITE_ prefix -- unprefixed vars are not available client-side, but setting envPrefix to empty string is a security risk.useFormState -- it's deprecated in React 19. Use useActionState instead.useFetcher<DataType>() -- React Router v7 requires useFetcher<typeof loader>() with the function type..js or .jsx files in the source tree.use* prefix). Components orchestrate hooks, not contain raw logic.key derived from domain data -- never from array index in dynamic lists.React.lazy + Suspense for route-level code splitting.package.json first — follow established patterns, don't introduce new libraries.useOptimistic for mutations that should feel instant.[{ id }] creates new ref every render → infinite loops. Stabilize with useMemo.useFetcher<DataType>() to useFetcher<typeof loader>().useOptimistic on server error leaves ghost data in the UI.any type. Disables type checking and hides bugs. Use proper interfaces, generics, or unknown.OrderList.tsx, UserProfile.tsx, AuthProvider.tsx. Non-component utilities use camelCase: formatDate.ts, apiClient.ts.use prefix. useAuth.ts, usePagination.ts, useDebounce.ts.index.ts. Internal details not re-exported.OrderList.tsx and OrderList.test.tsx in the same directory.Props suffix. CardProps, OrderListProps, ModalProps.@/ for all imports from src/. Never use relative paths that climb more than one level.src/
features/
orders/
OrderList.tsx
OrderList.test.tsx
OrderDetail.tsx
useOrders.ts
orderApi.ts
index.ts
auth/
LoginForm.tsx
useAuth.ts
authApi.ts
index.ts
hooks/ (shared hooks)
components/ (shared UI components)
lib/ (utilities, api client, constants)
When looking up framework documentation, use these Context7 library identifiers:
/facebook/react (use version /facebook/react/v19_1_1 for React 19 specifics)/websites/reactrouter -- routing, loaders, actions, navigation, framework mode/websites/vite_dev -- configuration, build, environment variables, pluginsvitest-dev/vitest -- test runner, assertions, mocking, configurationtesting-library/react-testing-library -- render, screen, queries, user eventsAlways check Context7 for the latest API when working with React 19 features (useActionState, useOptimistic, use(), form actions). Training data may be outdated.