When it comes to choosing a state management library for your React application, the options seem endless. A few years ago, Redux was practically synonymous with React, but today, the landscape has expanded significantly. There are Recoil, Zustand, XState, MobX, Redux Toolkit, and many more, with new ones popping up regularly. And all of them are great. The question then arises: Which one should you use? Most likely - none at all. Let's delve into why.
The majority of modern web applications synchronize their state with the backend seamlessly. Updates happen in real-time, and there's no need to hit a "Save" button. For instance, this very article I'm composing in a web editor is automatically saved after a few seconds of inactivity. What remains are small fragments of purely UI-related state, such as determining whether a drawer is open or if text should be expanded. And you don't need a whole state management library to deal with these UI states. React provides a robust set of tools for managing local state.
If you do encounter local state that is not synchronized with the backend and exhibits a complex structure, that's when you should consider utilizing a local state management library like Zustand or Recoil (or any other you like, all of them are actually great). However, this scenario is not applicable to the majority of modern web applications.
Now, let's explore the state management tools available in React and when to use them.
- useState. This is the most fundamental hook for preserving local state between renders. It's an excellent choice for handling primitive data types. Updating arrays and nested objects may be a bit harder.
- useReducer. An advanced counterpart to useState, useReducer requires you to provide a custom reducer function that understands how to create new state based on the old state + some additional data you'll provide. It shines when dealing with complex data structures. You can explicitly define which actions can be performed with your state (like "add new item at the beginning of an array" or "rollback to the initial state") and limit state changes only to these predefined actions. Additionally, useReducer provides a dispatch function which helps to avoid props drilling. IMO this is one of the most underrated React hooks.
- useRef. Although often referred to as a "hook to get imperative access to an element," useRef in fact is a mutable container for any type of data. The key difference compared to useState or useReducer is that mutating a ref container value does not trigger a component rerender (which means that what you see on the screen is potentially not the latest value and you need to handle such cases manually). It is typically employed as a performance optimization, useful when there are frequent state updates, and you wish to avoid rerendering on each update (for example with the onScroll event).
- React.Context + useContext. This powerful combination allows you to share state across all components within a subtree. It's effective at eliminating the need for prop drilling. You can put anything in a context: callback function, [value, setValue] pair from useState, JSX element (why not?). Combining context with useReducer can works as a simplified version of Redux, although there are some significant performance differences to consider. It may not perform optimally when dealing with frequently updated state, as it can trigger a full subtree rerender. Typical use-cases: store current user, permissions, settings, configuration etc.
- React Query (TanStack Query), SWR, RTK Query. For managing "remote" state (synced with the backend) we have these wonderful libraries like React Query. Don't even think about fetching your data in useEffect and storing it with useState, there are too many cases you'll forget to handle.
- URL + routing. The last but not least. All "significant" app state (by "significant" I mean "the state that affects the displayed data") should be stored in the URL. All components have access to it. You can easily share URL with almost no extra effort. The only drawback is that URL has a max characters limit and you won't be able to store a lot of data there (in this case you can think of some URL shortener service or similar solutions).
In conclusion, the key to effective state management in React is choosing the right tool for the job, keeping in mind the nature of the state you're handling, and avoiding unnecessary complexity when it's not warranted.
Коли мова йде про вибір бібліотеки для управління станом React застосунку, варіанти здаються безмежними. Кілька років тому Redux практично був синонімом React і його використовували в кожному першому проекті. Тепер з'явились Recoil, Zustand, XState, MobX, Redux Toolkit і мабуть ще з десяток бібліотек про які я і не чув. І всі вони добре виконують своє завдання. Виникає питання: що саме використати в наступному проекті? Скоріше за все, вам не знадобиться жодна з цих бібліотек. Давайте розберемося, чому.
Більшість сучасних веб-застосунків безперервно синхронізують свій стан із бекендом. Оновлення відбуваються в реальному часі, і немає потреби натискати кнопку "Save". Наприклад, цей самий текст, який я створюю у веб-редакторі, автоматично зберігається після декількох секунд бездіяльності. Залишаються невеликі фрагменти стану, які пов'язані лише з UI. Наприклад, визначення того, чи відкритий drawer, чи розгорнутий текст. І для роботи з цими станами вам не потрібна окрема бібліотека з купою крутих функцій. React і без сторонніх бібліотек має дуже потужний набір інструментів для менеджменту локального стану.
Якщо ви все ж зіткнетесь із локальним станом, який не синхронізується із бекендом і має складну структуру, тоді варто розглянути використання бібліотеки типу Zustand або Recoil (або будь-якої іншої, всі вони насправді чудові). Проте це доволі рідкісний сценарій для сучасних веб-застосунків.
Давайте розглянемо інструменти управління станом, доступні в React, і коли їх варто використовувати.
- useState. Це найбільш базовий хук для збереження локального стану між рендерами. Він чудово підходить для роботи із примітивними типами даних. Оновлення масивів і вкладених об'єктів можуть бути складнішими.
- useReducer. Просунутий аналог useState. useReducer вимагає надати функцію reducer, що розуміє, як створювати новий стан на основі старого стану. Він сяє при роботі із складними структурами даних. Ви можете явно визначити, які дії можна виконувати із вашим станом (наприклад, "додати новий елемент до початку масиву" або "повернутися до початкового стану"). І всі зміни локального стану будуть обмежені тільки операціями, які ви продумаєте заздалегідь. Крім того, useReducer надає функцію dispatch, яка допомагає уникнути проблеми props drilling. На мою думку, це один з самих недооцінених React хуків.
- useRef. Незважаючи на те, що його часто називають "хуком для отримання імперативного доступу до елемента", useRef фактично є контейнером для будь-якого типу даних. Основна відмінність порівняно з useState/useReducer полягає в тому, що зміна значення в ref контейнері не спричинює ререндер компоненту (це означає, що на екрані ви можете бачити потенційно застаріле значення, і вам потрібно вручну обробляти такі ситуації). Зазвичай його використовують як оптимізацію, коли присутні часті оновлення стану і ви хочете уникнути ререндерингу при кожному оновленні (наприклад, із подією onScroll).
- React.Context + useContext. Ця потужна комбінація дозволяє вам поширювати стан між всіма компонентами всередині піддерева. Вона ефективно усуває необхідність у передачі пропсів на декілька рівнів дерева компонентів. Ви можете помістити будь-що в контекст: колбек функцію, пару [value, setValue] з useState, JSX елемент (чому б і ні?). Комбінування контексту з useReducer може служити спрощеною версією Redux, хоча це не одне є те саме. Контекст може працювати не оптимально при роботі із часто оновлюваним станом, оскільки це спричинює ререндеринг всього піддерева. Типові варіанти використання: зберігання поточного користувача, налаштувань, конфігурації і т. д.
- React Query (TanStack Query), SWR, RTK Query. Для управління "віддаленим" станом (синхронізованим із бекендом) ми маємо чудові бібліотеки типу React Query. Навіть не думайте витягувати дані у useEffect та зберігати їх в useState, оскільки є дуже багато нюансів, які ви забудете врахувати (або код для тривіальної операції виросте до 100 рядків коду).
- URL + маршрутизація. Останнє, але не менш важливе. Весь "значущий" стан додатку (під "значущим" маю на увазі "стан, що впливає на відображені дані") слід зберігати у URL. У всіх компонентів є до нього доступ. Ви можете легко ділитися URL майже без жодних додаткових зусиль. Єдиний недолік - URL має обмеження на максимальну кількість символів, і ви не зможете зберігати там багато даних (в такому випадку можна подумати про якийсь сервіс скорочення URL або подібні рішення).
На завершення слід сказати: 1) думайте, який інструмент найбільше підходить для вирішення конкретної проблеми; 2) уникайте зайвої складності якщо це можливо.
Якщо вам сподобалась стаття і ви хочете дізнатись більше про тонкощі розробки на React, завітайте на мій Udemy курс українською мовою:
Founder & CEO at Algoritech | Custom Software Solutions Expert | Ex-Microsoft
1yAwsome stuff