Skip to content

TypeScript React Advanced — Complete Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. What does forwardRef do? Forwards a ref from a parent to a child's DOM element, enabling imperative access.

  2. What is a higher-order component? A function that takes a component and returns a new component with additional props/behavior.

  3. What is the render props pattern? A component that accepts a function as its children prop, calling it with state or methods.

  4. 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

Do I need forwardRef in every component?

Only if the component wraps a native DOM element and needs to expose it to parent components (common in form libraries).

What is the difference between HOCs and hooks?

Hooks replaced most HOC patterns. HOCs are still useful for wrapping (error boundaries, loading states) where hooks can't be used.

Can render props and hooks coexist?

Yes. Hooks are internal implementation; render props define the public API.

What is the `as` prop pattern?

A polymorphic component that renders as a different HTML element or custom component while preserving prop types.

Should I use compound components for everything?

No. Compound components are best for complex, flexible UIs (tables, tabs, forms). Simple components don't need this pattern.

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