Состояние — это одновременно самый мощный и самый хрупкий элемент в React-приложениях. Когда компоненты начинают неконтролируемо ререндериться, производительность падает, а ошибки синхронизации данных превращаются в кошмар поддержки. Рассмотрим три ключевых аспекта: выбор структуры хранилища, оптимизацию обновлений и борьбу с избыточными ререндерами.
Почему Context API ≠ Redux
Разработчики часто совершают ошибку, пытаясь заменить Redux Context API, не понимая их принципиальных различий. Посмотрите на этот типичный пример:
const AppContext = createContext();
export const AppProvider = ({children}) => {
const [user, setUser] = useState(null);
const [cart, setCart] = useState([]);
const value = { user, setUser, cart, setCart };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}
Каждый компонент, использующий useContext(AppContext)
, будет повторно рендериться при изменении любого значения контекста, даже если он использует только user
, а изменился cart
. Для средних и крупных приложений это становится проблемой.
Решение: разделяйте контексты по доменам:
const UserContext = createContext();
const CartContext = createContext();
// Отдельные провайдеры для разных сущностей
Для динамических данных лучше использовать специализированные библиотеки. Redux Toolkit с его createSlice
и мемоизированными селекторами через createSelector
дает точный контроль над подписками компонентов.
Селекторы с памятью: почему Reselect обязателен
Без мемоизации селекторы вычисляются при каждом рендере, даже если исходные данные не изменились. Вот как это исправить:
import { createSelector } from '@reduxjs/toolkit';
const selectItems = state => state.shop.items;
export const selectTotalPrice = createSelector(
[selectItems],
items => items.reduce((acc, item) => acc + item.price, 0)
);
Но настоящая сила селекторов проявляется при композиции зависимостей. Когда селектор A зависит от селектора B, Reselect автоматически кеширует результат до изменения входных данных.
Динамическая загрузка редьюсеров: когда ваш store слишком велик
Для приложений с code-splitting стандартный подход Redux приводит к загрузке всей логики состояния сразу. Решение — redux-dynamic-modules
:
const ProductModule = {
id: 'products',
reducerMap: {
products: productsReducer,
},
middlewares: [productsMiddleware],
};
function App() {
return (
<DynamicModuleLoader modules={[ProductModule]}>
<ProductPage />
</DynamicModuleLoader>
);
}
Это позволяет подключать части хранилища только когда в них есть необходимость, сокращая начальный размер бандла.
Оптимизация рендера списков: неочевидные нюансы
Даже с React.memo
длинные списки могут тормозить из-за неправильного ключевого параметра. Индекс вместо уникального ID — грубая ошибка:
// Плохо:
{items.map((item, index) => (
<Item key={index} {...item} />
))}
// Хорошо:
{items.map(item => (
<Item key={item.id} {...item} />
))}
Но для настоящей оптимизации используйте виртуализацию через react-window
. Выбор между FixedSizeList
и VariableSizeList
зависит от контента:
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
<List
height={600}
itemCount={10000}
itemSize={35}
width={300}
>
{Row}
</List>
Профилирование: Не доверяйте предположениям. React DevTools Profiler с фиксацией commit phases покажет настоящие узкие места. Обращайте внимание на время выполнения render vs commit phases — длительные рендеры требуют мемоизации, долгие коммиты говорят о проблемах с DOM-операциями.
Когда Redux — это антипаттерн
Локальное состояние — не враг. Формы, UI-флаги, модальные окна часто лучше держать в useState/useReducer. Критерий прост: если данные используются только внутри компонента и его непосредственных потомков — они не должны быть в глобальном хранилище.
Для комплексных форм рассмотрите Formik с контекстом формы. Его хук useFormikContext дает доступ к состоянию без глобального хранилища.
Заключение: контроль состояния как архитектурная дисциплина
- Сегментируйте хранилища по функциональным доменам
- Для часто изменяемых данных используйте специализированные решения (Apollo Client для GraphQL, react-query для REST)
- Профилируйте рендеры через DevTools, а не вслепую
- Принимайте решения на основе lifetime данных: временные данные – локально, сессионные – в Context, персистентные – в Redux + middleware
Последнее правило: всегда спрашивайте «Кому действительно нужны эти данные?» перед добавлением любого состояния в глобальное хранилище. Часто ответ оказывается не таким очевидным, как кажется на первый взгляд.