TypeScript React Advanced — Complete Guide
In this tutorial, you will learn about TypeScript React Advanced. We cover key concepts, practical examples, and best practices to help you master this topic.
Advanced React patterns like forwardRef, HOCs, render props, and compound components become even more powerful with TypeScript's type system — enabling reusable, type-safe component abstractions that are impossible to misuse.
What You'll Learn
- forwardRef with generic types
- Higher-order components with type preservation
- Render props with typed functions
- Compound component patterns
- Generics in component props
Why It Matters
Advanced patterns solve real problems — forwarding refs for form libraries, HOCs for cross-cutting concerns, compound components for flexible APIs. TypeScript ensures these patterns are used correctly, catching misuse at compile time.
Real-World Use
DodaTech's UI library uses forwardRef for all form inputs (ref forwarding to the native input), compound components for data tables (Table, TableHead, TableBody, TableRow), and a generic HOC for error boundary wrapping.
Learning Path
flowchart LR A[State Management] --> B[React Advanced] B --> C[Node Setup] B --> D[You Are Here] C --> E[Express APIs] E --> F[Next.js]
forwardRef
Forwarding refs to DOM elements with proper typing:
import { forwardRef, ChangeEvent } from 'react';
interface InputProps {
label: string;
error?: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, onChange }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} onChange={onChange} className={error ? 'error' : ''} />
{error && <span className="error-message">{error}</span>}
</div>
);
}
);
Input.displayName = 'Input';
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null!);
return <Input ref={inputRef} label="Name" onChange={() => {}} />;
}
Generic forwardRef
interface SelectProps<T extends string> {
options: T[];
value: T;
onChange: (value: T) => void;
}
const Select = forwardRef<HTMLSelectElement, SelectProps<string>>(
({ options, value, onChange }, ref) => {
return (
<select ref={ref} value={value} onChange={(e) => onChange(e.target.value as any)}>
{options.map(opt => <option key={opt} value={opt}>{opt}</option>)}
</select>
);
}
);
Higher-Order Components (HOCs)
interface WithLoadingProps {
loading: boolean;
}
function withLoading<T extends object>(
Component: React.ComponentType<T & WithLoadingProps>
) {
return function WrappedComponent(props: T & { isLoading: boolean }) {
const { isLoading, ...rest } = props;
if (isLoading) {
return <div className="spinner">Loading...</div>;
}
return <Component {...(rest as T)} loading={false} />;
};
}
// Usage
interface UserProfileProps {
user: { name: string; email: string };
loading: boolean;
}
function UserProfile({ user, loading }: UserProfileProps) {
return <div>{user.name}</div>;
}
const UserProfileWithLoading = withLoading(UserProfile);
// <UserProfileWithLoading user={{ name: 'Alice', email: 'a@b.com' }} isLoading={true} />
Render Props
interface DataFetcherProps<T> {
url: string;
children: (data: { data: T | null; loading: boolean; error: string | null }) => ReactNode;
}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then((d: T) => { setData(d); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, [url]);
return <>{children({ data, loading, error })}</>;
}
// Usage
interface Scan { id: string; threats: string[]; }
<DataFetcher<Scan[]> url="/api/scans">
{({ data, loading }) => loading ? <p>Loading...</p> : <pre>{JSON.stringify(data)}</pre>}
</DataFetcher>
Generic Compound Components
interface TableContextType<T> {
data: T[];
selectedId: string | null;
onSelect: (id: string) => void;
}
const TableContext = createContext<TableContextType<any> | undefined>(undefined);
interface TableProps<T> {
data: T[];
children: ReactNode;
onSelect?: (item: T) => void;
}
function Table<T extends { id: string }>({ data, children, onSelect }: TableProps<T>) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleSelect = (id: string) => {
setSelectedId(id);
onSelect?.(data.find(item => item.id === id)!);
};
return (
<TableContext.Provider value={{ data, selectedId, onSelect: handleSelect }}>
<table>{children}</table>
</TableContext.Provider>
);
}
function TableHead({ children }: { children: ReactNode }) {
return <thead>{children}</thead>;
}
function TableBody<T extends { id: string }>() {
const context = useContext(TableContext);
if (!context) throw new Error('TableBody must be inside Table');
return (
<tbody>
{context.data.map((item: T) => (
<tr
key={item.id}
onClick={() => context.onSelect(item.id)}
className={context.selectedId === item.id ? 'selected' : ''}
>
{Object.values(item).map((val, i) => <td key={i}>{String(val)}</td>)}
</tr>
))}
</tbody>
);
}
Table.Head = TableHead;
Table.Body = TableBody;
// Usage
interface User { id: string; name: string; email: string; }
<Table<User> data={users} onSelect={(user) => console.log(user.name)}>
<Table.Head>
<tr><th>Name</th><th>Email</th></tr>
</Table.Head>
<Table.Body />
</Table>
Generic Polymorphic Components
type PolymorphicProps<T extends React.ElementType, P = {}> = {
as?: T;
children: ReactNode;
} & P & Omit<React.ComponentPropsWithoutRef<T>, keyof (P & { as?: T; children: ReactNode })>;
function Box<T extends React.ElementType = 'div'>({ as, children, ...props }: PolymorphicProps<T>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// Usage
<Box as="section" className="container">
<h2>Content</h2>
</Box>
<Box as="button" onClick={() => {}} type="button">
Click
</Box>
Common Mistakes
1. Losing Type Information in HOCs
Always propagate generic types through HOCs. Use Omit to remove injected props from the wrapped component's props.
2. Not Forwarding Refs
If a component wraps DOM elements, always forward refs. Libraries like react-hook-form and react-final-form require ref forwarding.
3. Complex Generic Constraints
Keep generic parameters simple. Too many constraints make components hard to use.
4. Not Using as const for Discriminated Unions in Props
interface Props {
variant: 'primary' | 'secondary'; // without as const, string is allowed
}
5. Compound Components Without Context
Compound components need shared state (which child is selected). Always use Context for this.
Practice Questions
What does forwardRef do? Forwards a ref from a parent to a child's DOM element, enabling imperative access.
What is a higher-order component? A function that takes a component and returns a new component with additional props/behavior.
What is the render props pattern? A component that accepts a function as its children prop, calling it with state or methods.
How do compound components share state? Through React Context — the parent provides state via context, children consume it.
Challenge: Create a generic Tabs compound component with TabList, Tab, and TabPanel children. Use TypeScript generics to type the tab values.
FAQ
Mini Project: Typed Tabs Component
interface TabsContextType<T extends string> {
activeTab: T;
setActiveTab: (tab: T) => void;
}
function createTabs<T extends string>() {
const TabsContext = createContext<TabsContextType<T> | undefined>(undefined);
function Tabs({ tabs, initialTab, children }: {
tabs: { id: T; label: string }[];
initialTab: T;
children: ReactNode;
}) {
const [activeTab, setActiveTab] = useState<T>(initialTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ id, children }: { id: T; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
return (
<button className={ctx.activeTab === id ? 'active' : ''} onClick={() => ctx.setActiveTab(id)}>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: T; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
return ctx.activeTab === id ? <div className="tab-panel">{children}</div> : null;
}
return { Tabs, TabList, Tab, TabPanel };
}
const { Tabs, TabList, Tab, TabPanel } = createTabs<'scan' | 'results' | 'settings'>();
<Tabs tabs={[{ id: 'scan', label: 'Scan' }, { id: 'results', label: 'Results' }]} initialTab="scan">
<TabList>
<Tab id="scan">Scan</Tab>
<Tab id="results">Results</Tab>
</TabList>
<TabPanel id="scan">Scan content</TabPanel>
<TabPanel id="results">Results content</TabPanel>
</Tabs>
What's Next
You've completed Module 6: React with TypeScript. Now explore Node.js and Backend:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/41-react-state-management" >}} | Review state management |
| {{< ref "/programming-languages/typescript/43-node-setup" >}} | Node.js TypeScript setup |
| {{< ref "/programming-languages/typescript/44-express-apis" >}} | Express with TypeScript |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro