New Component Skill
Create a new React component following the project's conventions and best practices.
Trigger
Use this skill when: user says "create component", "add component", "new component", or "build a [something] component"
Workflow
Step 1: Determine Component Type
- •UI Component →
components/ui/(shadcn-style, reusable primitives) - •Feature Component →
components/[feature]/(feature-specific) - •Layout Component →
components/layout/(page layouts, headers, footers) - •Marketing Component →
components/marketing/(landing page sections)
Step 2: Determine Client/Server
- •Server Component (default): No interactivity, can fetch data directly
- •Client Component: Has state, effects, event handlers → add
"use client"
Step 3: Create Component File
Basic Component Template:
tsx
interface [ComponentName]Props {
// Define props with TypeScript
title?: string;
children?: React.ReactNode;
}
export function [ComponentName]({ title, children }: [ComponentName]Props) {
return (
<div className="">
{title && <h2>{title}</h2>}
{children}
</div>
);
}
Client Component Template:
tsx
"use client";
import { useState } from "react";
interface [ComponentName]Props {
defaultValue?: string;
onChange?: (value: string) => void;
}
export function [ComponentName]({ defaultValue = "", onChange }: [ComponentName]Props) {
const [value, setValue] = useState(defaultValue);
const handleChange = (newValue: string) => {
setValue(newValue);
onChange?.(newValue);
};
return (
<div className="">
{/* Interactive content */}
</div>
);
}
Data-Fetching Component Template:
tsx
import { createClient } from "@/supabase/server";
interface [ComponentName]Props {
userId?: string;
}
export async function [ComponentName]({ userId }: [ComponentName]Props) {
const supabase = await createClient();
const { data, error } = await supabase
.from("table")
.select("*")
.eq("user_id", userId);
if (error) {
return <div>Error loading data</div>;
}
return (
<div>
{data?.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
Step 4: Add Props Interface
Always define a TypeScript interface for props:
tsx
interface [ComponentName]Props {
// Required props
id: string;
// Optional props with defaults
variant?: "default" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
// Callback props
onClick?: () => void;
onChange?: (value: string) => void;
// Children
children?: React.ReactNode;
// HTML attributes passthrough
className?: string;
}
Step 5: Use Tailwind + cn() for Styling
tsx
import { cn } from "@/lib/utils";
export function [ComponentName]({ className, variant = "default" }: Props) {
return (
<div
className={cn(
"base-styles-here",
variant === "outline" && "border border-input",
className
)}
>
{/* content */}
</div>
);
}
Step 6: Add to Index Export (if in subdirectory)
Create or update components/[category]/index.ts:
tsx
export { [ComponentName] } from "./[component-name]";
Step 7: Write Tests
Create __tests__/components/[component-name].test.tsx:
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { [ComponentName] } from "@/components/[path]/[component-name]";
describe("[ComponentName]", () => {
it("renders correctly", () => {
render(<[ComponentName] />);
expect(screen.getByRole("...")).toBeInTheDocument();
});
it("handles click events", async () => {
const onClick = vi.fn();
const user = userEvent.setup();
render(<[ComponentName] onClick={onClick} />);
await user.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalled();
});
it("applies custom className", () => {
render(<[ComponentName] className="custom-class" />);
expect(screen.getByRole("...")).toHaveClass("custom-class");
});
});
File Naming Conventions
- •Component files:
kebab-case.tsx(e.g.,user-avatar.tsx) - •Component names:
PascalCase(e.g.,UserAvatar) - •Test files:
kebab-case.test.tsx
Checklist
- • Component file created
- • Props interface defined with TypeScript
- • "use client" added if needed
- • Tailwind + cn() for styling
- • Index export updated
- • Tests written
- • Build passes:
pnpm build