Skip to content
advanced Phase 4 · TypeScript with React

React TypeScript Patterns

Use generics, discriminated unions, and typed context in React apps.

1h 15m
0 problems
Topic Progress 0%

Compound Components

Compound Components

Pattern with Context

interface TabsContextType {
  activeTab: string;
  setActiveTab: (id: string) => void;
}

const TabsContext = createContext<TabsContextType | undefined>(undefined);

function useTabs() {
  const context = useContext(TabsContext);
  if (!context) throw new Error('useTabs must be used within Tabs');
  return context;
}

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }: { children: ReactNode }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab, setActiveTab } = useTabs();
  return (
    <button
      role="tab"
      aria-selected={activeTab === id}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

// Usage
<Tabs defaultTab="home">
  <TabList>
    <Tab id="home">Home</Tab>
    <Tab id="settings">Settings</Tab>
  </TabList>
</Tabs>

Polymorphic Components

Polymorphic Components

The 'as' Prop Pattern

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// Usage
type TextProps<C extends React.ElementType> = PolymorphicComponentProp<
  C,
  { size?: 'sm' | 'md' | 'lg'; color?: string }
>;

function Text<C extends React.ElementType = 'span'>({
  as,
  size = 'md',
  color,
  children,
  ...rest
}: TextProps<C>) {
  const Component = as || 'span';
  return (
    <Component className={`text-${size}`} style={{ color }} {...rest}>
      {children}
    </Component>
  );
}

// Renders as <span>
<Text size="lg" color="red">Hello</Text>

// Renders as <p>
<Text as="p" size="sm">Paragraph text</Text>

Generic Components

Generic Components

Type-Safe List Component

interface ListProps<T> {
  items: T[];
  keyExtractor: (item: T) => string;
  renderItem: (item: T, index: number) => ReactNode;
  emptyMessage?: string;
}

function List<T>({ items, keyExtractor, renderItem, emptyMessage = 'No items' }: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty">{emptyMessage}</div>;
  }

  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// Usage
interface User {
  id: number;
  name: string;
}

<List<User>
  items={users}
  keyExtractor={(user) => String(user.id)}
  renderItem={(user) => <span>{user.name}</span>}
/>

Generic Form Component

interface FormProps<T> {
  initialValues: T;
  onSubmit: (values: T) => void;
  validate?: (values: T) => Partial<Record<keyof T, string>>;
  children: (props: {
    values: T;
    errors: Partial<Record<keyof T, string>>;
    handleChange: (field: keyof T) => (value: T[keyof T]) => void;
    handleSubmit: () => void;
  }) => ReactNode;
}

function Form<T extends Record<string, unknown>>({
  initialValues,
  onSubmit,
  validate,
  children
}: FormProps<T>) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});

  const handleChange = (field: keyof T) => (value: T[keyof T]) => {
    setValues(prev => ({ ...prev, [field]: value }));
  };

  const handleSubmit = () => {
    if (validate) {
      const newErrors = validate(values);
      setErrors(newErrors);
      if (Object.keys(newErrors).length > 0) return;
    }
    onSubmit(values);
  };

  return <form>{children({ values, errors, handleChange, handleSubmit })}</form>;
}

Higher-Order Components

Higher-Order Components

HOC Pattern

// HOC that adds loading state
function withLoading<P extends object>(
  WrappedComponent: React.ComponentType<P>
) {
  return function WithLoadingComponent(
    props: P & { isLoading: boolean }
  ) {
    const { isLoading, ...restProps } = props;

    if (isLoading) {
      return <div className="spinner">Loading...</div>;
    }

    return <WrappedComponent {...restProps as P} />;
  };
}

// Usage
function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

const UserListWithLoading = withLoading(UserList);

// Now accepts isLoading prop
<UserListWithLoading users={users} isLoading={true} />

HOC with Additional Props

function withAuth<P extends object>(
  WrappedComponent: React.ComponentType<P & { user: User }>
) {
  return function WithAuthComponent(props: P) {
    const user = useAuth();

    if (!user) {
      return <Redirect to="/login" />;
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

Best Practices

  • Prefer composition over HOCs when possible
  • Use custom hooks for logic reuse
  • Keep HOCs focused on a single concern
  • Use generics to maintain type safety