18. RxJS как стейт-менеджер
RxJS как стейт-менеджер 🏪
Заголовок раздела «RxJS как стейт-менеджер 🏪»Привет! Яша здесь. Redux тяжёлый? Context API теряет в производительности? MobX магия? Попробуй BehaviorSubject — минимальный, предсказуемый и мощный паттерн управления состоянием на чистом RxJS. Никаких дополнительных библиотек!
1. 🏗️ Паттерн BehaviorSubject Store
Заголовок раздела «1. 🏗️ Паттерн BehaviorSubject Store»Идея проста: BehaviorSubject хранит текущее состояние и уведомляет всех подписчиков об изменениях:
import { BehaviorSubject, Observable } from 'rxjs';import { map, distinctUntilChanged, pluck } from 'rxjs/operators';
// Тип стейтаinterface AppState { todos: Todo[]; filter: 'all' | 'active' | 'done'; loading: boolean;}
// Действия (типизированный union)type Action = | { type: 'ADD_TODO'; payload: string } | { type: 'TOGGLE_TODO'; payload: number } | { type: 'REMOVE_TODO'; payload: number } | { type: 'SET_FILTER'; payload: AppState['filter'] } | { type: 'SET_LOADING'; payload: boolean };
// Начальное состояниеconst initialState: AppState = { todos: [], filter: 'all', loading: false,};
// Сам сторclass TodoStore { private state$ = new BehaviorSubject<AppState>(initialState); private dispatch$ = new Subject<Action>();
constructor() { // Редьюсер — обрабатывает действия через scan() this.dispatch$.pipe( scan(this.reducer, initialState) ).subscribe(this.state$); }
// Редьюсер — чистая функция, как в Redux private reducer(state: AppState, action: Action): AppState { switch (action.type) { case 'ADD_TODO': return { ...state, todos: [ ...state.todos, { id: Date.now(), text: action.payload, done: false } ], }; case 'TOGGLE_TODO': return { ...state, todos: state.todos.map(t => t.id === action.payload ? { ...t, done: !t.done } : t ), }; case 'REMOVE_TODO': return { ...state, todos: state.todos.filter(t => t.id !== action.payload), }; case 'SET_FILTER': return { ...state, filter: action.payload }; default: return state; } }
// Публичный API dispatch(action: Action) { this.dispatch$.next(action); }
// Селекторы — производные Observable get todos$(): Observable<Todo[]> { return this.state$.pipe( map(s => s.todos), distinctUntilChanged() ); }
get filteredTodos$(): Observable<Todo[]> { return this.state$.pipe( map(s => { if (s.filter === 'active') return s.todos.filter(t => !t.done); if (s.filter === 'done') return s.todos.filter(t => t.done); return s.todos; }), distinctUntilChanged() ); }
get completedCount$(): Observable<number> { return this.todos$.pipe(map(todos => todos.filter(t => t.done).length)); }}
export const store = new TodoStore();2. ⚡ Сравнение с Redux
Заголовок раздела «2. ⚡ Сравнение с Redux»| Концепция | Redux | RxJS Store |
|---|---|---|
| Хранилище | createStore(reducer) | new BehaviorSubject(state) |
| Действия | dispatch(action) | subject.next(action) |
| Редьюсер | (state, action) => state | scan((state, action) => state) |
| Селекторы | createSelector | pipe(map(...), distinctUntilChanged()) |
| Middleware | applyMiddleware(thunk) | pipe(mergeMap(...)) |
| DevTools | Redux DevTools | tap(state => console.log(state)) |
| Размер | ~7KB | 0KB (уже есть!) |
Главное отличие: в Redux стор — это синхронный объект. В RxJS — это реактивный поток. Это значит, что компоненты могут подписываться на конкретные части стейта, а не на весь стор целиком.
3. 🔌 Подключение к React
Заголовок раздела «3. 🔌 Подключение к React»import { useEffect, useState, useRef, useMemo } from 'react';import { Observable, BehaviorSubject } from 'rxjs';import { distinctUntilChanged, map } from 'rxjs/operators';
// Хук для подключения к BehaviorSubject сторуfunction useSelector<T, R>( store: BehaviorSubject<T>, selector: (state: T) => R): R { const selected$ = useMemo( () => store.pipe(map(selector), distinctUntilChanged()), [store, selector] );
const [value, setValue] = useState<R>(() => selector(store.getValue()));
useEffect(() => { const sub = selected$.subscribe(setValue); return () => sub.unsubscribe(); }, [selected$]);
return value;}
// Компонент использует только нужный срез стейтаfunction TodoCounter() { const total = useSelector(todoStore, s => s.todos.length); const done = useSelector(todoStore, s => s.todos.filter(t => t.done).length); const loading = useSelector(todoStore, s => s.loading);
return ( <div> {loading ? 'Загрузка...' : `${done}/${total} выполнено`} </div> );}distinctUntilChanged() — ключевой оператор: компонент не перерендерится, если его срез стейта не изменился.
4. 🌊 Асинхронные действия через потоки
Заголовок раздела «4. 🌊 Асинхронные действия через потоки»В отличие от Redux Thunk, в RxJS асинхронность — это просто операторы:
import { Subject, merge, from } from 'rxjs';import { mergeMap, map, catchError, startWith } from 'rxjs/operators';
// Действия с эффектамиconst loadTodos$ = new Subject<void>();
// "Эффект" — обрабатывает асинхронный экшенconst todosEffect$ = loadTodos$.pipe( mergeMap(() => from(fetch('/api/todos').then(r => r.json())).pipe( map(todos => ({ type: 'LOAD_TODOS_SUCCESS' as const, payload: todos })), startWith({ type: 'SET_LOADING' as const, payload: true }), catchError(err => [{ type: 'LOAD_TODOS_ERROR' as const, payload: err.message }]) ) ));
// Подписываемся и диспатчим результатыtodosEffect$.subscribe(action => store.dispatch(action));
// Вызвать загрузку:loadTodos$.next();⚠️ Типичные ошибки
Заголовок раздела «⚠️ Типичные ошибки»Ошибка 1: Мутировать стейт напрямую
// ❌ Мутация — подписчики не получат обновление!const state = store.getValue();state.todos.push({ id: 1, text: 'task', done: false });store.next(state); // ссылка та же — distinctUntilChanged пропустит!
// ✅ Всегда создавай новый объектstore.next({ ...state, todos: [...state.todos, { id: 1, text: 'task', done: false }],});Ошибка 2: Подписываться на весь стор без селектора
// ❌ Перерендер при ЛЮБОМ изменении стейтаuseEffect(() => { const sub = appStore$.subscribe(state => setTodos(state.todos)); return () => sub.unsubscribe();}, []);
// ✅ Только релевантные измененияuseEffect(() => { const sub = appStore$.pipe( map(s => s.todos), distinctUntilChanged() ).subscribe(setTodos); return () => sub.unsubscribe();}, []);🔗 Смотри также
Заголовок раздела «🔗 Смотри также»- 3. Subjects: горячие потоки — BehaviorSubject подробно
- 18. Кастомные хуки с RxJS — хуки useSelector
- 20. RxJS vs Effector — сравнение подходов
- 21. RxJS + MobX — MobX-подобные вычисляемые значения
Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: