useContext: данные для всего приложения
Проблема: props через десять уровней
Данные в React текут сверху вниз через props. Обычно это хорошо: видно, откуда что пришло. Но некоторые данные нужны почти всем компонентам: текущий пользователь, тема оформления, язык, корзина. Передавать их через props приходится через каждый промежуточный компонент — даже через те, которым они не нужны:
function App() {
const [user, setUser] = useState({ name: "Аня" });
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />; // Layout сам user не использует
}
function Header({ user }) {
return <Menu user={user} />; // и Header тоже
}
function Menu({ user }) {
return <p>Привет, {user.name}</p>; // нужен только здесь
}
Это называют prop drilling («сверление» props). Код становится хрупким: чтобы добавить новое значение, нужно менять все промежуточные компоненты.
Контекст позволяет компоненту наверху «объявить» значение, а любому компоненту ниже — прочитать его напрямую, без передачи через промежуточные уровни.
Три шага: создать, передать, прочитать
const { createContext, useContext } = React;
// 1. Создать контекст (обычно в отдельном файле). Аргумент — значение по умолчанию
const UserContext = createContext(null);
// 2. Передать значение: обернуть часть дерева в Provider
function App() {
const [user, setUser] = useState({ name: "Аня" });
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function Layout() {
return <Header />; // никаких props
}
function Header() {
return <Menu />;
}
// 3. Прочитать в любом компоненте внутри Provider
function Menu() {
const user = useContext(UserContext);
return <p>Привет, {user.name}</p>;
}
createContext(значениеПоУмолчанию)создаёт объект контекста.<Ctx.Provider value={...}>делает значение доступным всем компонентам внутри, на любой глубине.useContext(Ctx)возвращает значение ближайшего Provider выше по дереву. Если Provider'а нет — значение по умолчанию.
Когда значение меняется
Когда меняется value у Provider'а, React перерисовывает все компоненты, которые читают этот
контекст через useContext, — даже если их родители не перерисовывались. Так контекст остаётся
актуальным во всём приложении.
Provider'ы можно вкладывать: внутренний «перекрывает» внешний для своей части дерева.
<ThemeContext.Provider value="dark">
<Page /> {/* тут тема dark */}
<ThemeContext.Provider value="light">
<Footer /> {/* а тут light */}
</ThemeContext.Provider>
</ThemeContext.Provider>
Контекст + состояние
Сам по себе контекст ничего не хранит — он только передаёт значение вниз. Чтобы данные можно было менять, контекст соединяют с состоянием и передают вниз и значение, и функции для изменения:
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name) => setUser({ name });
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function LoginButton() {
const { user, login, logout } = useContext(AuthContext);
return user
? <button onClick={logout}>Выйти ({user.name})</button>
: <button onClick={() => login("Аня")}>Войти</button>;
}
function App() {
return (
<AuthProvider>
<Header />
<LoginButton />
</AuthProvider>
);
}
Это стандартная схема: компонент-«провайдер» с состоянием внутри и children снаружи. Через
children всё приложение оказывается внутри Provider'а.
Свой хук для контекста
В проектах почти всегда делают маленький хук-обёртку. Он короче, и он сразу сообщает об ошибке, если компонент случайно оказался вне провайдера:
function useAuth() {
const value = useContext(AuthContext);
if (value === null) {
throw new Error("useAuth must be used inside AuthProvider");
}
return value;
}
function Profile() {
const { user } = useAuth(); // вместо useContext(AuthContext)
return <p>{user ? user.name : "Гость"}</p>;
}
Хук — это обычная функция, имя которой начинается с use и внутри которой вызываются другие хуки.
Подробнее о своих хуках — в теме про загрузку данных.
Когда контекст не нужен
Контекст удобен, но не стоит класть в него всё подряд:
- если данные нужны одному-двум уровням ниже — передай props, это проще и нагляднее;
- часто помогает композиция: вместо передачи данных через
Layoutпередай готовый элемент черезchildren—<Layout><Menu user={user} /></Layout>; - каждое изменение
valueперерисовывает всех читателей. Для часто меняющихся данных (позиция мыши, текст в поле) контекст — плохой выбор.
Хорошие кандидаты для контекста: текущий пользователь, тема, язык, корзина, настройки.
Частые ошибки
- Компонент вне Provider.
useContextмолча вернёт значение по умолчанию (null), и где-то дальше будетCannot read properties of null. Хук с проверкой выдаст понятную ошибку сразу. - Передать в
useContextне тот объект. НужноuseContext(ThemeContext), а неuseContext(ThemeContext.Provider)или строку. - Забыть
value.<Ctx.Provider>безvalueпередастundefined. - Ожидать, что изменение объекта внутри
valueобновит экран. Как и с состоянием: нужен новый объект черезset….
Итог
- Контекст передаёт данные вглубь дерева без props:
createContext→<Ctx.Provider value>→useContext(Ctx). useContextберёт значение ближайшего Provider выше, иначе — значение по умолчанию.- При изменении
valueперерисовываются все читатели контекста. - Контекст +
useStateв компоненте-провайдере = общее изменяемое состояние приложения. - Свой хук
useX()с проверкой наnull— удобно и безопасно. - Для данных на 1–2 уровня — обычные props.