17. Кастомные хуки с RxJS
Кастомные React-хуки с RxJS 🎣
Заголовок раздела «Кастомные React-хуки с RxJS 🎣»Яша снова здесь! В прошлом уроке мы подключали RxJS прямо в компонентах. Но это быстро превращается в лапшу. Решение — кастомные хуки. Они инкапсулируют подписочную логику, дают переиспользуемые кирпичики и делают компоненты чистыми и декларативными. Строим хуки как Lego!
1. 🔩 Базовый хук: useObservable
Заголовок раздела «1. 🔩 Базовый хук: useObservable»Самый универсальный хук — принимает любой Observable и возвращает его последнее значение:
import { useState, useEffect } from 'react';import { Observable } from 'rxjs';
// T — тип значений из потока// I — тип начального значения (может отличаться от T | undefined)function useObservable<T>( observable$: Observable<T>, initialValue?: T): T | undefined { const [value, setValue] = useState<T | undefined>(initialValue);
useEffect(() => { const subscription = observable$.subscribe({ next: setValue, error: (err) => console.error('[useObservable]', err), });
return () => subscription.unsubscribe(); }, [observable$]); // зависимость: если поток меняется — перезапускаемся
return value;}
// Использование:const count = useObservable(counter$, 0);const windowSize = useObservable(resize$);⚠️ Важно: если
observable$— это новый объект при каждом рендере, эффект будет срабатывать бесконечно. Создавай стримы вне компонента или оборачивай вuseRef/useMemo.
2. 🎯 useSubject: двустороннее управление потоком
Заголовок раздела «2. 🎯 useSubject: двустороннее управление потоком»Хук для работы с Subject — позволяет и читать, и писать в поток:
import { useRef, useCallback, useEffect, useState } from 'react';import { Subject, Observable } from 'rxjs';
interface UseSubjectResult<T> { value: T | undefined; next: (value: T) => void; observable$: Observable<T>; complete: () => void;}
function useSubject<T>(initialValue?: T): UseSubjectResult<T> { // useRef гарантирует, что Subject создаётся один раз const subjectRef = useRef(new Subject<T>()); const [value, setValue] = useState<T | undefined>(initialValue);
useEffect(() => { const sub = subjectRef.current.subscribe(setValue); return () => sub.unsubscribe(); }, []);
const next = useCallback((val: T) => { subjectRef.current.next(val); }, []);
const complete = useCallback(() => { subjectRef.current.complete(); }, []);
return { value, next, observable$: subjectRef.current.asObservable(), complete, };}
// Использование:function SearchBox() { const { value: query, next: setQuery } = useSubject<string>(''); const results = useObservable( useMemo(() => search$(query ?? ''), [query]) ); return <input value={query} onChange={e => setQuery(e.target.value)} />;}3. 🏪 useBehaviorSubject: реактивный стейт с историей
Заголовок раздела «3. 🏪 useBehaviorSubject: реактивный стейт с историей»BehaviorSubject всегда хранит последнее значение и отдаёт его новым подписчикам. Идеален для состояния:
import { useRef, useState, useEffect, useCallback } from 'react';import { BehaviorSubject } from 'rxjs';import { distinctUntilChanged, map } from 'rxjs/operators';
function useBehaviorSubject<T>(initialValue: T) { const subjectRef = useRef(new BehaviorSubject<T>(initialValue)); const [state, setState] = useState<T>(initialValue);
useEffect(() => { const sub = subjectRef.current .pipe(distinctUntilChanged()) // не рендерим если значение не изменилось .subscribe(setState);
return () => sub.unsubscribe(); }, []);
const setValue = useCallback((next: T | ((prev: T) => T)) => { const current = subjectRef.current.getValue(); const nextValue = typeof next === 'function' ? (next as (prev: T) => T)(current) : next; subjectRef.current.next(nextValue); }, []);
// Доступ к сырому Subject для pipe()-цепочек в других местах const getSubject = useCallback(() => subjectRef.current, []);
return [state, setValue, getSubject] as const;}
// Использование:const [count, setCount, getCount$] = useBehaviorSubject(0);// Производный поток:const doubled$ = getCount$().pipe(map(n => n * 2));4. 🔗 TypeScript типизация хуков
Заголовок раздела «4. 🔗 TypeScript типизация хуков»Строгая типизация делает хуки безопасными:
import { Observable, BehaviorSubject, Subject } from 'rxjs';
// Тип для результата useObservabletype UseObservableState<T> = { value: T | undefined; loading: boolean; error: Error | null;};
// Продвинутая версия с loading/error состояниямиfunction useObservableWithStatus<T>( factory: () => Observable<T>, deps: React.DependencyList = []): UseObservableState<T> { const [state, setState] = useState<UseObservableState<T>>({ value: undefined, loading: true, error: null, });
useEffect(() => { setState(prev => ({ ...prev, loading: true, error: null }));
const sub = factory().subscribe({ next: value => setState({ value, loading: false, error: null }), error: error => setState(prev => ({ ...prev, loading: false, error })), complete: () => setState(prev => ({ ...prev, loading: false })), });
return () => sub.unsubscribe(); // eslint-disable-next-line react-hooks/exhaustive-deps }, deps);
return state;}
// Пример с типобезопасным хуком для API-запросовinterface User { id: number; name: string; }
function useUser(userId: number): UseObservableState<User> { return useObservableWithStatus<User>( () => fetchUser$(userId), [userId] );}
// В компоненте:const { value: user, loading, error } = useUser(42);⚠️ Типичные ошибки
Заголовок раздела «⚠️ Типичные ошибки»Ошибка 1: Observable в зависимостях — бесконечный цикл
// ❌ pipe() создаёт новый Observable на каждом рендереfunction BadHook({ userId }: { userId: number }) { const user$ = userStore$.pipe(map(state => state.users[userId])); const user = useObservable(user$); // Бесконечный цикл!}
// ✅ useMemo стабилизирует ссылкуfunction GoodHook({ userId }: { userId: number }) { const user$ = useMemo( () => userStore$.pipe(map(state => state.users[userId])), [userId] ); const user = useObservable(user$);}Ошибка 2: Subject снаружи компонента = один на всех
// ❌ Все экземпляры компонента шарят один Subjectconst sharedSubject$ = new Subject<number>(); // Глобальный!
// ✅ useRef создаёт отдельный Subject для каждого экземпляраfunction SafeComponent() { const subject$ = useRef(new Subject<number>()).current;}🔗 Смотри также
Заголовок раздела «🔗 Смотри также»- 17. RxJS + React — базовая интеграция
- 19. RxJS как стейт-менеджер — паттерн BehaviorSubject store
- 3. Subjects: горячие потоки — Subject и BehaviorSubject подробнее
Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: