Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Redux проти context API». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Redux vs Context API**: Context API передає стан через дерево провайдерів без додаткових залежностей; Redux керує станом через екшени та редюсери в окремому сторі. ```jsx // Context: всі споживачі перерендеряться при будь-якій зміні значення провайдера const theme = useContext(AppContext); // Redux: тільки підписані компоненти перерендеряться const theme = useSelector(state => state.theme); ``` **Ключове правило:** простий спільний стан (тема, авторизація) для кількох компонентів? Context. Async-логіка, часті оновлення, або командний проект? Redux з RTK.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Redux vs Context API**: Redux - це централізоване сховище стану, яке управляє даними через екшени та редюсери в окремому сторі; Context API - вбудований спосіб React передавати дані вниз по дереву компонентів без prop drilling. ## Теорія ### TL;DR - Context API - як сімейний груповий чат: просто, всі бачать усе, легко налаштувати. Redux - як корпоративний Slack з каналами та логами: структурований, для команд. - Головна різниця: Context перерендерить кожного споживача при зміні значення провайдера; Redux перерендерить тільки компоненти, підписані через `useSelector` на конкретний шматок стану. - До 5-10 компонентів із простим станом? Context. Складна async-логіка, 10+ компонентів, або командний дебагінг? Redux. - Redux додає boilerplate, але дає time-travel дебагінг через Redux DevTools; Context не дає нічого з коробки. - Redux Toolkit (RTK) суттєво скорочує boilerplate порівняно з класичним Redux. ### Швидкий приклад Ключова різниця - в тому, що тригерить перерендер: ```jsx // Context API - ВСІХ споживачів перерендерить при БУДЬ-ЯКІЙ зміні const AppContext = React.createContext(); function App() { const [theme, setTheme] = useState('light'); const [user, setUser] = useState(null); return ( // Один спільний об'єкт - будь-яка зміна тригерить усіх споживачів <AppContext.Provider value={{ theme, user }}> <ThemeConsumer /> {/* Перерендер при зміні theme АБО user */} <UserConsumer /> {/* Перерендер при зміні theme АБО user */} </AppContext.Provider> ); } // Redux - перерендерить тільки підписаний компонент function ThemeConsumer() { const theme = useSelector(state => state.theme); // Тільки при зміні theme return <div>{theme}</div>; } ``` Коли `user` змінюється в прикладі з Context, `ThemeConsumer` теж перерендериться, хоча `user` йому не потрібен. `useSelector` у Redux пропускає цей перерендер повністю. ### Ключова різниця Redux нав'язує однонаправлений потік даних: ти диспатчиш екшен, чистий редюсер обчислює наступний стан, і тільки компоненти, підписані на відповідний шматок стану, перерендеряться. Context просто передає значення вниз по дереву. У ньому немає поняття екшенів, редюсерів чи вибіркових підписок. Будь-яка зміна об'єкта провайдера тригерить перерендер у кожного споживача нижче. Це не баг, а дизайн, але він стає проблемою коли додаток росте. ### Коли що використовувати - Проста зміна теми або статус авторизації в 1-5 компонентах: Context API. - Стан форми між сусідніми компонентами без async-логіки: Context API. - Загальний стан із API-запитами, флагами завантаження та обробкою помилок: Redux з `createAsyncThunk`. - 10+ компонентів зі станом, що часто оновлюється: Redux + Reselect. - Командний проект із потребою в дебагінгу та код-рев'ю: Redux (лог екшенів коштує свого). - Списки з великою кількістю елементів і частими оновленнями: Redux + Reselect. ### Таблиця порівняння | Функція | Context API | Redux (RTK) | |---|---|---| | **Налаштування** | `createContext()` + Provider, без залежностей | `@reduxjs/toolkit` + `configureStore()` | | **Boilerplate** | Мінімальний | Середній (слайси, екшени) | | **Перерендери** | Всі споживачі при зміні значення | Тільки відповідні `useSelector` виклики | | **DevTools** | Немає | Time-travel, лог екшенів (Redux DevTools) | | **Async** | Вручну (`useEffect` + локальний стан) | `createAsyncThunk` з коробки | | **Масштабованість** | Малі та середні застосунки | Великі застосунки, команди | | **Коли використовувати** | Теми, токен авторизації, локаль | Кошик, дашборд із API-синхронізацією | ### Як це працює всередині React Context використовує fiber-дерево: Provider записує значення у `context._currentValue`, а кожен споживач іде вгору по fiber-стеку до найближчого Provider. Будь-яка зміна об'єкта провайдера не проходить перевірку на поверхневу рівність (shallow equality) і тригерить `scheduleUpdateOnFiber` для всіх споживачів нижче. Redux зберігає стан у звичайному JS-об'єкті (в RTK загорнутому у Immer для безпечних мутацій). Коли ти диспатчиш екшен, стор запускає редюсери, потім сповіщає підписані компоненти. `useSelector` порівнює попереднє і нове вибране значення на строгу рівність - якщо однакове, компонент не перерендериться. У React 18 Redux групує оновлення через `unstable_batchedUpdates`, тому кілька диспатчів в одному обробнику подій не викликають кілька рендер-циклів. ### Типові помилки **Весь стан в одному об'єкті Context:** ```jsx // Погано - новий об'єкт при кожному рендері <Context.Provider value={{ theme, user, cart }}> // Виправлення - мемоізувати значення const value = useMemo(() => ({ theme, user }), [theme, user]); <Context.Provider value={value}> ``` Новий об'єктний літерал створюється при кожному рендері, тому shallow equality завжди не виконується і всі споживачі перерендеряться, навіть якщо нічого важливого не змінилось. **Context для стану, що часто оновлюється:** ```jsx // Погано - введення тексту зі швидкістю 60fps перерендерить все піддерево const [searchQuery, setSearchQuery] = useState(''); <SearchContext.Provider value={{ searchQuery, setSearchQuery }}> ``` Тримай стан, що часто оновлюється, локально. У Context передавай тільки фінальне значення. **Занадто широкий селектор у Redux:** ```jsx // Погано - перерендер при будь-якій зміні стору const todos = useSelector(state => state.todos); // Виправлення - вибирай тільки потрібний шматок const todoList = useSelector(state => state.todos.list); ``` **Пряма мутація стану в редюсері без RTK:** ```jsx // Погано - ламає Redux DevTools і передбачуваність reducer: (state, action) => { state.arr.push(action.payload); // Пряма мутація return state; } // Виправлення з RTK - Immer робить це безпечним reducers: { addItem: (state, action) => { state.arr.push(action.payload); // RTK/Immer перетворить на незмінне оновлення } } ``` У класичному Redux без RTK завжди повертай новий об'єкт. З RTK Immer-draft дозволяє писати код у стилі мутації, який під капотом виробляє незмінний результат. ### Де використовується на практиці - React docs: `ThemeContext` для передачі теми вниз по дереву компонентів. - Next.js застосунки: auth context для сесії та даних користувача. - Shopify Polaris: Redux для централізованого стану мерчанта в адмін-панелі. - VS Code розширення: Redux для стану воркспейсу між панелями редактора. - RTK Query у великих застосунках: кешування даних із автоматичною інвалідацією кешу. Я бачив таке в реальних проектах: починали з Context для авторизації, потім додали для кошика, потім для сповіщень. Коли в дереві з'явилося 4 вкладені контексти, дебагінг перетворився на гадання. Після міграції на Redux лог екшенів сам по собі виправдав усі зусилля. ### Питання на співбесіді **Q:** Як Context викликає зайві перерендери і як це виправити? **A:** Будь-яка зміна об'єкта провайдера тригерить перерендер у всіх споживачів, бо об'єктні літерали не проходять shallow equality при кожному рендері. Виправлення: `useMemo` для значення провайдера, розподіл на окремі контексти за частотою оновлення, або перехід на Zustand для цього шматка стану. **Q:** Поясни потік Redux: екшен, thunk, редюсер, компонент. **A:** Ти диспатчиш екшен (звичайний об'єкт з полем `type`). Якщо асинхронно, thunk middleware перехоплює його, виконує async-роботу, потім диспатчить звичайний екшен. Редюсер отримує екшен і повертає новий стан. Компоненти з `useSelector` перевіряють, чи змінився їхній шматок стану, і якщо так - перерендеряться. **Q:** Чому Redux, а не Zustand або Jotai для великого застосунку? **A:** Redux DevTools з time-travel дебагінгом, зріла екосистема middleware (RTK Query, redux-saga), і усталені патерни для командної роботи. Zustand легший і добре підходить для середніх застосунків, але не дає такої глибини DevTools і гарантій передбачуваності при масштабуванні. **Q:** Порівняй продуктивність Context і Redux для 1000 елементів. **A:** Redux з Reselect нормалізує вибірки приблизно до O(1) через мемоізовані селектори. Context тригерить O(n) обхід fiber-дерева при кожній зміні значення. Для великих списків із частими оновленнями різниця стає помітною під час профілювання. **Q:** Як у React 18 concurrent mode батчінг Redux взаємодіє з `startTransition`? **A:** Redux використовує `unstable_batchedUpdates` для групування диспатчів. У concurrent mode `startTransition` позначає оновлення як не-термінові, тому UI залишається чуйним під час важких обчислень у селекторах. Для async thunk-ів, що мають показувати UI-фідбек (спінер завантаження), використовуй `useTransition` поряд із диспатчем. ## Приклади ### Перемикач теми через Context API ```jsx const ThemeContext = React.createContext(); function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // Мемоізуємо щоб уникнути зайвих перерендерів const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } function ThemeButton() { const { theme, setTheme } = useContext(ThemeContext); return ( <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}> Поточна тема: {theme} </button> ); } ``` Тема змінюється рідко, споживачів мало, async-логіки немає. Це класичний випадок для Context. ### Todos з API через Redux Toolkit ```jsx import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; export const fetchTodos = createAsyncThunk('todos/fetch', async () => { const res = await fetch('/api/todos'); return res.json(); }); const todosSlice = createSlice({ name: 'todos', initialState: { list: [], filter: 'all', loading: false }, reducers: { setFilter: (state, action) => { state.filter = action.payload; // Immer подбає про незмінність } }, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.loading = true; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.list = action.payload; state.loading = false; }); } }); // Компонент підписується тільки на потрібне function TodoList() { const list = useSelector(state => state.todos.list); // Ігнорує зміни filter const dispatch = useDispatch(); useEffect(() => { dispatch(fetchTodos()); }, [dispatch]); return <ul>{list.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>; } ``` `TodoList` перерендериться тільки при зміні `todos.list`. Зміна фільтра не зачіпає його. З Context обидва компоненти ділили б один об'єкт значення і обидва перерендерялись би при кожному оновленні стану. ### Edge case: посилання на об'єкти та мемоізація ```jsx // Context - новий об'єкт при кожному рендері ламає React.memo function BadProvider({ children }) { const [count, setCount] = useState(0); return ( // Новий об'єкт кожен рендер - React.memo на Child не допоможе <Context.Provider value={{ count, increment: () => setCount(c => c + 1) }}> <Child /> </Context.Provider> ); } // Redux - Immer створює нове посилання тільки якщо дані реально змінились const counterSlice = createSlice({ name: 'counter', initialState: { count: 0 }, reducers: { increment: state => { state.count += 1; } // Тільки count отримує нове посилання } }); // MemoizedChild пропускає перерендер при оновленні інших слайсів const MemoizedChild = React.memo(function Child() { const count = useSelector(state => state.counter.count); return <div>{count}</div>; }); ``` Immer створює нове посилання на об'єкт тільки коли дані справді змінились. У парі з вузьким селектором `React.memo` працює так, як ти очікуєш.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.