Будешь ли использовать React Context если компоненты не ререндерятся
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
React Context и ненужные ререндеринги
Ответ: Нет, не буду использовать Context, если компоненты не должны ререндеривать. Context автоматически триггерит ререндеринг подписанных компонентов при изменении значения, что может привести к проблемам производительности. Есть альтернативные решения.
Суть проблемы
React Context по умолчанию вызывает ререндеринг всех компонентов, потребляющих контекст, даже если им нужно только прочитать значение:
const AppContext = React.createContext();
function Provider({ children }) {
const [state, setState] = useState(0);
return (
<AppContext.Provider value={{ state, setState }}>
{children}
</AppContext.Provider>
);
}
function Consumer() {
const { state, setState } = useContext(AppContext);
// Компонент ререндеривается каждый раз, когда state меняется
console.log("Consumer ререндерен");
return <div>{state}</div>;
}
// Проблема: Consumer ререндеривается, хотя ему достаточно только читать значение
Когда Context вызывает ненужные ререндеринги
Сценарий 1: Разделение логики и представления
// ❌ Плохой паттерн - все компоненты ререндеривают друг из-за друга
const AppContext = React.createContext();
function App() {
const [userId, setUserId] = useState(null);
const [notifications, setNotifications] = useState([]);
const [theme, setTheme] = useState("light");
return (
<AppContext.Provider value={{
userId, setUserId,
notifications, setNotifications,
theme, setTheme
}}>
<UserComponent /> {/* Ререндер при изменении notifications */}
<NotifyComponent /> {/* Ререндер при изменении userId */}
<ThemeComponent /> {/* Ререндер при изменении всего */}
</AppContext.Provider>
);
}
Сценарий 2: Частые обновления
function App() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setMousePosition({ x: e.clientX, y: e.clientY });
// Обновляется 60+ раз в секунду!
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
return (
<PositionContext.Provider value={mousePosition}>
<Component /> {/* Ререндер 60+ раз в секунду! */}
</PositionContext.Provider>
);
}
Решение 1: Стабилизация значения Context
Используйте useMemo чтобы объект value не менялся, если его содержимое не изменилось:
function Provider({ children }) {
const [state, setState] = useState(0);
const [other, setOther] = useState("data");
// ✅ Значение меняется только когда state меняется
const value = useMemo(() => ({
state,
setState
}), [state]);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
Это не предотвращает ререндеринг, но предотвращает ненужные обновления других частей.
Решение 2: Разделение Context на несколько
Используйте несколько контекстов для разных данных:
// Отдельные контексты для отдельных данных
const UserContext = React.createContext();
const NotificationContext = React.createContext();
const ThemeContext = React.createContext();
function App() {
const [userId, setUserId] = useState(null);
const [notifications, setNotifications] = useState([]);
const [theme, setTheme] = useState("light");
const userValue = useMemo(() => ({ userId, setUserId }), [userId]);
const notifValue = useMemo(() => ({
notifications,
setNotifications
}), [notifications]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<UserContext.Provider value={userValue}>
<NotificationContext.Provider value={notifValue}>
<ThemeContext.Provider value={themeValue}>
<UserComponent /> {/* Ререндер только при изменении userId */}
<NotifyComponent /> {/* Ререндер только при изменении notifications */}
<ThemeComponent /> {/* Ререндер только при изменении theme */}
</ThemeContext.Provider>
</NotificationContext.Provider>
</UserContext.Provider>
);
}
Решение 3: Паттерн разделения State и Dispatch
// Отдельный контекст для данных (редко меняется)
const StateContext = React.createContext();
// Отдельный контекст для функций (не меняется)
const DispatchContext = React.createContext();
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
function UseState() {
return useContext(StateContext);
}
function UseDispatch() {
return useContext(DispatchContext);
}
// Компонент, который только читает данные
function Reader() {
const state = UseState();
return <div>{state.value}</div>;
}
// Компонент, который только изменяет данные (не ререндеривается)
function Writer() {
const dispatch = UseDispatch(); // Это не меняется!
return (
<button onClick={() => dispatch({ type: "INCREMENT" })}>
Изменить
</button>
);
}
Решение 4: Замена Context на другое решение
Если компоненты не должны ререндеривать, рассмотрите:
Вариант 1: Zustand (рекомендуется)
import { create } from zustand;
const useAppStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Component() {
// Подписывается только на count
const count = useAppStore((state) => state.count);
return <div>{count}</div>;
}
function Button() {
// Подписывается только на increment
const increment = useAppStore((state) => state.increment);
// НЕ ререндеривается при изменении count!
return <button onClick={increment}>+</button>;
}
Вариант 2: Jotai (атомарное состояние)
import { atom, useAtom } from jotai;
const countAtom = atom(0);
function Component() {
const [count, setCount] = useAtom(countAtom);
return <div>{count}</div>;
}
function Button() {
const [, setCount] = useAtom(countAtom);
// Компонент мемоизирует функции
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
Вариант 3: Recoil
import { atom, useRecoilState, useSetRecoilState } from recoil;
const countAtom = atom({ key: count, default: 0 });
function Reader() {
const [count] = useRecoilState(countAtom);
return <div>{count}</div>;
}
function Writer() {
const setCount = useSetRecoilState(countAtom);
// НЕ ререндеривается при изменении count
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
Практический пример: Неправильное использование Context
// ❌ ПЛОХО - Частые обновления вызывают ненужные ререндеринги
function AnimationProvider({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const animate = () => {
setPosition(prev => ({
x: prev.x + 1,
y: prev.y + 1
}));
};
const animationId = setInterval(animate, 16);
return () => clearInterval(animationId);
}, []);
return (
<AnimationContext.Provider value={position}>
{children}
</AnimationContext.Provider>
);
}
Практический пример: Правильное использование
// ✅ ХОРОШО - Zustand для частых обновлений
const useAnimation = create((set) => ({
position: { x: 0, y: 0 },
setPosition: (pos) => set({ position: pos }),
}));
function AnimationProvider({ children }) {
const setPosition = useAnimation((state) => state.setPosition);
useEffect(() => {
const animate = () => {
setPosition(prev => ({
x: prev.x + 1,
y: prev.y + 1
}));
};
const animationId = setInterval(animate, 16);
return () => clearInterval(animationId);
}, []);
return children;
}
function Animated() {
const position = useAnimation((state) => state.position);
// Компонент обновляется только по необходимости
return <div style={position}>Анимировано</div>;
}
Таблица: Когда что использовать
| Сценарий | Решение | Причина |
|---|---|---|
| Редкие обновления (тема, язык) | Context | Просто, встроено в React |
| Частые обновления (позиция, анимация) | Zustand/Jotai | Селективные подписки |
| Много связанного состояния | Zustand | Легко структурировать |
| Асинхронное состояние | Redux Toolkit | Встроенная поддержка |
| Простое локальное состояние | useState | KISS принцип |
Итоговые рекомендации
- Для простого глобального состояния: Context
- Для часто меняющегося состояния: Zustand или Jotai
- Если часть компонентов не должна ререндеривать: Разделите контексты или используйте state management
- Для производительности: Всегда мемоизируйте значение контекста
- Сомневаетесь? Используйте Zustand — это хороший компромисс между простотой Context и производительностью Redux
Главный вывод: Context не идеален для всех сценариев. Если компоненты не должны ререндеривать при изменении данных, лучше использовать специализированные библиотеки state management, которые поддерживают селективные подписки.