Пошук уроків, статей та іншого контенту
Застосуєте useContext для доступу до спільних даних у дереві компонентів без передачі пропсів через кожен рівень.
useContextУ React дані зазвичай передаються від батьківського компонента до дочірнього через props:
function App() {
const user = { name: 'Олена' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Page user={user} />;
}
function Page({ user }) {
return <Profile user={user} />;
}
function Profile({ user }) {
return <p>Вітаємо, {user.name}!</p>;
}У цьому прикладі Layout і Page не використовують user, але все одно передають його далі. Така ситуація називається prop drilling — передавання пропсів через кілька рівнів компонентів до компонента, якому ці дані справді потрібні.
useContext дає змогу:
створити спільний контекст;
обгорнути частину дерева компонентів у Provider;
отримати значення контексту в будь-якому дочірньому компоненті без проміжних пропсів.
Контекст створюють за допомогою createContext:
import { createContext } from 'react';
const UserContext = createContext(null);UserContext містить два основні компоненти й методи:
UserContext.Provider — передає значення дочірнім компонентам;
useContext(UserContext) — читає це значення.
Провайдеру потрібно передати значення через проп value:
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>Тепер компонент Profile може отримати user напряму:
import { useContext } from 'react';
function Profile() {
const user = useContext(UserContext);
return <p>Вітаємо, {user.name}!</p>;
}Компонент Profile має бути нащадком UserContext.Provider у дереві React.
Нижче наведено застосунок, у якому тема інтерфейсу зберігається в контексті. Компонент Toolbar не отримує тему через пропси, але його дочірня кнопка має до неї доступ.
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
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>
<h1>Налаштування профілю</h1>
<Toolbar />
</main>
);
}
function Toolbar() {
return (
<section>
<p>Панель інструментів</p>
<ThemeButton />
</section>
);
}
function ThemeButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button type="button" onClick={toggleTheme}>
Поточна тема: {theme}. Змінити тему
</button>
);
}
export default App;У цьому прикладі:
ThemeContext створює контекст.
App зберігає стан теми.
ThemeContext.Provider робить theme і toggleTheme доступними в піддереві.
Page і Toolbar не передають ці дані через пропси.
ThemeButton отримує значення через useContext.
Після виклику setTheme компонент, що використовує контекст, повторно рендериться з новим значенням.
Під час створення контексту можна вказати значення за замовчуванням:
const LanguageContext = createContext('uk');Якщо компонент викликає useContext(LanguageContext) поза межами провайдера, він отримає 'uk'.
Однак значення за замовчуванням не замінює Provider. Воно використовується лише тоді, коли в дереві немає відповідного провайдера.
const LanguageContext = createContext('uk');
function LanguageLabel() {
const language = useContext(LanguageContext);
return <p>Мова: {language}</p>;
}У цьому випадку компонент покаже uk, якщо не буде обгорнутий у LanguageContext.Provider.
Для складніших значень часто зручно використовувати null:
const AuthContext = createContext(null);Тоді відсутність провайдера можна виявити явно:
function UserInfo() {
const auth = useContext(AuthContext);
if (auth === null) {
return <p>Контекст авторизації недоступний.</p>;
}
return <p>Користувач: {auth.user.name}</p>;
}Контекст сам по собі не зберігає стан. Він лише передає значення компонентам.
Стан зазвичай створюють у батьківському компоненті, а потім передають разом із функціями для його зміни:
const CartContext = createContext(null);
function App() {
const [items, setItems] = useState([]);
function addItem(item) {
setItems((currentItems) => [...currentItems, item]);
}
function removeItem(itemId) {
setItems((currentItems) =>
currentItems.filter((item) => item.id !== itemId)
);
}
return (
<CartContext.Provider value={{ items, addItem, removeItem }}>
<ProductList />
<Cart />
</CartContext.Provider>
);
}Будь-який дочірній компонент може отримати і дані, і функції:
function Cart() {
const { items, removeItem } = useContext(CartContext);
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<button type="button" onClick={() => removeItem(item.id)}>
Видалити
</button>
</li>
))}
</ul>
);
}Така схема корисна для даних, які використовуються багатьма компонентами:
поточний користувач;
тема інтерфейсу;
мова застосунку;
стан кошика;
налаштування, спільні для певного піддерева.
Контекст доступний лише в піддереві конкретного провайдера:
function App() {
return (
<>
<ThemeContext.Provider value="dark">
<Header />
</ThemeContext.Provider>
<Footer />
</>
);
}Header і його дочірні компоненти отримають значення 'dark'. Footer не є нащадком провайдера, тому не отримає це значення.
Можна мати кілька провайдерів одного контексту:
function App() {
return (
<>
<ThemeContext.Provider value="light">
<LightSection />
</ThemeContext.Provider>
<ThemeContext.Provider value="dark">
<DarkSection />
</ThemeContext.Provider>
</>
);
}Компонент використовує значення найближчого провайдера в дереві.
Якщо багато компонентів напряму викликають useContext, можна створити власний хук. Це приховує деталі реалізації контексту й дає змогу перевірити, чи є провайдер.
import { createContext, useContext, useState } from 'react';
const CounterContext = createContext(null);
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
function increment() {
setCount((currentCount) => currentCount + 1);
}
const value = {
count,
increment,
};
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
);
}
function useCounter() {
const context = useContext(CounterContext);
if (context === null) {
throw new Error('useCounter має використовуватися всередині CounterProvider');
}
return context;
}
function Counter() {
const { count, increment } = useCounter();
return (
<button type="button" onClick={increment}>
Лічильник: {count}
</button>
);
}
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
export default App;Тепер компоненти використовують useCounter(), а не знають про назву й структуру CounterContext.
Компонент-провайдер зазвичай містить:
стан;
функції для зміни стану;
об’єкт value;
Context.Provider;
проп children.
Коли значення value провайдера змінюється, React повторно рендерить компоненти, які використовують цей контекст.
У цьому прикладі при кожному рендері створюється новий об’єкт:
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>Навіть якщо самі дані не змінилися, посилання на об’єкт є новим. Для простих компонентів це зазвичай не проблема. Якщо ж контекст великий і його споживачів багато, варто:
не додавати до контексту непотрібні дані;
розділяти незалежні дані на кілька контекстів;
не використовувати контекст там, де достатньо звичайних пропсів.
Контекст найкраще підходить для даних, які справді потрібні багатьом компонентам одного піддерева.
function App() {
return <Profile />;
}
function Profile() {
const user = useContext(UserContext);
return <p>{user.name}</p>;
}Якщо UserContext має значення null, спроба прочитати user.name спричинить помилку.
Потрібно розмістити провайдер вище:
function App() {
const user = { name: 'Олена' };
return (
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
);
}У цьому прикладі useContext не прочитає значення щойно створеного провайдера:
function App() {
const value = useContext(ThemeContext);
return (
<ThemeContext.Provider value="dark">
<p>{value}</p>
</ThemeContext.Provider>
);
}useContext шукає провайдер серед батьків компонента, а не серед його дочірніх елементів. Потрібно винести споживача в окремий компонент:
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemeLabel />
</ThemeContext.Provider>
);
}
function ThemeLabel() {
const theme = useContext(ThemeContext);
return <p>Тема: {theme}</p>;
}Не варто складати в один контекст усі дані застосунку без потреби:
<AppContext.Provider
value={{
user,
theme,
cart,
notifications,
settings,
}}
>
{children}
</AppContext.Provider>Якщо ці дані не пов’язані між собою, краще створити окремі контексти. Так компоненти залежатимуть лише від потрібної частини стану.
Контекст не потрібно застосовувати для кожного значення. Якщо дані передаються лише на один-два рівні, звичайні пропси часто зрозуміліші:
function Profile({ user }) {
return <p>{user.name}</p>;
}Контекст виправданий тоді, коли передавання пропсів стає незручним або одні й ті самі дані потрібні багатьом компонентам у піддереві.
createContext створює контекст для спільного значення.
Provider передає значення компонентам у своєму піддереві.
useContext читає найближче значення відповідного провайдера.
Через контекст можна передавати як дані, так і функції для зміни стану.
Контекст доступний лише нащадкам провайдера.
Стан не створюється контекстом автоматично — його потрібно зберігати, наприклад, через useState.
Для зручності можна створити власний хук на основі useContext.
Не слід використовувати контекст там, де звичайних пропсів достатньо.