Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
hieutrtr avatar

React Frontend Expert

  • 184 installs
  • 8 repo stars
  • Updated February 6, 2026
  • hieutrtr/ai1-skills

Implement React UI with expert patterns for components, hooks, state, routing, accessibility, and performance in production web apps.

About

react-frontend-expert from hieutrtr/ai1-skills encodes senior React guidance for building modern SPAs and dashboards. It steers component design, hooks usage, routing, a11y, and performance tuning so agents ship cleaner, scalable frontend code aligned with current React ecosystem conventions.

  • React component and hooks guidance
  • State management and rendering patterns
  • Accessibility and UX-minded UI structure
  • Performance and bundle-conscious practices
  • Production-ready frontend architecture advice

React Frontend Expert by the numbers

  • 184 all-time installs (skills.sh)
  • +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
  • Ranked #886 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hieutrtr/ai1-skills --skill react-frontend-expert

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs184
repo stars8
Last updatedFebruary 6, 2026
Repositoryhieutrtr/ai1-skills

What it does

Implement React UI with expert patterns for components, hooks, state, routing, accessibility, and performance in production web apps.

Files

SKILL.mdMarkdownGitHub ↗

React Frontend Expert

When to Use

Activate this skill when:

  • Creating or modifying React components (functional components only)
  • Writing custom hooks (useXxx)
  • Building pages with routing
  • Implementing data fetching with TanStack Query
  • Handling forms with validation
  • Setting up project structure for a React/TypeScript application

Do NOT use this skill for:

  • Writing component or hook tests (use react-testing-patterns)
  • E2E browser testing (use e2e-testing)
  • API contract design (use api-design-patterns)
  • Backend implementation (use python-backend-expert)
  • Deployment or CI/CD (use deployment-pipeline)

Instructions

Project Structure

src/
├── api/                  # API client functions and query options
│   ├── client.ts         # Axios/fetch instance with interceptors
│   ├── users.ts          # User API functions + query options
│   └── posts.ts
├── components/           # Shared, reusable UI components
│   ├── Button.tsx
│   ├── Modal.tsx
│   ├── Table/
│   │   ├── Table.tsx
│   │   └── TablePagination.tsx
│   └── Form/
│       ├── Input.tsx
│       └── Select.tsx
├── features/             # Domain-specific feature components
│   ├── users/
│   │   ├── UserList.tsx
│   │   └── UserProfile.tsx
│   └── posts/
│       └── PostEditor.tsx
├── hooks/                # Custom hooks
│   ├── useAuth.ts
│   ├── useDebounce.ts
│   └── usePagination.ts
├── layouts/              # Layout components
│   ├── MainLayout.tsx
│   └── AuthLayout.tsx
├── pages/                # Route-level page components
│   ├── HomePage.tsx
│   ├── LoginPage.tsx
│   └── users/
│       ├── UserListPage.tsx
│       └── UserDetailPage.tsx
├── types/                # Shared TypeScript types
│   ├── api.ts            # API response types
│   └── user.ts
├── App.tsx               # Root component with providers and router
└── main.tsx              # Entry point

Component Structure

Functional Components Only
interface UserCardProps {
  user: User;
  onEdit: (userId: number) => void;
  showEmail?: boolean;
}

export function UserCard({ user, onEdit, showEmail = false }: UserCardProps) {
  return (
    <article className="user-card">
      <h3>{user.displayName}</h3>
      {showEmail && <p>{user.email}</p>}
      <button type="button" onClick={() => onEdit(user.id)}>
        Edit
      </button>
    </article>
  );
}

Component rules:

  • Named exports for shared components: export function Button
  • Default exports for page components: export default function UserListPage
  • Props interface named {Component}Props
  • Destructure props in function signature
  • Keep components under 200 lines — extract sub-components or hooks when larger
  • Use children and composition over deep prop drilling
  • Never use React.FC — use plain function syntax
Component File Organization

For complex components, co-locate related files:

UserProfile/
├── UserProfile.tsx       # Main component
├── UserProfile.css       # Styles (or .module.css)
├── UserAvatar.tsx        # Sub-component
└── index.ts              # Re-export: export { UserProfile } from './UserProfile'

Hooks Rules and Custom Hooks

Rules of Hooks

1. Only call hooks at the top level — never inside loops, conditions, or nested functions 2. Only call hooks from React function components or custom hooks 3. Custom hooks must start with use

Custom Hook Patterns

useDebounce:

export function useDebounce<T>(value: T, delayMs: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delayMs);
    return () => clearTimeout(timer);
  }, [value, delayMs]);

  return debouncedValue;
}

useAuth:

interface AuthContext {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContext | null>(null);

export function useAuth(): AuthContext {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

usePagination:

interface PaginationState {
  cursor: string | null;
  hasMore: boolean;
  goToNext: (nextCursor: string) => void;
  reset: () => void;
}

export function usePagination(): PaginationState {
  const [cursor, setCursor] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(true);

  return {
    cursor,
    hasMore,
    goToNext: (nextCursor: string) => {
      setCursor(nextCursor);
    },
    reset: () => {
      setCursor(null);
      setHasMore(true);
    },
  };
}

When to extract a custom hook:

  • Logic is reused across 2+ components
  • Component has complex state management (>3 useState calls)
  • Side effects need encapsulation (subscriptions, timers)
  • Data fetching logic can be shared

Data Fetching with TanStack Query

Query Options Factory (Recommended)

Centralize query key and function definitions to prevent key collisions:

// api/users.ts
import { queryOptions } from "@tanstack/react-query";

export const userQueries = {
  all: () =>
    queryOptions({
      queryKey: ["users"],
      queryFn: () => apiClient.get<UserListResponse>("/users"),
    }),

  detail: (userId: number) =>
    queryOptions({
      queryKey: ["users", userId],
      queryFn: () => apiClient.get<UserResponse>(`/users/${userId}`),
    }),

  search: (query: string) =>
    queryOptions({
      queryKey: ["users", "search", query],
      queryFn: () => apiClient.get<UserListResponse>(`/users?q=${query}`),
      enabled: query.length > 0,
    }),
};
Using Queries in Components
export function UserDetailPage({ userId }: { userId: number }) {
  const { data: user, isPending, isError, error } = useQuery(
    userQueries.detail(userId)
  );

  if (isPending) return <Spinner />;
  if (isError) return <ErrorMessage error={error} />;

  return <UserProfile user={user} />;
}
Mutations with Cache Invalidation
export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: UserCreate) =>
      apiClient.post<UserResponse>("/users", data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

TanStack Query rules:

  • Set staleTime > 0 (default 0 is too aggressive): staleTime: 5 * 60 * 1000 (5 min)
  • Use invalidateQueries() after mutations — never manual refetch()
  • Handle all states: isPending, isError, data
  • Use queryOptions() factory — prevents key typos and duplication
  • Use enabled to prevent queries from running with incomplete parameters

TypeScript Conventions

// Use `interface` for object shapes (components props, API responses)
interface User {
  id: number;
  email: string;
  displayName: string;
  role: "admin" | "editor" | "member";
}

// Use `type` for unions, intersections, and computed types
type UserRole = User["role"];
type CreateOrUpdate = UserCreate | UserUpdate;

// Discriminated unions for state machines
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

TypeScript rules:

  • Enable strict: true in tsconfig.json — no exceptions
  • Never use any — use unknown for truly unknown types
  • Use as const for literal object types
  • Prefer interface for extensible types, type for everything else
  • Use generics for reusable utility types and hooks
  • Export types from types/ directory for shared use

Form Handling

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const userSchema = z.object({
  email: z.string().email("Invalid email"),
  displayName: z.string().min(1, "Required").max(100),
  role: z.enum(["admin", "editor", "member"]),
});

type UserFormData = z.infer<typeof userSchema>;

export function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<UserFormData>({
    resolver: zodResolver(userSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" {...register("email")} aria-invalid={!!errors.email} />
      {errors.email && <span role="alert">{errors.email.message}</span>}

      <label htmlFor="displayName">Name</label>
      <input id="displayName" {...register("displayName")} aria-invalid={!!errors.displayName} />
      {errors.displayName && <span role="alert">{errors.displayName.message}</span>}

      <button type="submit" disabled={isSubmitting}>Save</button>
    </form>
  );
}

Accessibility Requirements

Every component must meet WCAG 2.1 AA:

1. Semantic HTML first: Use <button>, <nav>, <main>, <article> — not <div onClick> 2. Labels: Every form input has a <label> with matching htmlFor/id 3. ARIA only when needed: aria-label for icon-only buttons, aria-live for dynamic updates, role="alert" for errors 4. Keyboard navigation: All interactive elements reachable via Tab, activatable via Enter/Space 5. Focus management: Set focus to main content on route change, trap focus in modals 6. Color contrast: Minimum 4.5:1 for normal text, 3:1 for large text 7. Alt text: All <img> tags have descriptive alt (or alt="" for decorative images)

Examples

User List Page with Search and Pagination

export default function UserListPage() {
  const [search, setSearch] = useState("");
  const debouncedSearch = useDebounce(search, 300);
  const pagination = usePagination();

  const { data, isPending } = useQuery(
    userQueries.list({ q: debouncedSearch, cursor: pagination.cursor })
  );

  return (
    <main>
      <h1>Users</h1>
      <input
        type="search"
        value={search}
        onChange={(e) => { setSearch(e.target.value); pagination.reset(); }}
        placeholder="Search users..."
        aria-label="Search users"
      />
      {isPending ? <Spinner /> : (
        <>
          <UserTable users={data.items} />
          {data.hasMore && (
            <button onClick={() => pagination.goToNext(data.nextCursor)}>
              Load more
            </button>
          )}
        </>
      )}
    </main>
  );
}

Edge Cases

  • Stale closures in hooks: When using callbacks that reference state, use useRef for mutable values that change frequently, or include dependencies in useCallback/useEffect arrays.
  • TanStack Query key collisions: Structure keys hierarchically: ["users"] for list, ["users", id] for detail, ["users", { q, page }] for filtered list. Use queryOptions() factory to centralize key definitions.
  • Infinite re-renders: Common causes: missing dependency arrays, creating new objects/arrays in render (wrap in useMemo), state updates in useEffect without proper conditions.
  • Hydration mismatches: Avoid rendering content that depends on browser-only APIs (window, localStorage) during initial render. Use useEffect or check typeof window !== "undefined".
  • Memory leaks: Cancel async operations in useEffect cleanup. TanStack Query handles this automatically for queries.

See references/component-templates.md for annotated component templates. See references/tanstack-query-patterns.md for CRUD query patterns.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.