
React Syntax Components
- 10 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-syntax-components is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-syntax-components
- Frontend Development
- AI-coding skill
React Syntax Components by the numbers
- 10 all-time installs (skills.sh)
- Ranked #1,691 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-syntax-componentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-syntax-components
Quick Reference
Function Component Typing
| Pattern | Syntax | When |
|---|---|---|
| Props interface + return | function Comp(props: Props): React.ReactElement | Default for all components |
| Destructured props | function Comp({ name, age }: Props) | When accessing props directly |
| Generic component | function List<T>(props: ListProps<T>) | Reusable data-driven components |
| Default props | { size = 'md' }: Props | Optional props with defaults |
Component API Quick Lookup
| API | Purpose | React 18 | React 19 |
|---|---|---|---|
React.memo | Skip re-render when props unchanged | Yes | Yes (Compiler reduces need) |
forwardRef | Pass ref through component | Required | DEPRECATED -- use ref as prop |
React.lazy | Code-split with dynamic import | Yes | Yes |
createPortal | Render outside DOM parent | Yes | Yes |
useImperativeHandle | Expose custom ref handle | With forwardRef | With ref prop |
Critical Warnings
NEVER use class components for new code -- ALWAYS use function components with hooks. Class components are legacy and cannot use hooks.
NEVER define components inside other components -- this destroys state on every render. ALWAYS define components at module scope.
NEVER call hooks conditionally inside components -- React relies on consistent hook call order. ALWAYS call hooks at the top level.
ALWAYS use TypeScript interfaces for props -- bare any or untyped props defeat type safety and make refactoring dangerous.
ALWAYS use React.ReactNode for children type -- it covers strings, numbers, elements, arrays, fragments, portals, null, and undefined.
---
Decision Tree: Component Pattern Selection
Need to render children?
+-- Fixed structure --> Standard props interface
+-- Flexible layout --> children: React.ReactNode
+-- Parent needs control over rendering --> Render props pattern
Need to share behavior across components?
+-- Shared state logic --> Custom hook (ALWAYS prefer this)
+-- Shared rendering wrapper --> Compound component pattern
+-- Cross-cutting concern (rare) --> HOC pattern
Need performance optimization?
+-- Expensive render, stable props --> React.memo
+-- Code splitting by route --> React.lazy + Suspense
Need DOM access from parent?
+-- React 18 --> forwardRef + useImperativeHandle
+-- React 19 --> ref as prop + useImperativeHandle
Need to render outside DOM hierarchy?
+-- Modals, tooltips, overlays --> createPortal---
Function Components with TypeScript
Props Interface Pattern
interface UserCardProps {
readonly name: string;
readonly email: string;
readonly role?: 'admin' | 'user' | 'guest'; // optional with union
readonly onSelect: (id: string) => void;
}
function UserCard({ name, email, role = 'user', onSelect }: UserCardProps): React.ReactElement {
return (
<div onClick={() => onSelect(name)}>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}ALWAYS mark props as readonly -- props must never be mutated.
Generic Component Pattern
interface ListProps<T> {
readonly items: readonly T[];
readonly renderItem: (item: T, index: number) => React.ReactNode;
readonly keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>): React.ReactElement {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage -- TypeScript infers T from items
<List items={users} renderItem={(user) => <span>{user.name}</span>} keyExtractor={(u) => u.id} />---
Children Patterns
React.ReactNode (default)
interface CardProps {
readonly title: string;
readonly children: React.ReactNode;
}
function Card({ title, children }: CardProps): React.ReactElement {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}PropsWithChildren Utility
import { type PropsWithChildren } from 'react';
interface PanelProps {
readonly variant: 'primary' | 'secondary';
}
function Panel({ variant, children }: PropsWithChildren<PanelProps>): React.ReactElement {
return <div className={`panel-${variant}`}>{children}</div>;
}Note: PropsWithChildren<P> adds children?: React.ReactNode -- children becomes optional.
---
React.memo -- Memoized Components
interface ExpensiveListProps {
readonly items: readonly string[];
readonly onItemClick: (item: string) => void;
}
const ExpensiveList = React.memo(function ExpensiveList(
{ items, onItemClick }: ExpensiveListProps
): React.ReactElement {
return (
<ul>
{items.map((item) => (
<li key={item} onClick={() => onItemClick(item)}>{item}</li>
))}
</ul>
);
});Custom Comparison
const UserRow = React.memo(
function UserRow({ user, onSelect }: UserRowProps): React.ReactElement {
return <tr onClick={() => onSelect(user.id)}><td>{user.name}</td></tr>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);ALWAYS ensure parent stabilizes callback props with useCallback -- otherwise memo is ineffective because a new function reference is created every render.
React 19: The React Compiler auto-memoizes, making manual memo largely unnecessary. Keep memo for React 18 compatibility.
---
forwardRef and Ref Forwarding
React 19 -- ref as Regular Prop (PREFERRED)
interface InputProps {
readonly label: string;
readonly ref?: React.Ref<HTMLInputElement>;
}
function TextInput({ label, ref, ...props }: InputProps): React.ReactElement {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}React 18 -- forwardRef Required
interface InputProps {
readonly label: string;
}
const TextInput = React.forwardRef<HTMLInputElement, InputProps>(
function TextInput({ label, ...props }, ref): React.ReactElement {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
);useImperativeHandle -- Custom Ref API
interface TextInputHandle {
focus: () => void;
clear: () => void;
}
// React 19
function TextInput({ ref }: { ref?: React.Ref<TextInputHandle> }): React.ReactElement {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => ({
focus() { inputRef.current?.focus(); },
clear() { if (inputRef.current) inputRef.current.value = ''; },
}), []);
return <input ref={inputRef} />;
}
// Parent usage
const inputRef = React.useRef<TextInputHandle>(null);
<TextInput ref={inputRef} />
// inputRef.current?.focus();React 19 Ref Cleanup Function
// React 19 -- ref callbacks can return a cleanup function
<div ref={(node) => {
// Setup: node is attached
node?.addEventListener('scroll', handleScroll);
// Cleanup: returned function runs when ref detaches
return () => node?.removeEventListener('scroll', handleScroll);
}} />---
React.lazy -- Code Splitting
// ALWAYS declare lazy components at module top level
const AdminPanel = React.lazy(() => import('./AdminPanel'));
const UserDashboard = React.lazy(() => import('./UserDashboard'));
function App(): React.ReactElement {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/admin" element={<AdminPanel />} />
<Route path="/dashboard" element={<UserDashboard />} />
</Routes>
</React.Suspense>
);
}ALWAYS wrap lazy components in <Suspense> -- without it, a lazy component throws an error.
ALWAYS declare lazy() at module scope -- declaring inside a component recreates it every render and destroys state.
---
createPortal -- Rendering Outside the DOM Tree
import { createPortal } from 'react-dom';
interface ModalProps {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly children: React.ReactNode;
}
function Modal({ isOpen, onClose, children }: ModalProps): React.ReactElement | null {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}Event bubbling: Events from portals bubble through the React tree (not the DOM tree). A click inside a portal still triggers onClick on React ancestors.
ALWAYS use portals for modals, tooltips, and overlays that must escape overflow: hidden or z-index stacking contexts.
---
Composition Patterns
Compound Components (Context-based)
See references/patterns.md for the full compound component pattern with Context.
Render Props
See references/patterns.md for render props and function-as-children patterns.
Higher-Order Components (HOC)
See references/patterns.md for HOC typing with generics. ALWAYS prefer custom hooks over HOCs for new code.
---
Reference Links
- references/examples.md -- Complete component typing examples
- references/patterns.md -- Compound components, render props, HOC patterns
- references/anti-patterns.md -- Component mistakes and fixes
Official Sources
- https://react.dev/reference/react/memo
- https://react.dev/reference/react/forwardRef
- https://react.dev/reference/react/lazy
- https://react.dev/reference/react-dom/createPortal
- https://react.dev/reference/react/useImperativeHandle
- https://react.dev/learn/passing-props-to-a-component
- https://react.dev/learn/typescript
Component Anti-Patterns
Reference file for react-syntax-components. Common mistakes with components and how to fix them.---
AP-01: Defining Components Inside Components
Severity: Critical -- causes state reset on every parent render.
// WRONG -- Inner component is recreated every render, destroying state
function Parent(): React.ReactElement {
function Child(): React.ReactElement {
const [count, setCount] = useState(0); // resets to 0 every time Parent renders
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
return <Child />;
}
// CORRECT -- define at module scope
function Child(): React.ReactElement {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
function Parent(): React.ReactElement {
return <Child />;
}Why: React uses the component function's identity to determine if a component is the same between renders. A new function reference = new component = full remount.
---
AP-02: Using React.FC / React.FunctionComponent
Severity: Low -- works but adds no value and has historical baggage.
// AVOID -- React.FC adds implicit children (React 17), obscures return type
const Greeting: React.FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}</h1>;
};
// PREFERRED -- explicit, clear, no hidden behavior
function Greeting({ name }: GreetingProps): React.ReactElement {
return <h1>Hello, {name}</h1>;
}Why: React.FC historically included implicit children in the props type (fixed in React 18 types), does not support generics natively, and hides the return type. Plain function declarations are clearer.
---
AP-03: Spreading Props Without Filtering
Severity: Medium -- passes invalid attributes to DOM elements.
// WRONG -- custom props leak to DOM, causing React warnings
interface CardProps {
readonly variant: 'primary' | 'secondary';
readonly elevated: boolean;
}
function Card({ ...props }: CardProps): React.ReactElement {
return <div {...props}>Content</div>; // variant and elevated go to <div>
}
// CORRECT -- destructure known props, spread the rest
function Card({ variant, elevated, ...rest }: CardProps & React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
return (
<div
className={`card card-${variant} ${elevated ? 'elevated' : ''}`}
{...rest}
>
Content
</div>
);
}---
AP-04: Memo Without Stable Props
Severity: Medium -- React.memo does nothing if parent recreates props every render.
// WRONG -- new function reference every render, memo is useless
function Parent(): React.ReactElement {
const [items, setItems] = useState<string[]>([]);
return (
<MemoizedList
items={items}
onItemClick={(item) => console.log(item)} // new function every render
/>
);
}
// CORRECT -- stabilize callback with useCallback
function Parent(): React.ReactElement {
const [items, setItems] = useState<string[]>([]);
const handleItemClick = useCallback((item: string) => {
console.log(item);
}, []);
return (
<MemoizedList
items={items}
onItemClick={handleItemClick}
/>
);
}---
AP-05: Conditional Hook Calls
Severity: Critical -- violates Rules of Hooks, causes runtime crashes.
// WRONG -- hook called conditionally
function UserProfile({ userId }: { userId: string | null }): React.ReactElement {
if (!userId) return <p>No user selected</p>;
const [user, setUser] = useState<User | null>(null); // called conditionally!
// ...
}
// CORRECT -- hooks at top level, conditional rendering below
function UserProfile({ userId }: { userId: string | null }): React.ReactElement {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
if (!userId) return;
fetchUser(userId).then(setUser);
}, [userId]);
if (!userId) return <p>No user selected</p>;
return <div>{user?.name}</div>;
}---
AP-06: Using Index as Key for Dynamic Lists
Severity: High -- causes state corruption when items are reordered, inserted, or removed.
// WRONG -- index key breaks state association when list changes
{items.map((item, index) => (
<ItemEditor key={index} item={item} /> // state gets mismatched after reorder
))}
// CORRECT -- use stable, unique identifier from data
{items.map((item) => (
<ItemEditor key={item.id} item={item} />
))}When index key is acceptable: ONLY for truly static lists that NEVER change order, are NEVER filtered, and items have no internal state.
---
AP-07: forwardRef in React 19
Severity: Low -- works but deprecated and unnecessary.
// UNNECESSARY in React 19
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// PREFERRED in React 19 -- ref as regular prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }): React.ReactElement {
return <input ref={ref} {...props} />;
}Why: React 19 passes ref as a regular prop. forwardRef adds unnecessary wrapping. Keep forwardRef ONLY if you must support React 18.
---
AP-08: Lazy Inside Component Body
Severity: Critical -- destroys component state on every render.
// WRONG -- lazy component recreated every render
function App(): React.ReactElement {
const Page = React.lazy(() => import('./Page')); // new lazy wrapper every render!
return (
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
);
}
// CORRECT -- declare at module scope
const Page = React.lazy(() => import('./Page'));
function App(): React.ReactElement {
return (
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
);
}---
AP-09: Missing Suspense for Lazy Components
Severity: Critical -- causes unrecoverable error.
// WRONG -- lazy component without Suspense boundary throws error
const AdminPanel = React.lazy(() => import('./AdminPanel'));
function App(): React.ReactElement {
return <AdminPanel />; // ERROR: A component suspended while responding to synchronous input
}
// CORRECT -- wrap in Suspense
function App(): React.ReactElement {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<AdminPanel />
</React.Suspense>
);
}---
AP-10: Portal Event Bubbling Surprise
Severity: Medium -- events from portals bubble through React tree, not DOM tree.
// SURPRISE -- clicking inside the modal triggers Parent's onClick
function Parent(): React.ReactElement {
return (
<div onClick={() => console.log('Parent clicked!')}>
<Modal isOpen={true} onClose={() => {}}>
<button>Click me</button> {/* triggers Parent onClick! */}
</Modal>
</div>
);
}
// FIX -- stop propagation in the portal content
function Modal({ isOpen, onClose, children }: ModalProps): React.ReactElement | null {
if (!isOpen) return null;
return createPortal(
<div onClick={(e) => e.stopPropagation()}>
{children}
</div>,
document.body
);
}Why: React's event system follows the React component tree, not the DOM tree. Portal children are logically still inside the parent React tree.
---
AP-11: Mutating Props
Severity: Critical -- violates React's unidirectional data flow.
// WRONG -- mutating props object
function UserCard(props: UserCardProps): React.ReactElement {
props.name = props.name.toUpperCase(); // NEVER mutate props
return <h2>{props.name}</h2>;
}
// CORRECT -- derive values without mutation
function UserCard({ name }: UserCardProps): React.ReactElement {
const displayName = name.toUpperCase();
return <h2>{displayName}</h2>;
}---
AP-12: HOC Applied Inside Render
Severity: Critical -- creates a new component type every render.
// WRONG -- new component type every render
function App(): React.ReactElement {
const EnhancedList = withSorting(List); // recreated every render!
return <EnhancedList items={items} />;
}
// CORRECT -- apply HOC at module scope
const EnhancedList = withSorting(List);
function App(): React.ReactElement {
return <EnhancedList items={items} />;
}---
Summary Table
| ID | Anti-Pattern | Severity | Fix |
|---|---|---|---|
| AP-01 | Component inside component | Critical | Define at module scope |
| AP-02 | Using React.FC | Low | Plain function declaration |
| AP-03 | Unfiltered prop spreading | Medium | Destructure known props first |
| AP-04 | Memo without stable props | Medium | useCallback/useMemo in parent |
| AP-05 | Conditional hook calls | Critical | Hooks at top level always |
| AP-06 | Index as key (dynamic lists) | High | Use stable data ID |
| AP-07 | forwardRef in React 19 | Low | ref as regular prop |
| AP-08 | lazy inside component | Critical | Declare at module scope |
| AP-09 | lazy without Suspense | Critical | Wrap in Suspense boundary |
| AP-10 | Portal event bubbling | Medium | stopPropagation in portal |
| AP-11 | Mutating props | Critical | Derive new values |
| AP-12 | HOC inside render | Critical | Apply at module scope |
Component Typing Examples
Reference file for react-syntax-components. Complete TypeScript examples for component patterns.---
Basic Function Component
interface GreetingProps {
readonly name: string;
readonly greeting?: string;
}
function Greeting({ name, greeting = 'Hello' }: GreetingProps): React.ReactElement {
return <h1>{greeting}, {name}!</h1>;
}---
Component with Event Handlers
interface ButtonProps {
readonly label: string;
readonly variant?: 'primary' | 'secondary' | 'danger';
readonly disabled?: boolean;
readonly onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
function Button({
label,
variant = 'primary',
disabled = false,
onClick,
}: ButtonProps): React.ReactElement {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
}---
Component with Children
interface LayoutProps {
readonly sidebar: React.ReactNode;
readonly children: React.ReactNode;
}
function Layout({ sidebar, children }: LayoutProps): React.ReactElement {
return (
<div className="layout">
<aside className="sidebar">{sidebar}</aside>
<main className="content">{children}</main>
</div>
);
}
// Usage
<Layout sidebar={<NavMenu />}>
<h1>Page Title</h1>
<p>Page content here.</p>
</Layout>---
Generic Data Table Component
interface Column<T> {
readonly key: keyof T & string;
readonly header: string;
readonly render?: (value: T[keyof T], row: T) => React.ReactNode;
}
interface DataTableProps<T> {
readonly data: readonly T[];
readonly columns: readonly Column<T>[];
readonly keyExtractor: (row: T) => string;
readonly onRowClick?: (row: T) => void;
}
function DataTable<T>({
data,
columns,
keyExtractor,
onRowClick,
}: DataTableProps<T>): React.ReactElement {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={col.key}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={keyExtractor(row)} onClick={() => onRowClick?.(row)}>
{columns.map((col) => (
<td key={col.key}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Usage with type inference
interface User {
id: string;
name: string;
email: string;
active: boolean;
}
const columns: Column<User>[] = [
{ key: 'name', header: 'Name' },
{ key: 'email', header: 'Email' },
{ key: 'active', header: 'Status', render: (val) => (val ? 'Active' : 'Inactive') },
];
<DataTable<User>
data={users}
columns={columns}
keyExtractor={(u) => u.id}
onRowClick={(user) => navigate(`/users/${user.id}`)}
/>---
Discriminated Union Props
// Use discriminated unions when props depend on a variant
type AlertProps =
| { readonly variant: 'success'; readonly message: string }
| { readonly variant: 'error'; readonly message: string; readonly retry: () => void }
| { readonly variant: 'loading' };
function Alert(props: AlertProps): React.ReactElement {
switch (props.variant) {
case 'success':
return <div className="alert-success">{props.message}</div>;
case 'error':
return (
<div className="alert-error">
{props.message}
<button onClick={props.retry}>Retry</button>
</div>
);
case 'loading':
return <div className="alert-loading">Loading...</div>;
}
}
// TypeScript enforces correct props per variant
<Alert variant="error" message="Failed" retry={() => refetch()} />
// <Alert variant="error" message="Failed" /> // ERROR: retry is required---
Component with Ref (React 19)
interface TextAreaProps {
readonly placeholder?: string;
readonly ref?: React.Ref<HTMLTextAreaElement>;
readonly onChange?: (value: string) => void;
}
function TextArea({ placeholder, ref, onChange }: TextAreaProps): React.ReactElement {
return (
<textarea
ref={ref}
placeholder={placeholder}
onChange={(e) => onChange?.(e.target.value)}
/>
);
}
// Parent
function Form(): React.ReactElement {
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
const handleSubmit = () => {
const value = textAreaRef.current?.value ?? '';
console.log('Submitted:', value);
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<TextArea ref={textAreaRef} placeholder="Enter text..." />
<button type="submit">Submit</button>
</form>
);
}---
Component with Ref (React 18 -- forwardRef)
interface TextAreaProps {
readonly placeholder?: string;
readonly onChange?: (value: string) => void;
}
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
function TextArea({ placeholder, onChange }, ref): React.ReactElement {
return (
<textarea
ref={ref}
placeholder={placeholder}
onChange={(e) => onChange?.(e.target.value)}
/>
);
}
);---
Memo with Generic Component
interface SelectOptionProps<T> {
readonly value: T;
readonly label: string;
readonly isSelected: boolean;
readonly onSelect: (value: T) => void;
}
// React.memo with generics requires a type assertion
const SelectOption = React.memo(function SelectOption<T>({
value,
label,
isSelected,
onSelect,
}: SelectOptionProps<T>): React.ReactElement {
return (
<li
className={isSelected ? 'selected' : ''}
onClick={() => onSelect(value)}
>
{label}
</li>
);
}) as <T>(props: SelectOptionProps<T>) => React.ReactElement;---
Lazy-Loaded Route Components
// ALWAYS at module top level
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
const Profile = React.lazy(() => import('./pages/Profile'));
function AppRoutes(): React.ReactElement {
return (
<React.Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</React.Suspense>
);
}
function PageSkeleton(): React.ReactElement {
return (
<div className="skeleton">
<div className="skeleton-header" />
<div className="skeleton-body" />
</div>
);
}---
Portal: Tooltip Component
import { createPortal } from 'react-dom';
interface TooltipProps {
readonly text: string;
readonly targetRect: DOMRect | null;
}
function Tooltip({ text, targetRect }: TooltipProps): React.ReactElement | null {
if (!targetRect) return null;
const style: React.CSSProperties = {
position: 'fixed',
top: targetRect.bottom + 8,
left: targetRect.left + targetRect.width / 2,
transform: 'translateX(-50%)',
};
return createPortal(
<div className="tooltip" style={style}>{text}</div>,
document.body
);
}
// Usage with hover state
function IconButton({ icon, tooltip }: { icon: string; tooltip: string }): React.ReactElement {
const [targetRect, setTargetRect] = React.useState<DOMRect | null>(null);
const buttonRef = React.useRef<HTMLButtonElement>(null);
return (
<>
<button
ref={buttonRef}
onPointerEnter={() => setTargetRect(buttonRef.current?.getBoundingClientRect() ?? null)}
onPointerLeave={() => setTargetRect(null)}
>
{icon}
</button>
<Tooltip text={tooltip} targetRect={targetRect} />
</>
);
}Component Composition Patterns
Reference file for react-syntax-components. Compound components, render props, and HOC patterns.---
Compound Components (Context-Based)
The compound component pattern creates a flexible API where related components share implicit state through Context. The parent owns the state; children consume it.
Full Pattern
import React, { createContext, useContext, useState, useCallback, type PropsWithChildren } from 'react';
// 1. Define shared state shape
interface AccordionContextValue {
readonly openItems: ReadonlySet<string>;
readonly toggle: (id: string) => void;
}
// 2. Create context with NO default -- forces provider usage
const AccordionContext = createContext<AccordionContextValue | null>(null);
// 3. Custom hook for safe consumption
function useAccordion(): AccordionContextValue {
const context = useContext(AccordionContext);
if (context === null) {
throw new Error('Accordion compound components must be used within <Accordion>');
}
return context;
}
// 4. Root component -- owns state, provides context
interface AccordionProps {
readonly allowMultiple?: boolean;
readonly children: React.ReactNode;
}
function Accordion({ allowMultiple = false, children }: AccordionProps): React.ReactElement {
const [openItems, setOpenItems] = useState<ReadonlySet<string>>(new Set());
const toggle = useCallback((id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!allowMultiple) next.clear();
next.add(id);
}
return next;
});
}, [allowMultiple]);
const value = React.useMemo(() => ({ openItems, toggle }), [openItems, toggle]);
return (
<AccordionContext value={value}>
<div className="accordion">{children}</div>
</AccordionContext>
);
}
// 5. Child components -- consume context
interface AccordionItemProps {
readonly id: string;
readonly children: React.ReactNode;
}
function AccordionItem({ id, children }: AccordionItemProps): React.ReactElement {
const { openItems } = useAccordion();
const isOpen = openItems.has(id);
return (
<div className={`accordion-item ${isOpen ? 'open' : ''}`} data-item-id={id}>
{children}
</div>
);
}
interface AccordionTriggerProps {
readonly itemId: string;
readonly children: React.ReactNode;
}
function AccordionTrigger({ itemId, children }: AccordionTriggerProps): React.ReactElement {
const { toggle } = useAccordion();
return (
<button className="accordion-trigger" onClick={() => toggle(itemId)}>
{children}
</button>
);
}
interface AccordionContentProps {
readonly itemId: string;
readonly children: React.ReactNode;
}
function AccordionContent({ itemId, children }: AccordionContentProps): React.ReactElement | null {
const { openItems } = useAccordion();
if (!openItems.has(itemId)) return null;
return <div className="accordion-content">{children}</div>;
}
// 6. Attach sub-components to root for clean API
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
// Usage
<Accordion allowMultiple>
<Accordion.Item id="faq-1">
<Accordion.Trigger itemId="faq-1">What is React?</Accordion.Trigger>
<Accordion.Content itemId="faq-1">
<p>React is a JavaScript library for building user interfaces.</p>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item id="faq-2">
<Accordion.Trigger itemId="faq-2">What are hooks?</Accordion.Trigger>
<Accordion.Content itemId="faq-2">
<p>Hooks let you use state and other React features in function components.</p>
</Accordion.Content>
</Accordion.Item>
</Accordion>When to Use Compound Components
- Building reusable UI primitives (tabs, accordions, dropdown menus, dialogs)
- Consumer needs flexible control over layout and ordering of sub-components
- Internal state must be shared without prop drilling
Rules
- ALWAYS throw an error in the context hook when used outside the provider
- ALWAYS memoize the context value to prevent unnecessary re-renders
- ALWAYS attach sub-components to the root component for a clean dot-notation API
- NEVER expose internal context outside the compound component module
---
Render Props Pattern
The render props pattern delegates rendering control to the consumer by accepting a function that returns React elements.
Function as Children
interface MouseTrackerProps {
readonly children: (position: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps): React.ReactElement {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
return (
<div
style={{ height: '100%' }}
onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
>
{children(position)}
</div>
);
}
// Usage
<MouseTracker>
{({ x, y }) => (
<div>
Mouse is at ({x}, {y})
</div>
)}
</MouseTracker>Named Render Prop
interface DataFetcherProps<T> {
readonly url: string;
readonly render: (data: T | null, loading: boolean, error: string | null) => React.ReactNode;
}
function DataFetcher<T>({ url, render }: DataFetcherProps<T>): React.ReactElement {
const [data, setData] = React.useState<T | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
let ignore = false;
setLoading(true);
setError(null);
fetch(url)
.then((res) => res.json() as Promise<T>)
.then((result) => {
if (!ignore) {
setData(result);
setLoading(false);
}
})
.catch((err: Error) => {
if (!ignore) {
setError(err.message);
setLoading(false);
}
});
return () => { ignore = true; };
}, [url]);
return <>{render(data, loading, error)}</>;
}
// Usage
<DataFetcher<User[]>
url="/api/users"
render={(data, loading, error) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
return <UserList users={data!} />;
}}
/>When to Use Render Props
- Consumer needs full control over what gets rendered with shared logic
- Building headless components (logic without UI)
- ALWAYS prefer custom hooks for pure logic sharing -- render props are for when you also need to wrap rendering
---
Higher-Order Components (HOC)
HOCs wrap a component to inject additional props or behavior. ALWAYS prefer custom hooks for new code -- HOCs are harder to type and debug.
Typed HOC Pattern
interface WithAuthProps {
readonly currentUser: User;
}
function withAuth<P extends WithAuthProps>(
WrappedComponent: React.ComponentType<P>
): React.ComponentType<Omit<P, keyof WithAuthProps>> {
function WithAuthComponent(props: Omit<P, keyof WithAuthProps>): React.ReactElement {
const currentUser = useAuth(); // custom hook
if (!currentUser) {
return <Navigate to="/login" />;
}
// Type assertion required due to TypeScript limitation with HOCs
return <WrappedComponent {...(props as P)} currentUser={currentUser} />;
}
// ALWAYS set displayName for DevTools debugging
WithAuthComponent.displayName = `withAuth(${WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'})`;
return WithAuthComponent;
}
// Usage
interface DashboardProps extends WithAuthProps {
readonly title: string;
}
function Dashboard({ currentUser, title }: DashboardProps): React.ReactElement {
return <h1>{title} - Welcome, {currentUser.name}</h1>;
}
const ProtectedDashboard = withAuth(Dashboard);
// <ProtectedDashboard title="Home" /> -- currentUser is injectedHOC Rules
- ALWAYS set
displayNameon the wrapper component - NEVER mutate the original component -- wrap it in a new component
- NEVER apply HOCs inside render -- ALWAYS apply at module scope
- ALWAYS forward all unknown props to the wrapped component
- ALWAYS prefer custom hooks over HOCs for new code
When HOCs Are Still Useful
- Wrapping third-party components you cannot modify
- Adding cross-cutting behavior to many route-level components (e.g., authentication guards)
- Legacy codebases migrating incrementally to hooks
---
Pattern Comparison
| Pattern | State Sharing | Render Control | TypeScript | Complexity |
|---|---|---|---|---|
| Custom Hook | Logic only | None | Excellent | Low |
| Compound Component | Implicit via Context | Consumer arranges children | Good | Medium |
| Render Props | Via function args | Consumer controls output | Good | Medium |
| HOC | Via injected props | Wrapper controls | Difficult | High |
Decision order (ALWAYS follow this): 1. Custom hook -- if you only need shared logic 2. Compound component -- if you need a flexible multi-part UI 3. Render props -- if consumer needs full rendering control 4. HOC -- only as a last resort or for legacy integration