사용 방법
- 설치 명령을 복사하고 자신의 에이전트 환경에 설치하기 전에 소스 패키지를 확인하세요.
- 필요한 도구, 인증 정보, 지원 환경은 원본 문서에서 확인하세요.
- 입력과 원하는 산출물을 설명하고 출처와 결과를 대조하세요.
npx skills add https://github.com/duyet/codex-claude-plugins --skill react-nextjs-patterns시작 전 확인
- 설치에는 모델 구독, API 한도, 파일 접근 권한이 포함되지 않습니다.
- 별점 수와 설치 수는 활동 지표이며 독립적인 품질·보안 검증이 아닙니다.
원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.
This skill provides React and Next.js specific patterns for building performant, maintainable frontend applications.
When to Invoke This Skill
Automatically activate for:
- React component implementation
- Next.js page and API routes
- State management patterns
- Performance optimization
- Server/Client component decisions
Next.js App Router Patterns
Server vs Client Components
// Server Component (default) - data fetching, no interactivity
// app/users/page.tsx
export default async function UsersPage() {
const users = await getUsers(); // Runs on server
return (
<div>
<h1>Users</h1>
<UserList users={users} />
</div>
);
}
// Client Component - interactivity required
// components/user-search.tsx
'use client';
import { useState } from 'react';
export function UserSearch({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && onSearch(query)}
/>
);
}Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Fast data loads first */}
<Suspense fallback={<StatsSkeleton />}>
<StatsSection />
</Suspense>
{/* Slow data streams in */}
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
);
}
// Async component that streams
async function StatsSection() {
const stats = await getStats(); // Can be slow
return <Stats data={stats} />;
}Data Fetching Patterns
// Parallel data fetching
async function DashboardPage() {
// Fetch in parallel, not sequentially
const [users, orders, stats] = await Promise.all([
getUsers(),
getOrders(),
getStats(),
]);
return <Dashboard users={users} orders={orders} stats={stats} />;
}
// With error boundary
import { notFound } from 'next/navigation';
async function UserPage({ params }: { params: { id: string } }) {
const user = await getUser(params.id);
if (!user) {
notFound(); // Renders not-found.tsx
}
return <UserProfile user={user} />;
}React Performance Patterns
Component Decomposition
// BAD: Large component with all state
function BadUserList() {
const [filter, setFilter] = useState('');
const [users, setUsers] = useState<User[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
// All users re-render on any state change
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{users.map(user => (
<div
key={user.id}
onClick={() => setSelectedId(user.id)}
className={selectedId === user.id ? 'selected' : ''}
>
{user.name}
</div>
))}
</div>
);
}
// GOOD: Push state down to where it's needed
function GoodUserList() {
const [users] = useState<User[]>([]);
return <FilterableUserList users={users} />;
}
function FilterableUserList({ users }: { users: User[] }) {
const [filter, setFilter] = useState('');
const filtered = useMemo(
() => users.filter(u => u.name.includes(filter)),
[users, filter]
);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<SelectableList users={filtered} />
</div>
);
}
function SelectableList({ users }: { users: User[] }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
return users.map(user => (
<UserItem
key={user.id}
user={user}
selected={selectedId === user.id}
onSelect={() => setSelectedId(user.id)}
/>
));
}Memoization Strategies
// Only memo when there's a measurable benefit
const UserItem = memo(function UserItem({
user,
selected,
onSelect
}: {
user: User;
selected: boolean;
onSelect: () => void;
}) {
return (
<div
onClick={onSelect}
className={selected ? 'selected' : ''}
>
{user.name}
</div>
);
});
// useMemo for expensive computations
function ExpensiveList({ items }: { items: Item[] }) {
const processed = useMemo(() => {
return items
.filter(complexFilter)
.sort(complexSort)
.map(complexTransform);
}, [items]);
return <List items={processed} />;
}
// useCallback for stable references passed to children
function Parent() {
const [items, setItems] = useState<Item[]>([]);
const handleDelete = useCallback((id: string) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return <ItemList items={items} onDelete={handleDelete} />;
}State Management Patterns
Context with Reducer
// types
interface State {
user: User | null;
isLoading: boolean;
error: Error | null;
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; user: User }
| { type: 'FETCH_ERROR'; error: Error }
| { type: 'LOGOUT' };
// reducer
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, isLoading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, isLoading: false, user: action.user };
case 'FETCH_ERROR':
return { ...state, isLoading: false, error: action.error };
case 'LOGOUT':
return { ...state, user: null };
}
}
// context
const AuthContext = createContext<{
state: State;
dispatch: Dispatch<Action>;
} | null>(null);
// provider
function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
user: null,
isLoading: true,
error: null,
});
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
// hook
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}Custom Hooks
// Data fetching hook
function useQuery<T>(
key: string,
fetcher: () => Promise<T>
): { data: T | null; isLoading: boolean; error: Error | null; refetch: () => void } {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const result = await fetcher();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setIsLoading(false);
}
}, [fetcher]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, isLoading, error, refetch: fetchData };
}
// Debounced value hook
function useDebouncedValue<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Local storage hook
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue(prev => {
const newValue = value instanceof Function ? value(prev) : value;
localStorage.setItem(key, JSON.stringify(newValue));
return newValue;
});
}, [key]);
return [storedValue, setValue];
}Component Patterns
Compound Components
// Flexible API with compound components
const Tabs = ({ children, defaultValue }: { children: ReactNode; defaultValue: string }) => {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
};
Tabs.List = function TabsList({ children }: { children: ReactNode }) {
return <div className="tabs-list">{children}</div>;
};
Tabs.Tab = function Tab({ value, children }: { value: string; children: ReactNode }) {
const { activeTab, setActiveTab } = useTabsContext();
return (
<button
className={activeTab === value ? 'active' : ''}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
};
Tabs.Panel = function TabsPanel({ value, children }: { value: string; children: ReactNode }) {
const { activeTab } = useTabsContext();
if (activeTab !== value) return null;
return <div className="tabs-panel">{children}</div>;
};
// Usage
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
<Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="tab1">Content 1</Tabs.Panel>
<Tabs.Panel value="tab2">Content 2</Tabs.Panel>
</Tabs>Render Props & Children as Function
// Data provider with render prop
function DataProvider<T>({
fetcher,
children,
}: {
fetcher: () => Promise<T>;
children: (data: T | null, isLoading: boolean) => ReactNode;
}) {
const { data, isLoading } = useQuery('data', fetcher);
return <>{children(data, isLoading)}</>;
}
// Usage
<DataProvider fetcher={getUsers}>
{(users, isLoading) => (
isLoading ? <Spinner /> : <UserList users={users} />
)}
</DataProvider>Best Practices Checklist
- [ ] Use Server Components by default, Client Components only when needed
- [ ] Push state down to the lowest component that needs it
- [ ] Break large components into smaller, focused ones
- [ ] Use Suspense boundaries for async operations
- [ ] Memoize only when profiling shows benefit
- [ ] Create custom hooks for reusable stateful logic
- [ ] Use discriminated unions for component state
- [ ] Implement proper error boundaries
- [ ] Ensure accessibility (ARIA, keyboard navigation)
- [ ] Use proper loading and error states

