Урок 03

Композиция и подъём

Иногда поднимать состояние неудобно. В таких случаях помогает композиция — передача готовых компонентов как children или пропсов.

Пример без композиции

function App() {
    const [user, setUser] = useState<User | null>(null);

    return (
        <Layout user={user}>
            <Sidebar user={user} />
            <Profile user={user} />
        </Layout>
    );
}

function Layout({ user, children }: { user: User | null; children: React.ReactNode }) {
    return (
        <div>
            <header>{user?.name}</header>
            {children}
        </div>
    );
}

Layout не использует user как данные — он просто передаёт его дальше. Это лишний пропс.

Композиция через children

function App() {
    const [user, setUser] = useState<User | null>(null);

    return (
        <Layout header={<h1>{user?.name}</h1>}>
            <Sidebar user={user} />
            <Profile user={user} />
        </Layout>
    );
}

function Layout({ header, children }: {
    header: React.ReactNode;
    children: React.ReactNode;
}) {
    return (
        <div>
            <header>{header}</header>
            {children}
        </div>
    );
}

Теперь Layout не знает про user — он принимает готовую разметку.

Композиция вместо подъёма

Иногда вместо того чтобы поднимать состояние, проще передать компонент целиком:

// Вместо
<Layout user={user} />

// Лучше
<Layout sidebar={<UserCard user={user} />} />

Layout не знает, что такое user, и не должен знать.

Render props

Компонент принимает функцию, которая возвращает разметку:

function List<T>({ items, renderItem }: {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
}) {
    return <ul>{items.map(renderItem)}</ul>;
}

<List
    items={users}
    renderItem={(user) => <li key={user.id}>{user.name}</li>}
/>

Компонент List отвечает только за список. Как отрисовать элемент — решает родитель.

Контроль над состоянием

Иногда компонент предоставляет своё состояние родителю через callback:

function ControlledInput({ value, onChange }: {
    value: string;
    onChange: (v: string) => void;
}) {
    return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}

Родитель хранит значение, компонент отображает.

Неуправляемый компонент

Если состояние не нужно — компонент сам хранит значение:

function UncontrolledInput() {
    const [value, setValue] = useState('');
    return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

Родитель не может прочитать или изменить это значение.

Когда что использовать

  • Подъём — когда данные нужны нескольким компонентам
  • Композиция — когда компонент не должен знать о структуре данных
  • Render props — когда нужно гибко управлять рендером
  • Контролируемый — когда родитель управляет значением

Итоги

  • Композиция уменьшает prop drilling
  • children и пропсы-элементы делают компоненты гибкими
  • Render props дают контроль над рендером
  • Контролируемый компонент — состояние у родителя
  • Неуправляемый — состояние внутри