如何用rxjs做状态管理
更新时间:2023-02-22 15:57:53标签:rxjsreactweb前端
什么是状态管理
状态管理,我们一般都称之为Store,他有点像一个始终存在并且可以让每个人都可以读取和写入的存储空间
如何用Rxjs实现一个状态管理
核心代码
core.ts
1import { BehaviorSubject, catchError, distinctUntilChanged, map, skip, tap, throwError } from 'rxjs';2import type { OperatorFunction, MonoTypeOperatorFunction, Observable } from 'rxjs';34export class Store<T> {5 private behavior$: BehaviorSubject<T>;6 public change$: Observable<T>;7 public state$: Observable<T>;8 public get state() {9 return this.behavior$.value;10 }11 public get tap(): MonoTypeOperatorFunction<T> {12 return tap(v => this.set(v));13 }14 constructor(defaultState: T) {15 const behavior$ = this.behavior$ = new BehaviorSubject(defaultState);16 this.state$ = behavior$.asObservable();17 this.change$ = this.state$.pipe(18 skip(1),19 distinctUntilChanged(),20 );21 }22 public map<I>(fn: (input: I, index: number) => T): OperatorFunction<I, T> {23 return map((v, index) => {24 const ret = fn(v, index);25 this.set(ret);26 return ret;27 });28 }29 public always<I>(fn: (input: T) => T): OperatorFunction<I, T> {30 return source => source.pipe(31 this.map(() => fn(this.state)),32 catchError((err) => {33 this.set(fn(this.state));34 return throwError(() => err);35 }),36 );37 }38 public capture<I>(fn?: (err: any, input: T) => T): OperatorFunction<I, I> {39 return catchError((err, caught) => {40 try {41 fn && this.set(fn(err, this.state));42 } catch (e) {43 // eslint-disable-next-line no-console44 console.error(e);45 }46 return caught;47 });48 }49 public set(value: T) {50 this.behavior$.next(value);51 return this.state;52 }53 public destroy() {54 this.behavior$.complete();55 }56}5758export const createStore = <T>(defaultState: T) => {59 return new Store(defaultState);60};
React Hooks
hook.ts
1import { type Store, createStore } from './core';2import { useEffect, useMemo, useRef, useState } from 'react';34export const useStore = <T>(defaultState: () => T): [T, Store<T>] => {56 // eslint-disable-next-line react-hooks/exhaustive-deps7 const storeRef = useRef(useMemo(() => createStore(defaultState()), []));8 const [state, setState] = useState(defaultState);910 useEffect(() => {11 const instance = storeRef.current;12 Object.assign(instance, createStore(instance.state));13 const subscription = instance.change$.subscribe(res => setState(res));14 return () => {15 instance.destroy();16 subscription.unsubscribe();17 };18 }, []);1920 return [state, storeRef.current];21};2223export const useGetter = <T>(store: Store<T>) => {24 const [state, setState] = useState(store.state);25 useEffect(() => {26 const subscription = store.change$.subscribe(res => setState(res));27 return () => subscription.unsubscribe();28 }, [store]);29 return state;30};
hooks/observable.ts
1import { Observable, Subject, Subscription } from 'rxjs';2import { DependencyList, useEffect, useState } from 'react';34export const useSubject = <T=void, O=unknown>(factory: (action: Subject<T>) => Observable<O>, deps: DependencyList) => {56 const [action, setAction] = useState<Subject<T>>();78 useEffect(() => {9 let subscription: Subscription | null = null;10 if (action) {11 subscription = factory(action).subscribe();12 }13 return () => subscription?.unsubscribe();14 // eslint-disable-next-line react-hooks/exhaustive-deps15 }, [action]);1617 useEffect(() => {18 const subject = new Subject<T>();19 setAction(subject);20 return () => subject.complete();21 // eslint-disable-next-line react-hooks/exhaustive-deps22 }, deps);2324 return action;25};
eslint配置
1{2 "rules": {3 "react-hooks/exhaustive-deps": ["warn", {4 "additionalHooks": "(useSubject)"5 }]6 }7}
实战例子
1. 简单计数器
这是一个简单的计数器,增加或减少
count: 1
代码
1import { FC } from 'react';2import styled from 'styled-components';3import { useStore } from '@shared/stores';4import { useSubject } from '@shared/hooks/observable';56const Wrapper = styled.div`7 margin-bottom: 20px;8 & > *:not(:first-child){9 margin-left: 10px;10 }11`;1213export const CountExample: FC = () => {14 const [state, store] = useStore(() => 1);1516 const increase = useSubject((action) => action.pipe(17 store.map(() => store.state + 1),18 ), [store]);1920 const reduce = useSubject((action) => action.pipe(21 store.map(() => store.state - 1),22 ), [store]);2324 return (25 <Wrapper>26 <button onClick={() => increase?.next()}>增加</button>27 <button onClick={() => reduce?.next()}>减少</button>28 <span>count: { state }</span>29 </Wrapper>30 );31};
2. 防抖
防抖是前端经常需要用到的技术点,例如搜索框,用户在快速输入文字时,保证不会频繁的调用接口
value:
代码
1import { FC } from 'react';2import styled from 'styled-components';3import { createStore, useGetter } from '@shared/stores';4import { debounce, delay, of, switchMap, timer } from 'rxjs';5import { useSubject } from '@shared/hooks/observable';67const Wrapper = styled.div`8 margin-bottom: 20px;9 & > *:not(:first-child){10 margin-left: 10px;11 }12`;1314const store = createStore(''); // 也可以在组件外创建,这样就可以全局共享状态数据了1516export const DebounceExample: FC = () => {1718 const state = useGetter(store);1920 const action = useSubject<string>((action) => action.pipe(21 debounce(() => timer(500)), // 500毫秒内的按键,只有最后一次会被触发22 switchMap((res) => of(res).pipe(delay(Math.random() * 1000))), // 模拟接口返回延迟,随机 0ms - 1000ms23 store.tap,24 ), []);2526 return (27 <Wrapper>28 <input type="text" placeholder={'请输入关键字'} onChange={e => action?.next(e.target.value)} />29 <span>value: {state}</span>30 </Wrapper>31 );32};
3. 稍微复杂点的例子
1
name | gender | cell | phone |
---|
代码
1import { FC, useEffect, useMemo } from 'react';2import { Map } from 'immutable';3import { fromFetch } from 'rxjs/fetch';4import { concatMap, of, switchMap } from 'rxjs';5import { useStore } from '@shared/stores';6import { useSubject } from '@shared/hooks/observable';78const ListExample: FC = () => {910 const [data, store] = useStore(() => Map({11 loading: false,12 error: false,13 page: 1,14 list: [] as any[],15 }));1617 const list = useMemo(() => data.get('list') as any[], [data]);18 const page = useMemo(() => data.get('page') as number, [data]);1920 const getList = useSubject<string | void>(action => action.pipe(21 switchMap(data => {22 if (data === 'cancel') {23 return of(store.state);24 }25 return of(store.state).pipe(26 store.map(state => state.withMutations(m => {27 m.set('loading', true);28 m.set('error', false);29 })),30 concatMap(state => fromFetch(`https://randomuser.me/api?page=${page}&results=10&inc=id,name,email,phone,cell,gender`).pipe(31 switchMap(res => res.json()),32 store.map(res => state.set('list', res.results)),33 )),34 );35 }),36 store.always(data => data.set('loading', false)),37 store.capture((err, data) => data.set('error', true)),38 ), [page, store]);39 const previousPage = useSubject(action => action.pipe(40 store.map(() => store.state.set('page', (store.state.get('page') as number) - 1)),41 ), [store]);42 const nextPage = useSubject(action => action.pipe(43 store.map(() => store.state.set('page', (store.state.get('page') as number) + 1)),44 ), [store]);4546 useEffect(() => {47 getList?.next();48 }, [getList]);4950 return (51 <div>52 <p>53 <button onClick={() => getList?.next()}>54 <span>55 {56 data.get('loading') ? '刷新中...' : '刷新'57 }58 </span>59 </button>60 <button style={{ marginLeft: 10 }} onClick={() => getList?.next('cancel')}>取消请求</button>61 </p>62 <p>63 <button onClick={() => previousPage?.next()}>上一页</button>64 <span>{ data.get('page') }</span>65 <button onClick={() => nextPage?.next()}>下一页</button>66 </p>67 {68 data.get('error') ? (69 <p style={{ color: 'red' }}>出错了,点击刷新按钮重新加载</p>70 ) : null71 }72 <table>73 <thead>74 <tr>75 <th>name</th>76 <th>gender</th>77 <th>email</th>78 <th>cell</th>79 <th>phone</th>80 </tr>81 </thead>82 <tbody>83 {84 list.map((v, k) => (85 <tr key={k}>86 <td>{v.name.title}. {v.name.first} {v.name.last}</td>87 <td>{v.gender}</td>88 <td>{v.email}</td>89 <td>{v.cell}</td>90 <td>{v.phone}</td>91 </tr>92 ))93 }94 </tbody>95 </table>96 </div>97 );98};99100export { ListExample };