如何用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';
3
4export 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-console
44 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}
57
58export 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';
3
4export const useStore = <T>(defaultState: () => T): [T, Store<T>] => {
5
6 // eslint-disable-next-line react-hooks/exhaustive-deps
7 const storeRef = useRef(useMemo(() => createStore(defaultState()), []));
8 const [state, setState] = useState(defaultState);
9
10 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 }, []);
19
20 return [state, storeRef.current];
21};
22
23export 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';
3
4export const useSubject = <T=void, O=unknown>(factory: (action: Subject<T>) => Observable<O>, deps: DependencyList) => {
5
6 const [action, setAction] = useState<Subject<T>>();
7
8 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-deps
15 }, [action]);
16
17 useEffect(() => {
18 const subject = new Subject<T>();
19 setAction(subject);
20 return () => subject.complete();
21 // eslint-disable-next-line react-hooks/exhaustive-deps
22 }, deps);
23
24 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';
5
6const Wrapper = styled.div`
7 margin-bottom: 20px;
8 & > *:not(:first-child){
9 margin-left: 10px;
10 }
11`;
12
13export const CountExample: FC = () => {
14 const [state, store] = useStore(() => 1);
15
16 const increase = useSubject((action) => action.pipe(
17 store.map(() => store.state + 1),
18 ), [store]);
19
20 const reduce = useSubject((action) => action.pipe(
21 store.map(() => store.state - 1),
22 ), [store]);
23
24 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';
6
7const Wrapper = styled.div`
8 margin-bottom: 20px;
9 & > *:not(:first-child){
10 margin-left: 10px;
11 }
12`;
13
14const store = createStore(''); // 也可以在组件外创建,这样就可以全局共享状态数据了
15
16export const DebounceExample: FC = () => {
17
18 const state = useGetter(store);
19
20 const action = useSubject<string>((action) => action.pipe(
21 debounce(() => timer(500)), // 500毫秒内的按键,只有最后一次会被触发
22 switchMap((res) => of(res).pipe(delay(Math.random() * 1000))), // 模拟接口返回延迟,随机 0ms - 1000ms
23 store.tap,
24 ), []);
25
26 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

namegenderemailcellphone
代码
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';
7
8const ListExample: FC = () => {
9
10 const [data, store] = useStore(() => Map({
11 loading: false,
12 error: false,
13 page: 1,
14 list: [] as any[],
15 }));
16
17 const list = useMemo(() => data.get('list') as any[], [data]);
18 const page = useMemo(() => data.get('page') as number, [data]);
19
20 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]);
45
46 useEffect(() => {
47 getList?.next();
48 }, [getList]);
49
50 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 ) : null
71 }
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};
99
100export { ListExample };