По мере роста приложения возникает несколько проблем:
React Context хорошо справляется с редко изменяемыми данными (тема, язык, авторизация), но для часто обновляемого состояния нужны специализированные инструменты.
Redux — классическое решение, но требует много шаблонного кода:
// Redux: action types, action creators, reducers, store, connect...
const ADD_TODO = 'ADD_TODO'
const addTodo = (text) => ({ type: ADD_TODO, payload: text })
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO: return [...state, { text: action.payload }]
default: return state
}
}
const store = createStore(todosReducer)
// ... ещё connect, mapStateToProps, mapDispatchToPropsДля небольших и средних приложений это избыточно.
Zustand (нем. "состояние") — минималистичная библиотека с отличной производительностью. Весь стор — один объект с состоянием и действиями:
import { create } from 'zustand'
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
// В компоненте — как обычный хук
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return <button onClick={increment}>{count}</button>
}Ключевое: компонент подписывается только на нужные поля через селектор — остальные изменения его не перерисовывают!
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, qty) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity: qty } : item
)
})),
// Вычисляемое значение через get()
getTotal: () => {
const { items } = get()
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
clear: () => set({ items: [] }),
}))
// Компонент читает только нужные данные
function CartTotal() {
const getTotal = useCartStore((state) => state.getTotal)
return <span>Итого: {getTotal()} ₽</span>
}import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
const useStore = create(
devtools( // подключает Redux DevTools
persist( // сохраняет в localStorage автоматически
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'app-storage' } // ключ в localStorage
)
)
)| | Context | useReducer+Context | Zustand | Redux Toolkit |
|---|---|---|---|---|
| Настройка | Минимальная | Средняя | Минимальная | Много |
| Производительность | Перерисует всех | Перерисует всех | Точечные подписки | Точечные подписки |
| DevTools | Нет | Нет | Есть | Отличные |
| Размер бандла | 0кб | 0кб | ~1кб | ~15кб |
| Когда использовать | Статичные данные | Простая логика | Большинство случаев | Большие команды |
Рекомендация: начинайте с useState/Context, переходите на Zustand когда нужна производительность или общее состояние между несвязанными компонентами.
Реализация Zustand-подобного хранилища с нуля: замыкание с подписчиками, точечные обновления, селекторы
// Реализуем Zustand-like create() с нуля.
// Это покажет, как Zustand достигает точечных обновлений без Context.
function create(storeCreator) {
let state
const listeners = new Set()
// set() — передаётся в storeCreator, позволяет обновлять состояние
const set = (updater) => {
const prevState = state
const updates = typeof updater === 'function' ? updater(state) : updater
state = { ...state, ...updates }
// Уведомляем подписчиков только если состояние изменилось
if (state !== prevState) {
listeners.forEach(listener => listener(state, prevState))
}
}
// get() — позволяет читать текущее состояние из actions
const get = () => state
// Инициализируем стор
state = storeCreator(set, get)
// Возвращаем хук-функцию (в React это был бы useStore)
function useStore(selector) {
// Без React: просто читаем значение через селектор
return selector ? selector(state) : state
}
// Подписка на изменения (в React это делает хук автоматически)
useStore.subscribe = (listener, selector) => {
const wrappedListener = (newState, prevState) => {
// Если передан селектор — проверяем только выбранное поле
if (selector) {
const newSelected = selector(newState)
const prevSelected = selector(prevState)
if (newSelected === prevSelected) return // точечная подписка!
}
listener(newState)
}
listeners.add(wrappedListener)
return () => listeners.delete(wrappedListener)
}
useStore.getState = get
useStore.setState = set
return useStore
}
// --- Создаём стор для корзины ---
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, qty) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity: qty } : item
)
})),
getTotal: () => {
const { items } = get()
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
clear: () => set({ items: [] })
}))
// --- Точечные подписки (ключевая фишка Zustand) ---
console.log('=== Точечные подписки ===')
let totalRenders = 0
let itemsRenders = 0
// Компонент 1: подписан только на items
const unsubItems = useCartStore.subscribe(
(state) => { itemsRenders++; console.log(` [CartList] обновление, товаров: ${state.items.length}`) },
(state) => state.items // селектор
)
// Компонент 2: подписан только на total через функцию
const unsubTotal = useCartStore.subscribe(
(state) => {
totalRenders++
const total = useCartStore.getState().getTotal()
console.log(` [CartTotal] обновление, итого: ${total} ₽`)
},
(state) => state.items // перерисовываемся только при изменении items
)
// --- Действия ---
const { addItem, removeItem, updateQuantity, clear } = useCartStore.getState()
console.log('
Добавляем товары:')
addItem({ id: 1, name: 'Молоко', price: 89 })
addItem({ id: 2, name: 'Хлеб', price: 45 })
addItem({ id: 3, name: 'Масло', price: 120 })
console.log('
Изменяем количество:')
updateQuantity(1, 2) // 2 литра молока
console.log('
Удаляем товар:')
removeItem(2)
const total = useCartStore.getState().getTotal()
console.log(`
Итого: ${total} ₽`) // 89*2 + 120 = 298
unsubItems()
unsubTotal()По мере роста приложения возникает несколько проблем:
React Context хорошо справляется с редко изменяемыми данными (тема, язык, авторизация), но для часто обновляемого состояния нужны специализированные инструменты.
Redux — классическое решение, но требует много шаблонного кода:
// Redux: action types, action creators, reducers, store, connect...
const ADD_TODO = 'ADD_TODO'
const addTodo = (text) => ({ type: ADD_TODO, payload: text })
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO: return [...state, { text: action.payload }]
default: return state
}
}
const store = createStore(todosReducer)
// ... ещё connect, mapStateToProps, mapDispatchToPropsДля небольших и средних приложений это избыточно.
Zustand (нем. "состояние") — минималистичная библиотека с отличной производительностью. Весь стор — один объект с состоянием и действиями:
import { create } from 'zustand'
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
// В компоненте — как обычный хук
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return <button onClick={increment}>{count}</button>
}Ключевое: компонент подписывается только на нужные поля через селектор — остальные изменения его не перерисовывают!
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, qty) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity: qty } : item
)
})),
// Вычисляемое значение через get()
getTotal: () => {
const { items } = get()
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
clear: () => set({ items: [] }),
}))
// Компонент читает только нужные данные
function CartTotal() {
const getTotal = useCartStore((state) => state.getTotal)
return <span>Итого: {getTotal()} ₽</span>
}import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
const useStore = create(
devtools( // подключает Redux DevTools
persist( // сохраняет в localStorage автоматически
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'app-storage' } // ключ в localStorage
)
)
)| | Context | useReducer+Context | Zustand | Redux Toolkit |
|---|---|---|---|---|
| Настройка | Минимальная | Средняя | Минимальная | Много |
| Производительность | Перерисует всех | Перерисует всех | Точечные подписки | Точечные подписки |
| DevTools | Нет | Нет | Есть | Отличные |
| Размер бандла | 0кб | 0кб | ~1кб | ~15кб |
| Когда использовать | Статичные данные | Простая логика | Большинство случаев | Большие команды |
Рекомендация: начинайте с useState/Context, переходите на Zustand когда нужна производительность или общее состояние между несвязанными компонентами.
Реализация Zustand-подобного хранилища с нуля: замыкание с подписчиками, точечные обновления, селекторы
// Реализуем Zustand-like create() с нуля.
// Это покажет, как Zustand достигает точечных обновлений без Context.
function create(storeCreator) {
let state
const listeners = new Set()
// set() — передаётся в storeCreator, позволяет обновлять состояние
const set = (updater) => {
const prevState = state
const updates = typeof updater === 'function' ? updater(state) : updater
state = { ...state, ...updates }
// Уведомляем подписчиков только если состояние изменилось
if (state !== prevState) {
listeners.forEach(listener => listener(state, prevState))
}
}
// get() — позволяет читать текущее состояние из actions
const get = () => state
// Инициализируем стор
state = storeCreator(set, get)
// Возвращаем хук-функцию (в React это был бы useStore)
function useStore(selector) {
// Без React: просто читаем значение через селектор
return selector ? selector(state) : state
}
// Подписка на изменения (в React это делает хук автоматически)
useStore.subscribe = (listener, selector) => {
const wrappedListener = (newState, prevState) => {
// Если передан селектор — проверяем только выбранное поле
if (selector) {
const newSelected = selector(newState)
const prevSelected = selector(prevState)
if (newSelected === prevSelected) return // точечная подписка!
}
listener(newState)
}
listeners.add(wrappedListener)
return () => listeners.delete(wrappedListener)
}
useStore.getState = get
useStore.setState = set
return useStore
}
// --- Создаём стор для корзины ---
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, qty) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity: qty } : item
)
})),
getTotal: () => {
const { items } = get()
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
clear: () => set({ items: [] })
}))
// --- Точечные подписки (ключевая фишка Zustand) ---
console.log('=== Точечные подписки ===')
let totalRenders = 0
let itemsRenders = 0
// Компонент 1: подписан только на items
const unsubItems = useCartStore.subscribe(
(state) => { itemsRenders++; console.log(` [CartList] обновление, товаров: ${state.items.length}`) },
(state) => state.items // селектор
)
// Компонент 2: подписан только на total через функцию
const unsubTotal = useCartStore.subscribe(
(state) => {
totalRenders++
const total = useCartStore.getState().getTotal()
console.log(` [CartTotal] обновление, итого: ${total} ₽`)
},
(state) => state.items // перерисовываемся только при изменении items
)
// --- Действия ---
const { addItem, removeItem, updateQuantity, clear } = useCartStore.getState()
console.log('
Добавляем товары:')
addItem({ id: 1, name: 'Молоко', price: 89 })
addItem({ id: 2, name: 'Хлеб', price: 45 })
addItem({ id: 3, name: 'Масло', price: 120 })
console.log('
Изменяем количество:')
updateQuantity(1, 2) // 2 литра молока
console.log('
Удаляем товар:')
removeItem(2)
const total = useCartStore.getState().getTotal()
console.log(`
Итого: ${total} ₽`) // 89*2 + 120 = 298
unsubItems()
unsubTotal()Реализуй подъём состояния (lifting state up). Есть два дочерних компонента TemperatureInput: один для Цельсия, другой для Фаренгейта. При вводе в один — другой автоматически обновляется. Храни температуру в компоненте App и передавай вниз через пропсы.
Формула C→F: celsius * 9/5 + 32. Формула F→C: (fahrenheit - 32) * 5/9. TemperatureInput получает value и onChange через пропсы. onChange для Цельсия: setCelsius. onChange для Фаренгейта: handleFahrenheitChange. Это классический паттерн "поднятия состояния".