Пошук уроків, статей та іншого контенту
Використовуйте React Context для передачі спільних даних без надмірного прокидання пропсів через дерево компонентів.
У React дані зазвичай передаються від батьківського компонента до дочірнього через props. Це прозорий і передбачуваний підхід, але іноді дані потрібні багатьом компонентам на різних рівнях дерева.
Наприклад:
App
└── Layout
├── Header
│ └── UserMenu
└── Main
└── SettingsЯкщо UserMenu і Settings повинні отримати інформацію про поточного користувача, її можна передати через усі проміжні компоненти:
<App user={user}>
<Layout user={user}>
<Header user={user}>
<UserMenu user={user} />
</Header>
</Layout>
</App>Проміжні компоненти в цьому випадку не використовують user, а лише передають його далі. Це називається prop drilling, або надмірне прокидання пропсів.
Context дає змогу створити спільне джерело даних і отримувати ці дані безпосередньо в потрібних компонентах.
Для роботи з Context зазвичай використовують:
createContext — створює контекст;
Provider — надає значення компонентам-предкам;
useContext — отримує значення контексту в компоненті.
Загальна схема має такий вигляд:
const SomeContext = createContext(defaultValue);
function App() {
return (
<SomeContext.Provider value={someValue}>
<ChildComponent />
</SomeContext.Provider>
);
}
function ChildComponent() {
const value = useContext(SomeContext);
return <p>{value}</p>;
}Компонент може отримати значення контексту, якщо він знаходиться всередині відповідного Provider.
Розглянемо приклад із темою інтерфейсу. Користувач може перемикати світлу й темну тему, а різні компоненти отримують поточну тему через Context.
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});
function App() {
const [theme, setTheme] = useState('light');
function toggleTheme() {
setTheme((currentTheme) =>
currentTheme === 'light' ? 'dark' : 'light'
);
}
const contextValue = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={contextValue}>
<Page />
</ThemeContext.Provider>
);
}
function Page() {
return (
<main className="page">
<Header />
<Content />
</main>
);
}
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header className={`header ${theme}`}>
<h1>Налаштування теми</h1>
<button type="button" onClick={toggleTheme}>
Увімкнути {theme === 'light' ? 'темну' : 'світлу'} тему
</button>
</header>
);
}
function Content() {
const { theme } = useContext(ThemeContext);
return (
<section className={`content ${theme}`}>
<p>Поточна тема: {theme}</p>
</section>
);
}
export default App;У цьому прикладі:
ThemeContext зберігає значення теми.
ThemeContext.Provider робить це значення доступним для всіх дочірніх компонентів.
Header отримує theme і toggleTheme.
Content отримує лише theme.
Page не приймає і не передає жодних пропсів, пов’язаних із темою.
Оголошене значення за замовчуванням:
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});використовується лише тоді, коли компонент викликає useContext(ThemeContext) поза Provider. У звичайному сценарії значення береться з найближчого Provider.
Provider приймає значення через проп value:
<ThemeContext.Provider value={contextValue}>
<Page />
</ThemeContext.Provider>Усі компоненти всередині цього Provider, які використовують useContext(ThemeContext), отримують значення contextValue.
Важливо, що Provider діє лише на своїх нащадків:
<ThemeContext.Provider value="dark">
<Inside />
</ThemeContext.Provider>
<Outside />Inside побачить значення "dark", а Outside не матиме доступу до цього Provider.
Якщо в дереві є кілька Provider одного типу, компонент отримує значення найближчого з них:
<ThemeContext.Provider value="light">
<section>
<ThemeContext.Provider value="dark">
<Button />
</ThemeContext.Provider>
</section>
</ThemeContext.Provider>У цьому прикладі Button отримає значення "dark".
useContextХук useContext викликають безпосередньо в компоненті:
function UserMenu() {
const user = useContext(UserContext);
return <p>Вітаємо, {user.name}</p>;
}Компонент не повинен отримувати це саме значення через проміжні props, якщо дані вже доступні в Context.
useContext можна використовувати лише на верхньому рівні компонента:
function Component() {
const theme = useContext(ThemeContext);
if (theme === 'dark') {
return <DarkView />;
}
return <LightView />;
}Не слід викликати його всередині умов, циклів або вкладених функцій.
Context сам по собі не є сховищем стану. Він лише передає значення компонентам. Стан зазвичай зберігається в компоненті, який рендерить Provider, а потім передається через value.
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
function toggleTheme() {
setTheme((currentTheme) =>
currentTheme === 'light' ? 'dark' : 'light'
);
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}Окремий компонент-постачальник допомагає приховати логіку стану й спростити використання Context у застосунку:
function App() {
return (
<ThemeProvider>
<Page />
</ThemeProvider>
);
}Повний приклад із власним ThemeProvider:
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
function toggleTheme() {
setTheme((currentTheme) =>
currentTheme === 'light' ? 'dark' : 'light'
);
}
const value = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('useTheme має використовуватися всередині ThemeProvider');
}
return context;
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<h1>Моя сторінка</h1>
<button type="button" onClick={toggleTheme}>
Перемкнути тему
</button>
</header>
);
}
function Profile() {
const { theme } = useTheme();
return (
<section className={theme}>
<p>Поточна тема: {theme}</p>
</section>
);
}
function App() {
return (
<ThemeProvider>
<Header />
<Profile />
</ThemeProvider>
);
}
export default App;У цьому варіанті:
ThemeProvider відповідає за стан і зміну теми;
компоненти не залежать від внутрішньої реалізації цього стану;
useTheme є зручним власним хуком для доступу до контексту;
помилка виникає одразу, якщо useTheme використано без ThemeProvider.
Коли змінюється значення value у Provider, компоненти, які використовують цей Context, отримують нове значення і повторно рендеряться.
У прикладі з темою:
const [theme, setTheme] = useState('light');
const value = {
theme,
toggleTheme,
};Після виклику setTheme змінюється theme, створюється новий об’єкт value, і споживачі ThemeContext бачать оновлену тему.
Це зручно для даних, які справді потрібні багатьом компонентам:
поточний користувач;
тема інтерфейсу;
мова інтерфейсу;
налаштування, спільні для частини застосунку.
Context не обов’язково оголошувати на найвищому рівні застосунку. Provider можна розмістити лише навколо тієї частини дерева, якій потрібні дані:
function App() {
return (
<>
<Header />
<ThemeProvider>
<Dashboard />
</ThemeProvider>
<Footer />
</>
);
}У цьому прикладі Dashboard має доступ до теми, а Header і Footer — ні.
Це допомагає обмежити область дії даних і не робити кожен Context глобальним для всього застосунку.
Context доречний, коли:
значення потрібно багатьом компонентам;
компоненти розташовані на різних рівнях дерева;
передавання через props створює багато проміжного коду;
дані належать до певної спільної області застосунку.
Якщо значення потрібне лише безпосередньому дочірньому компоненту, краще передати його через props. Це робить залежності компонента явними.
Context не потрібно використовувати для кожного стану. Наприклад, локальний стан відкриття одного меню зазвичай краще залишити в компоненті цього меню.
Якщо компонент не знаходиться всередині потрібного Provider, він отримає значення за замовчуванням:
const ThemeContext = createContext('light');Через це помилка може бути непомітною. Для обов’язкового Context краще використовувати null і перевіряти значення у власному хуку:
function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('Компонент має бути всередині ThemeProvider');
}
return context;
}useContext має отримувати саме той об’єкт Context, який було створено через createContext:
const value = useContext(ThemeContext);Недостатньо імпортувати лише Provider або передати інший об’єкт із подібними даними.
Компонент, який рендерить Provider, не отримує значення цього Provider через useContext у тому самому місці:
function ThemeProvider() {
const theme = useContext(ThemeContext);
return (
<ThemeContext.Provider value={theme}>
{/* ... */}
</ThemeContext.Provider>
);
}Щоб передати значення, його потрібно спочатку отримати з локального стану або іншого джерела, а вже потім використати в value.
Якщо один Context містить багато незалежних значень, зміна будь-якого з них може спричинити повторний рендер усіх його споживачів.
Замість одного великого Context іноді краще створити кілька менших, наприклад окремо для:
даних користувача;
налаштувань інтерфейсу;
локалі.
Розділяти Context потрібно тоді, коли ці дані мають різні області використання або змінюються незалежно.
valueProvider повинен отримати значення через value:
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>Передавання даних як іншого пропса не створює значення Context:
<ThemeContext.Provider theme={theme}>
<Page />
</ThemeContext.Provider>Context передає спільні дані через дерево компонентів без проміжного прокидання props.
createContext створює об’єкт контексту.
Provider надає значення дочірнім компонентам через value.
useContext отримує значення найближчого Provider.
Зміна value оновлює компоненти, які використовують цей Context.
Context має обмежену область дії, визначену розташуванням Provider.
Для обов’язкових значень корисно створювати власний хук із перевіркою на відсутність Provider.
Локальні дані краще залишати в стані компонента, а Context використовувати для справді спільних значень.