Chuyển đến nội dung chính

Lesson 2: Components, Props & TypeScript

Functional components, props, children. TypeScript with React, typing props, events, refs. Component composition, render props pattern. React.FC vs function declarations.

💻 Programming — Lesson 2 Lesson 2: Components, Props & TypeScript

React & Next.js: From Basics to Advanced

Part 1: React Fundamentals

xdev.asia

1. Functional Components

// Cách 1: Function declaration (khuyến khích)
function Button() {
  return <button>Click me</button>;
}

// Cách 2: Arrow function
const Button = () => {
  return <button>Click me</button>;
};

// Cách 3: React.FC (KHÔNG khuyến khích — implicit children, generic issues)
const Button: React.FC = () => {
  return <button>Click me</button>;
};

2. Props

// Define props interface
interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary' | 'danger'; // Optional with union type
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick: () => void;
}

function Button({ label, variant = 'primary', size = 'md', disabled = false, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// Usage
<Button label="Submit" variant="primary" onClick={() => console.log('clicked')} />
<Button label="Cancel" variant="secondary" onClick={handleCancel} disabled />

3. Children Prop

interface CardProps {
  title: string;
  children: React.ReactNode; // Explicit children typing
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="User Profile">
  <p>Name: Duy Tran</p>
  <p>Role: Developer</p>
</Card>

4. TypeScript Patterns for React

4.1. Event Typing

function Form() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('clicked');
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      // submit
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

4.2. Generic Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

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

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

4.3. Discriminated Unions

// Props that depend on each other
type AlertProps =
  | { variant: 'success'; message: string }
  | { variant: 'error'; message: string; onRetry: () => void }
  | { variant: 'loading' };

function Alert(props: AlertProps) {
  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.onRetry}>Retry</button>
        </div>
      );
    case 'loading':
      return <div className="alert-loading">Loading...</div>;
  }
}

5. Component Composition

// Compound Components pattern
interface TabsProps {
  children: React.ReactNode;
  defaultTab?: string;
}

function Tabs({ children, defaultTab }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab || '');

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

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

function Tab({ value, children }: { value: string; children: React.ReactNode }) {
  const { activeTab, setActiveTab } = useTabsContext();
  return (
    <button
      role="tab"
      className={activeTab === value ? 'active' : ''}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
}

function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
  const { activeTab } = useTabsContext();
  if (activeTab !== value) return null;
  return <div role="tabpanel">{children}</div>;
}

// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
<Tabs defaultTab="overview">
  <Tabs.List>
    <Tabs.Tab value="overview">Overview</Tabs.Tab>
    <Tabs.Tab value="settings">Settings</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="overview">Content here</Tabs.Panel>
  <Tabs.Panel value="settings">Settings here</Tabs.Panel>
</Tabs>

Next article: Hooks Deep Dive — useState, useEffect, useRef, useMemo, useCallback, and custom hooks.