TypeScript React patterns in 2026 go beyond basic component typing. Compound components, render props, custom hooks with complex return types, and React 19’s new server component patterns all benefit from TypeScript’s type system. This guide covers the patterns senior React developers use daily.
📋 Table of Contents
Generic Components
// Generic list component — renders any data type
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyComponent?: React.ReactNode;
className?: string;
}
function List<T>({
items,
renderItem,
keyExtractor,
emptyComponent = <p>No items</p>,
className,
}: ListProps<T>) {
if (items.length === 0) return <>{emptyComponent}</>;
return (
<ul className={className}>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage — fully type-safe
interface User { id: number; name: string; email: string; }
<List<User>
items={users}
keyExtractor={u => u.id}
renderItem={user => (
<UserCard user={user} />
)}
/>
Polymorphic Components
// A component that can render as any HTML element or component
type AsProp<T extends React.ElementType> = { as?: T };
type PropsToOmit<T extends React.ElementType, P> = keyof (AsProp<T> & P);
type PolymorphicComponentProp<T extends React.ElementType, Props = {}> = React.PropsWithChildren<
Props & AsProp<T>
> &
Omit<React.ComponentPropsWithoutRef<T>, PropsToOmit<T, Props>>;
type PolymorphicRef<T extends React.ElementType> = React.ComponentPropsWithRef<T>["ref"];
type PolymorphicComponentPropWithRef<T extends React.ElementType, Props = {}> =
PolymorphicComponentProp<T, Props> & { ref?: PolymorphicRef<T> };
// Text component that renders as any element
interface TextProps {
variant?: "h1" | "h2" | "h3" | "body" | "caption";
color?: "primary" | "secondary" | "muted";
}
const variantMap = { h1: "h1", h2: "h2", h3: "h3", body: "p", caption: "span" } as const;
function Text<T extends React.ElementType = "span">({
as,
variant = "body",
color = "primary",
children,
className,
...props
}: PolymorphicComponentProp<T, TextProps>) {
const Component = as ?? variantMap[variant];
return (
<Component
className={`text-${variant} text-${color} ${className ?? ""}`}
{...props}
>
{children}
</Component>
);
}
// Usage
<Text variant="h1">Heading 1</Text> // renders <h1>
<Text as="label" htmlFor="email">Email</Text> // renders <label>
<Text as={Link} to="/about" variant="body">About</Text> // renders <Link>
Compound Components Pattern
import { createContext, useContext, useState } from 'react';
// Accordion compound component
interface AccordionContextValue {
openItem: string | null;
setOpenItem: (id: string | null) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const ctx = useContext(AccordionContext);
if (!ctx) throw new Error('Must be used within <Accordion>');
return ctx;
}
function Accordion({ children, defaultOpen = null }: {
children: React.ReactNode;
defaultOpen?: string | null;
}) {
const [openItem, setOpenItem] = useState<string | null>(defaultOpen);
return (
<AccordionContext.Provider value={{ openItem, setOpenItem }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ id, title, children }: {
id: string;
title: string;
children: React.ReactNode;
}) {
const { openItem, setOpenItem } = useAccordion();
const isOpen = openItem === id;
return (
<div className={`accordion-item ${isOpen ? 'open' : ''}`}>
<button onClick={() => setOpenItem(isOpen ? null : id)}>
{title}
<span>{isOpen ? '▲' : '▼'}</span>
</button>
{isOpen && <div className="accordion-content">{children}</div>}
</div>
);
}
// Attach as namespaced components
Accordion.Item = AccordionItem;
// Usage
<Accordion defaultOpen="faq-1">
<Accordion.Item id="faq-1" title="What is TypeScript?">
TypeScript is a typed superset of JavaScript...
</Accordion.Item>
<Accordion.Item id="faq-2" title="Why use React?">
React provides a declarative way to build UIs...
</Accordion.Item>
</Accordion>
Typed Custom Hooks
import { useCallback, useReducer } from 'react';
// Async state management hook
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
type AsyncAction<T> =
| { type: 'start' }
| { type: 'success'; data: T }
| { type: 'error'; error: Error }
| { type: 'reset' };
function asyncReducer<T>(state: AsyncState<T>, action: AsyncAction<T>): AsyncState<T> {
switch (action.type) {
case 'start': return { status: 'loading' };
case 'success': return { status: 'success', data: action.data };
case 'error': return { status: 'error', error: action.error };
case 'reset': return { status: 'idle' };
}
}
function useAsync<T, Args extends unknown[]>(
fn: (...args: Args) => Promise<T>
) {
const [state, dispatch] = useReducer(
asyncReducer as React.Reducer<AsyncState<T>, AsyncAction<T>>,
{ status: 'idle' } as AsyncState<T>
);
const execute = useCallback(
async (...args: Args) => {
dispatch({ type: 'start' });
try {
const data = await fn(...args);
dispatch({ type: 'success', data });
return data;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
dispatch({ type: 'error', error });
throw error;
}
},
[fn]
);
const reset = useCallback(() => dispatch({ type: 'reset' }), []);
return { ...state, execute, reset };
}
// Usage with full type inference
function UserProfile({ userId }: { userId: number }) {
const { status, data, error, execute } = useAsync(fetchUser);
useEffect(() => { execute(userId); }, [userId]);
if (status === 'loading') return <Spinner />;
if (status === 'error') return <Error message={error.message} />;
if (status === 'success') return <Profile user={data} />; // data is User
return null;
}
React 19 Server Components with TypeScript
// Server Component — no 'use client', runs on server
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
interface Props {
params: Promise<{ slug: string }>;
searchParams: Promise<{ view?: string }>;
}
// Type-safe metadata generation
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await fetchPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
},
};
}
export default async function PostPage({ params, searchParams }: Props) {
const { slug } = await params;
const { view = 'full' } = await searchParams;
const post = await fetchPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
{view === 'full' ? (
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
) : (
<p>{post.excerpt}</p>
)}
<Suspense fallback={<CommentSkeleton />}>
<Comments postId={post.id} />
</Suspense>
</article>
);
}
// Client component with typed props
'use client';
interface CommentFormProps {
postId: number;
onSubmit: (comment: { text: string; author: string }) => Promise<void>;
}
function CommentForm({ postId, onSubmit }: CommentFormProps) {
// ...client-side interactivity
}
TypeScript React patterns in 2026 are mature — generic components eliminate duplicate code, polymorphic components create flexible APIs, compound components replace complex prop drilling, and typed custom hooks make async patterns safe. The combination of React 19’s server components with TypeScript creates fully type-safe full-stack applications.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment