← Back to list

react-components
by MLGBJDLW
Terminal-first AI coding assistant
⭐ 0🍴 0📅 Jan 25, 2026
SKILL.md
name: react-components description: Best practices for building React components including hooks patterns, state management, composition, and performance optimization techniques version: 1.0.0 priority: 25 tags:
- react
- components
- hooks
- typescript
- builtin triggers:
- type: keyword pattern: react
- type: keyword pattern: component
- type: keyword pattern: hooks
- type: glob pattern: "**/*.tsx"
- type: glob pattern: "/components/" globs:
- "**/*.tsx"
- "/components//*.ts"
React Components
Guidelines for building maintainable, performant React components with TypeScript.
Rules
- Single Responsibility: Each component should do one thing well
- Props Over State: Prefer controlled components; lift state when needed
- Type Everything: Use TypeScript interfaces for all props and state
- Composition Over Inheritance: Build complex UIs by composing simple components
- Avoid Inline Functions in Render: Use
useCallbackfor event handlers passed to children - Memoize Expensive Calculations: Use
useMemofor costly computations - Key Prop Stability: Never use array index as key for dynamic lists
- Error Boundaries: Wrap major sections with error boundaries
- Accessibility First: Include ARIA attributes and keyboard navigation
Patterns
Component Structure
import { type FC, useState, useCallback, useMemo } from "react";
// Props interface with clear documentation
interface UserCardProps {
/** User data to display */
user: User;
/** Called when edit button is clicked */
onEdit?: (userId: string) => void;
/** Additional CSS classes */
className?: string;
}
/**
* Displays user information in a card format.
* Supports optional edit functionality.
*/
export const UserCard: FC<UserCardProps> = ({
user,
onEdit,
className,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const handleEdit = useCallback(() => {
onEdit?.(user.id);
}, [onEdit, user.id]);
const displayName = useMemo(
() => `${user.firstName} ${user.lastName}`.trim(),
[user.firstName, user.lastName]
);
return (
<article className={`user-card ${className ?? ""}`}>
<h3>{displayName}</h3>
{onEdit && (
<button onClick={handleEdit} aria-label={`Edit ${displayName}`}>
Edit
</button>
)}
</article>
);
};
```markdown
### Custom Hooks
```typescript
import { useState, useEffect, useCallback } from "react";
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
export function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
```markdown
### Compound Components
```typescript
import { createContext, useContext, type FC, type ReactNode } from "react";
// Context for compound component communication
interface TabsContextValue {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error("Tab components must be used within Tabs");
}
return context;
}
// Parent component
interface TabsProps {
defaultTab: string;
children: ReactNode;
}
export const Tabs: FC<TabsProps> & {
List: typeof TabList;
Tab: typeof Tab;
Panel: typeof TabPanel;
} = ({ defaultTab, children }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
};
// Child components
const TabList: FC<{ children: ReactNode }> = ({ children }) => (
<div role="tablist">{children}</div>
);
const Tab: FC<{ id: string; children: ReactNode }> = ({ id, children }) => {
const { activeTab, setActiveTab } = useTabsContext();
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
const TabPanel: FC<{ id: string; children: ReactNode }> = ({ id, children }) => {
const { activeTab } = useTabsContext();
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
};
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
```markdown
### Render Props & Children as Function
```typescript
interface RenderProps<T> {
data: T;
loading: boolean;
error: Error | null;
}
interface DataLoaderProps<T> {
url: string;
children: (props: RenderProps<T>) => ReactNode;
}
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
const { data, loading, error } = useFetch<T>(url);
return <>{children({ data, loading, error })}</>;
}
// Usage
<DataLoader<User[]> url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserList users={data ?? []} />;
}}
</DataLoader>
```markdown
## Anti-Patterns
```typescript
// ❌ Props drilling through many levels
const App = ({ user }) => <Layout user={user} />;
const Layout = ({ user }) => <Sidebar user={user} />;
const Sidebar = ({ user }) => <Avatar user={user} />; // Use context instead
// ❌ Mutating state directly
const handleAdd = () => {
items.push(newItem); // Wrong!
setItems(items);
};
// ✅ Create new array
const handleAdd = () => setItems([...items, newItem]);
// ❌ Missing dependency in useEffect
useEffect(() => {
fetchUser(userId); // userId not in deps!
}, []); // Will cause stale closures
// ❌ Index as key for dynamic lists
{items.map((item, i) => <Item key={i} {...item} />)} // Breaks on reorder
// ❌ Derived state in useState
const [fullName, setFullName] = useState(`${first} ${last}`);
// ✅ Compute during render or useMemo
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
// ❌ Unnecessary useEffect for sync
useEffect(() => {
setCount(items.length);
}, [items]); // Just compute: const count = items.length;
```markdown
## Examples
### Form with Validation
```typescript
import { type FC, type FormEvent, useState, useCallback } from "react";
interface FormData {
email: string;
password: string;
}
interface FormErrors {
email?: string;
password?: string;
}
export const LoginForm: FC<{ onSubmit: (data: FormData) => void }> = ({
onSubmit,
}) => {
const [data, setData] = useState<FormData>({ email: "", password: "" });
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Set<keyof FormData>>(new Set());
const validate = useCallback((values: FormData): FormErrors => {
const errs: FormErrors = {};
if (!values.email.includes("@")) errs.email = "Invalid email";
if (values.password.length < 8) errs.password = "Min 8 characters";
return errs;
}, []);
const handleChange = useCallback((field: keyof FormData, value: string) => {
setData((prev) => ({ ...prev, [field]: value }));
}, []);
const handleBlur = useCallback((field: keyof FormData) => {
setTouched((prev) => new Set(prev).add(field));
setErrors(validate(data));
}, [data, validate]);
const handleSubmit = useCallback(
(e: FormEvent) => {
e.preventDefault();
const validationErrors = validate(data);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
onSubmit(data);
}
},
[data, onSubmit, validate]
);
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={data.email}
onChange={(e) => handleChange("email", e.target.value)}
onBlur={() => handleBlur("email")}
aria-invalid={touched.has("email") && !!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{touched.has("email") && errors.email && (
<span id="email-error" role="alert">{errors.email}</span>
)}
</div>
<button type="submit">Login</button>
</form>
);
};
```markdown
### List with Virtualization Hook
```typescript
import { useRef, useState, useEffect, useMemo, type CSSProperties } from "react";
interface UseVirtualListOptions {
itemCount: number;
itemHeight: number;
overscan?: number;
}
export function useVirtualList({ itemCount, itemHeight, overscan = 3 }: UseVirtualListOptions) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const [containerHeight, setContainerHeight] = useState(0);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => setScrollTop(container.scrollTop);
const handleResize = () => setContainerHeight(container.clientHeight);
container.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", handleResize);
handleResize();
return () => {
container.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleResize);
};
}, []);
const { startIndex, endIndex, offsetY } = useMemo(() => {
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const end = Math.min(itemCount - 1, start + visibleCount + overscan * 2);
return { startIndex: start, endIndex: end, offsetY: start * itemHeight };
}, [scrollTop, containerHeight, itemHeight, itemCount, overscan]);
const totalHeight = itemCount * itemHeight;
return { containerRef, startIndex, endIndex, offsetY, totalHeight };
}
References
Score
Total Score
65/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
✓最近の活動
1ヶ月以内に更新
+10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
✓タグ
1つ以上のタグが設定されている
+5
Reviews
💬
Reviews coming soon


