children та Render Props.React.memo та проблема нестабільних посилань (Referential Equality).Для розробників рівня Senior/Middle+, розуміння компонентів React виходить далеко за межі синтаксису “функція повертає JSX”. На масштабах ентерпрайз-додатків компонент стає фундаментальною одиницею архітектури. Нерозуміння принципів імутабельності (Immutability), відмінності між передачею за посиланням (by reference) та за значенням (by value), а також побічних ефектів (Side Effects) неминуче призводить до серйозних проблем із продуктивністю (zombie renders, memory leaks) та архітектурної зв’язності (tight coupling). Ця лекція фокусується на глибинних механізмах функціональних компонентів, патернах їх композиції та застосуванні строгих контрактів за допомогою TypeScript.
До версії React 16.8 (2019 рік) індустрія ділила компоненти на “розумні” (Smart/Class) та “дурні” (Dumb/Functional). Класи використовувалися там, де був потрібен стан або методи життєвого циклу (componentDidMount). Проте класи принесли три системні проблеми:
this: Непередбачувана втрата контексту при передачі методів класу як callback-функцій вимагала постійного .bind(this) або використання стрілочних функцій, що генерували нові посилання при кожному рендері.componentDidMount та componentWillUnmount.Перехід на функціональні компоненти з Хуками (Hooks) дозволив застосувати підходи функціонального програмування: відокремлення даних від поведінки та можливість композиції логіки (Custom Hooks).
React покладається на те, що кожен функціональний компонент є чистою функцією відносно своїх пропсів.
У комп’ютерних науках чиста функція має дві властивості:
Порушення чистоти (Мутація пропсів - АНТИПАТЕРН):
const BadProfile = ({ user }) => {
// КАТЕГОРИЧНО ЗАБОРОНЕНО: мутація об'єкта, що прийшов за посиланням
user.lastLogin = new Date(); // Side Effect!
return (
<div>
{user.name} logged at {user.lastLogin.toString()}
</div>
);
};
React у Strict Mode навмисно викликає функції-компоненти двічі у середовищі розробки, щоб виявити такі приховані мутації. Архітектурне правило: Рендеринг має бути чистою операцією обчислення дерева UI. Будь-які мутації або запити в мережу повинні бути ізольовані (через обробники подій або useEffect).
props (скорочення від properties) — це JavaScript об’єкт, який передається від батьківського компонента до дочірнього.
Головна архітектурна аксіома: Props є read-only (лише для читання).
Коли ми пишемо JSX: <UserCard name="Alex" config= onClick={handleClick} />, компілятор збирає всі атрибути в один об’єкт.
Важливо розуміти, що при кожному рендері батьківського компонента створюється новий об’єкт props з новим посиланням у пам’яті.
Але значення всередині цього об’єкта передаються згідно з правилами JS:
name="Alex", числа) передаються за значенням.config=, onClick={handleClick}) передаються за посиланням (by reference).Це означає, що створення інлайн-об’єкта config= гарантовано створює нове посилання в купі (Heap) при кожному рендері, навіть якщо розробнику здається, що вміст не змінився. Це прямо впливає на оптимізацію компонента.
Архітектура React базується на One-Way Data Flow (Односпрямованому потоці даних), що спускається зверху вниз по дереву компонентів. Дочірній компонент не має права і технічної можливості напряму змінити пропси, які йому передали. Якщо “дитина” потребує зміни даних, використовується патерн Lifting State Up (Підняття стану): дитині передається callback-функція, яку вона викликає, щоб повідомити “батька” про подію.
// Інженерний грамотний дизайн: компонент не знає ПРО бізнес-логіку, він лише диспетчеризує події
interface TodoItemProps {
id: string;
title: string;
isCompleted: boolean;
onToggle: (id: string, nextStatus: boolean) => void; // Контракт зворотної дії
}
const TodoItem = ({ id, title, isCompleted, onToggle }: TodoItemProps) => (
<div onClick={() => onToggle(id, !isCompleted)}>
{title} - {isCompleted ? "Done" : "Pending"}
</div>
);
Цей підхід робить компоненти надзвичайно передбачуваними, оскільки джерело істини (Single Source of Truth) знаходиться виключно нагорі, а не розмазане по всьому додатку (як це було при двоспрямованому зв’язуванні, Two-Way Binding, в AngularJS).
children та Render PropsУ складних ентерпрайз-додатках жорстке прокидання пропсів через багато рівнів (Prop Drilling) призводить до сильного зв’язування (Tight Coupling). Системна інженерія вирішує це через патерни композиції.
children (Вкладеність як абстракція)Властивість props.children дозволяє компоненту виступати в ролі “контейнера”, не знаючи наперед, який контент у нього передадуть.
// Modal.tsx - контейнер знає ЛИШЕ про візуальне оформлення вікна
const Modal = ({
children,
onClose,
}: {
children: React.ReactNode;
onClose: () => void;
}) => {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
};
// App.tsx
<Modal onClose={close}>
<ComplexBillingForm /> {/* Modal нічого не знає про BillingForm. Це IoC. */}
</Modal>;
Якщо “дитина” повинна повідомити якісь дані “нагору” безпосередньо в момент побудови JSX, використовується патерн Render Props (проп-функція, що повертає JSX).
<MouseTracker
render={({ x, y }) => (
<h1>
Курсор знаходиться на: {x}, {y}
</h1>
)}
/>
На рівні Senior розробки JavaScript без типізації для великих проектів вважається неприпустимим. TypeScript забезпечує статичну перевірку контрактів (Props) ще на етапі написання (Compile-time), що ліквідує цілий клас помилок (ReferenceError, TypeError).
Типовий ентерпрайз патерн типізації:
import type { ComponentPropsWithoutRef, ReactNode } from "react";
// Наслідуємо ВСІ нативні атрибути кнопки (type, disabled, aria-label, onClick)
interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary" | "danger";
isLoading?: boolean;
leftIcon?: ReactNode; // Типізація для будь-якого React-вмісту (JSX, рядки, фрагменти)
}
// Деструктуризація пропсів + збір решти (...rest) для делегування в DOM
export const Button = ({
variant = "primary",
isLoading,
leftIcon,
children,
className,
...rest
}: ButtonProps) => {
return (
<button
className={`btn-${variant} ${className || ""}`}
disabled={isLoading || rest.disabled}
{...rest} // Прокидаємо всі інші нативні пропси до реального DOM елементу
>
{isLoading ? <Spinner /> : leftIcon}
{children}
</button>
);
};
Використання ComponentPropsWithoutRef<'button'> звільняє розробника від ручного написання інтерфейсів для подій миші, клавіатури, tabindex тощо. Прокидання ...rest гарантує, що кастомна кнопка підтримуватиме всі ті самі атрибути, що й нативна HTML-кнопка (a11y атрибути, data-атрибути).
React.memo та Referential EqualityЗа замовчуванням, якщо батьківський компонент рендериться, React рекурсивно рендерить всі його дочірні компоненти, незалежно від того, змінилися їхні пропси чи ні. На великих деревах це величезна трата ресурсів процесора.
Для оптимізації використовують HOC (High-Order Component) React.memo(). Він здійснює Shallow Comparison (поверхневе порівняння через Object.is()) старих і нових пропсів. Якщо вони ідентичні — компонент не перераховується.
Проблема глибокого порівняння (Referential Equality):
const Parent = () => {
// Кожен рендер створює НОВЕ місце в пам'яті для об'єкта config та функції onAction
const config = { theme: "dark" };
const handleAction = () => console.log("Action");
// React.memo для <Child /> БУДЕ ЗЛАМАНО. Object.is(oldConfig, newConfig) === false
return <MemoizedChild config={config} onAction={handleAction} />;
};
Для вирішення цього, розробники зобов’язані стабілізувати посилання (Referential Stability) за допомогою хуків мемоїзації: useMemo (для обчислень та об’єктів) та useCallback (для функцій). Без цього React.memo стає “мертвим кодом”, що виконує марні порівняння і все одно спричиняє ререндер.
ComponentProps створює надійну систему контролю типів, запобігаючи використанню незадокументованих атрибутів типізуючи розриви в API (Breaking API Changes) на етапі компіляції.children та делегування пропсів (через ...rest) дозволяють створювати гнучкі, слабо пов’язані (loosely-coupled) архітектурні рішення та ефективні бібліотеки базових компонентів (UI Kits).props (наприклад, props.user.name = 'Нове ім'я') є антипатерном і як саме це ламає архітектуру односпрямованого потоку даних?children (створення Wrapper-компонентів)?ComponentPropsWithoutRef?config= безпосередньо у пропси компонента, огорнутого в React.memo, призведе до його гарантованого ререндеру?