useState отлично справляется с простым состоянием: счётчики, флаги, строки. Но когда состояние становится сложным — много связанных полей, сложные переходы, зависимость следующего состояния от предыдущего — код начинает запутываться:
// Проблема: много useState, логика разбросана по обработчикам
function ShoppingCart() {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
const [discount, setDiscount] = useState(0)
const [loading, setLoading] = useState(false)
const addItem = (item) => {
const newItems = [...items, item]
setItems(newItems)
// Нужно вручную синхронизировать total!
setTotal(newItems.reduce((sum, i) => sum + i.price, 0))
}
// ... ещё 10 обработчиков с похожей проблемой
}Редьюсер — чистая функция, которая принимает текущее состояние и действие (action), возвращает новое состояние:
reducer(state, action) => newState// Редьюсер описывает ВСЮ логику изменения состояния в одном месте
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const newItems = [...state.items, action.payload]
return {
...state,
items: newItems,
total: newItems.reduce((sum, i) => sum + i.price, 0)
}
case 'REMOVE_ITEM':
const filtered = state.items.filter(i => i.id !== action.payload)
return {
...state,
items: filtered,
total: filtered.reduce((sum, i) => sum + i.price, 0)
}
case 'CLEAR':
return { items: [], total: 0, discount: 0 }
default:
return state // всегда возвращаем state по умолчанию!
}
}function ShoppingCart() {
const initialState = { items: [], total: 0, discount: 0 }
const [state, dispatch] = useReducer(cartReducer, initialState)
// Диспетчеризация действий — чисто и явно
const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item })
const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id })
const clearCart = () => dispatch({ type: 'CLEAR' })
return (
<div>
{state.items.map(item => (
<CartItem key={item.id} item={item} onRemove={removeItem} />
))}
<p>Итого: {state.total} ₽</p>
<button onClick={clearCart}>Очистить</button>
</div>
)
}1. Вся логика в одном месте — редьюсер легко читать и тестировать
2. Явные действия — по коду сразу видно что происходит
3. Чистые функции — редьюсер не зависит от внешних данных, легко тестировать
4. Предсказуемость — то же состояние + то же действие = тот же результат
Комбинируя useReducer и Context, можно создать глобальное хранилище:
const StoreContext = createContext(null)
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState)
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
)
}
// В любом компоненте
function CartBadge() {
const { state } = useContext(StoreContext)
return <span>{state.cart.items.length}</span>
}
function AddToCartButton({ product }) {
const { dispatch } = useContext(StoreContext)
return (
<button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
В корзину
</button>
)
}// Discriminated union — TypeScript автоматически сужает тип
type Action =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: number }
| { type: 'CLEAR' }
interface CartState {
items: CartItem[]
total: number
}
function cartReducer(state: CartState, action: Action): CartState {
switch (action.type) {
case 'ADD_ITEM':
// TypeScript знает: action.payload — это CartItem
return { ...state, items: [...state.items, action.payload] }
case 'REMOVE_ITEM':
// TypeScript знает: action.payload — это number (id)
return { ...state, items: state.items.filter(i => i.id !== action.payload) }
case 'CLEAR':
return { items: [], total: 0 }
}
}Реализация Redux-подобного хранилища с нуля: createStore с dispatch/getState/subscribe, затем редьюсер для списка задач
// Реализуем createStore — сердце Redux — с нуля.
// useReducer в React — это упрощённая версия именно этого паттерна.
function createStore(reducer, initialState) {
let state = initialState
const listeners = new Set()
return {
// Читаем текущее состояние
getState() {
return state
},
// Отправляем действие — reducer вычисляет новое состояние
dispatch(action) {
const prevState = state
state = reducer(state, action)
// Уведомляем подписчиков только если состояние изменилось
if (state !== prevState) {
listeners.forEach(fn => fn(state))
}
console.log(`[dispatch] ${action.type}`, action.payload ?? '')
return action
},
// Подписываемся на изменения (как useEffect в компонентах)
subscribe(listener) {
listeners.add(listener)
return () => listeners.delete(listener) // функция отписки
}
}
}
// --- Редьюсер для списка задач (ToDo) ---
const initialState = {
todos: [],
nextId: 1
}
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: state.nextId,
text: action.payload,
completed: false
}],
nextId: state.nextId + 1
}
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
}
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
}
default:
return state // важно: всегда возвращаем state по умолчанию!
}
}
// --- Запускаем ---
const store = createStore(todoReducer, initialState)
// Подписываемся на изменения
const unsubscribe = store.subscribe(state => {
const completed = state.todos.filter(t => t.completed).length
console.log(` Задач: ${state.todos.length}, выполнено: ${completed}`)
})
console.log('=== ToDo Store ===')
store.dispatch({ type: 'ADD_TODO', payload: 'Изучить useReducer' })
store.dispatch({ type: 'ADD_TODO', payload: 'Написать тесты' })
store.dispatch({ type: 'ADD_TODO', payload: 'Сделать code review' })
store.dispatch({ type: 'TOGGLE_TODO', payload: 1 })
store.dispatch({ type: 'TOGGLE_TODO', payload: 2 })
console.log('
Текущие задачи:')
store.getState().todos.forEach(t => {
console.log(` [${t.completed ? 'x' : ' '}] #${t.id}: ${t.text}`)
})
store.dispatch({ type: 'CLEAR_COMPLETED' })
console.log('
После очистки выполненных:')
store.getState().todos.forEach(t => {
console.log(` [ ] #${t.id}: ${t.text}`)
})
store.dispatch({ type: 'DELETE_TODO', payload: 3 })
console.log(
Итого задач: ${store.getState().todos.length})
unsubscribe() // отписываемся от уведомленийuseState отлично справляется с простым состоянием: счётчики, флаги, строки. Но когда состояние становится сложным — много связанных полей, сложные переходы, зависимость следующего состояния от предыдущего — код начинает запутываться:
// Проблема: много useState, логика разбросана по обработчикам
function ShoppingCart() {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
const [discount, setDiscount] = useState(0)
const [loading, setLoading] = useState(false)
const addItem = (item) => {
const newItems = [...items, item]
setItems(newItems)
// Нужно вручную синхронизировать total!
setTotal(newItems.reduce((sum, i) => sum + i.price, 0))
}
// ... ещё 10 обработчиков с похожей проблемой
}Редьюсер — чистая функция, которая принимает текущее состояние и действие (action), возвращает новое состояние:
reducer(state, action) => newState// Редьюсер описывает ВСЮ логику изменения состояния в одном месте
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const newItems = [...state.items, action.payload]
return {
...state,
items: newItems,
total: newItems.reduce((sum, i) => sum + i.price, 0)
}
case 'REMOVE_ITEM':
const filtered = state.items.filter(i => i.id !== action.payload)
return {
...state,
items: filtered,
total: filtered.reduce((sum, i) => sum + i.price, 0)
}
case 'CLEAR':
return { items: [], total: 0, discount: 0 }
default:
return state // всегда возвращаем state по умолчанию!
}
}function ShoppingCart() {
const initialState = { items: [], total: 0, discount: 0 }
const [state, dispatch] = useReducer(cartReducer, initialState)
// Диспетчеризация действий — чисто и явно
const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item })
const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id })
const clearCart = () => dispatch({ type: 'CLEAR' })
return (
<div>
{state.items.map(item => (
<CartItem key={item.id} item={item} onRemove={removeItem} />
))}
<p>Итого: {state.total} ₽</p>
<button onClick={clearCart}>Очистить</button>
</div>
)
}1. Вся логика в одном месте — редьюсер легко читать и тестировать
2. Явные действия — по коду сразу видно что происходит
3. Чистые функции — редьюсер не зависит от внешних данных, легко тестировать
4. Предсказуемость — то же состояние + то же действие = тот же результат
Комбинируя useReducer и Context, можно создать глобальное хранилище:
const StoreContext = createContext(null)
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState)
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
)
}
// В любом компоненте
function CartBadge() {
const { state } = useContext(StoreContext)
return <span>{state.cart.items.length}</span>
}
function AddToCartButton({ product }) {
const { dispatch } = useContext(StoreContext)
return (
<button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
В корзину
</button>
)
}// Discriminated union — TypeScript автоматически сужает тип
type Action =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: number }
| { type: 'CLEAR' }
interface CartState {
items: CartItem[]
total: number
}
function cartReducer(state: CartState, action: Action): CartState {
switch (action.type) {
case 'ADD_ITEM':
// TypeScript знает: action.payload — это CartItem
return { ...state, items: [...state.items, action.payload] }
case 'REMOVE_ITEM':
// TypeScript знает: action.payload — это number (id)
return { ...state, items: state.items.filter(i => i.id !== action.payload) }
case 'CLEAR':
return { items: [], total: 0 }
}
}Реализация Redux-подобного хранилища с нуля: createStore с dispatch/getState/subscribe, затем редьюсер для списка задач
// Реализуем createStore — сердце Redux — с нуля.
// useReducer в React — это упрощённая версия именно этого паттерна.
function createStore(reducer, initialState) {
let state = initialState
const listeners = new Set()
return {
// Читаем текущее состояние
getState() {
return state
},
// Отправляем действие — reducer вычисляет новое состояние
dispatch(action) {
const prevState = state
state = reducer(state, action)
// Уведомляем подписчиков только если состояние изменилось
if (state !== prevState) {
listeners.forEach(fn => fn(state))
}
console.log(`[dispatch] ${action.type}`, action.payload ?? '')
return action
},
// Подписываемся на изменения (как useEffect в компонентах)
subscribe(listener) {
listeners.add(listener)
return () => listeners.delete(listener) // функция отписки
}
}
}
// --- Редьюсер для списка задач (ToDo) ---
const initialState = {
todos: [],
nextId: 1
}
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: state.nextId,
text: action.payload,
completed: false
}],
nextId: state.nextId + 1
}
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
}
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
}
default:
return state // важно: всегда возвращаем state по умолчанию!
}
}
// --- Запускаем ---
const store = createStore(todoReducer, initialState)
// Подписываемся на изменения
const unsubscribe = store.subscribe(state => {
const completed = state.todos.filter(t => t.completed).length
console.log(` Задач: ${state.todos.length}, выполнено: ${completed}`)
})
console.log('=== ToDo Store ===')
store.dispatch({ type: 'ADD_TODO', payload: 'Изучить useReducer' })
store.dispatch({ type: 'ADD_TODO', payload: 'Написать тесты' })
store.dispatch({ type: 'ADD_TODO', payload: 'Сделать code review' })
store.dispatch({ type: 'TOGGLE_TODO', payload: 1 })
store.dispatch({ type: 'TOGGLE_TODO', payload: 2 })
console.log('
Текущие задачи:')
store.getState().todos.forEach(t => {
console.log(` [${t.completed ? 'x' : ' '}] #${t.id}: ${t.text}`)
})
store.dispatch({ type: 'CLEAR_COMPLETED' })
console.log('
После очистки выполненных:')
store.getState().todos.forEach(t => {
console.log(` [ ] #${t.id}: ${t.text}`)
})
store.dispatch({ type: 'DELETE_TODO', payload: 3 })
console.log(
Итого задач: ${store.getState().todos.length})
unsubscribe() // отписываемся от уведомленийСоздай компонент App со списком задач на useReducer. Редьюсер должен обрабатывать три действия: ADD (добавить задачу по тексту из поля ввода), TOGGLE (отметить выполненной по id), DELETE (удалить по id). Отображай список задач с чекбоксами и кнопками удаления.
useReducer(todoReducer, initialState) возвращает [state, dispatch]. dispatch({ type: "ADD", payload: text }) добавляет задачу. В TOGGLE используй !t.completed. В DELETE используй .filter(). nextId увеличивай на 1 в каждом ADD.