我的 React 状态管理进化之路 | 创作者训练营第二期

1,300 阅读19分钟

前言

自我加入工作后,就一直在学习、使用 react。react 是流行的前端框架,极致简洁的 JSX 语法,深深的吸引着我,它像是一个好朋友,伴我同行,并肩作战。本文主要描述本人使用 react 进行开发,对状态管理以及数据流理解的心路历程。从组件状态、context、unstated-next 到 redux、mobx、rxjs、swr,作为自己这段时间的总结,希望大家能够有所收获。

历程

1. 初入 react,校场练兵,温柔乡

大学生时期的小陈,自学了前端技术,包括 html、css、js,还学习了 vue2.0 的基础知识,想着工作时候应该用得上。

刚加入工作的小陈,被告知公司前端主要使用的是 react,小陈心头一凉,“vue 不会白学了吧”。翻开 react 的官方文档,跟着教程做了做 demo,没想到2天的时间小陈就上手了 react(原来学习 vue 的经验并没有白费,很多概念、机制是相通的)。

2. 首秀之小试牛刀,心满意足

小陈自信心爆棚,开始从产品那里接一些小的需求,开发业务系统的前端,一开始只是显示一个列表,于是他放置了一个 antd 的 Table 组件,对着后端给出的 数据 API 文档,在页面组件 mount 生命周期那里,获取了的第一页数据并赋值给 list 状态,又加了个 page 状态,点击翻页按钮,改变 page 状态 并在 update 生命周期那里重新获取数据渲染。小陈自信地敲下 git commit、git push之后,就收工回家了。

const List = ()=>{
    const [listState, setList] = useState([]);
    const [page, setPage] = useState(0)
    const columns = [
        ...
    ]
    useEffect(()=>{
        getList(page).then(res=>setList(res));
    },[page]);
    
    return <>
        <Table dataSource={listState.list} columns={columns} />
        <Pagination page={page} pages={listState.pages} onChange={setPage} />
    </>
}

3. 警钟敲响,风雨将至

过了几天,产品又找到了之前做列表页面的小陈,说要加一个详情页,入口是在列表页面的每一项增加一个操作按钮,点击下钻到详情页。理解了需求,从后端那要到了数据接口,小陈马上动手,点击列表操作项,运用 react-router-dom,传入 id 参数进入详情页,加上新的路由,编写新的页面,获取明细数据,平铺展示,done。小陈没有意识到即将出现的问题,又自信地敲下 git commit、git push之后,开开心心收工回家了。

4. 敌人入侵,满城风雨

第二天,产品找上门来了,说道,”小陈啊,你这个列表页面有点问题,下钻详情页后,点击返回键回到列表页的时候,列表又回到了第一页,不方面逐个下钻查看,你修一下吧。“小陈心中咯噔一跳,出啥子问题了,连忙应了下来,马上修复。

小陈看了又看,百思不得其解,于是询问了旁边的前端大佬。大佬笑着说,”这不是很明显嘛,页面跳转的时候,组件卸载了,所以之前的状态没了,再一次返回列表页的时候,重置了状态,重新请求了数据,于是就形成了你看到的问题,你需要把状态变成全局的,这样它就不会被组件销毁啦,简单的问题,用 context 就可以解决了……不过……考虑到之后……“。

”好的,明白了,谢谢大佬。”小陈打断了大佬的话,没有继续听大佬说下去,回到了自己的工位上,赶紧翻开 react 文档,并快速找到了 context 的用法,并把它套在 App 上面。

<ListContext.Provider value={listState}>
  <App/>
</ListContext.Provider>

并在列表组件那里拿出context的值并使用它。

const List = ()=>{

const {list, page, setList, setPage} = useContext(ListContext);
    ... 
    return ...
}

好的,问题解决了,赶紧上线。

5. 内忧外患,祸不单行

后面的几个星期里,小陈风雨无阻地开发着,需求一直在进行着,小陈发现,自己越来越多地使用了 context,有各种原因,为了共享状态、跨多层级的组件通讯、状态保持…… 这些 context,有的套在 App 上面,有的套在独立 Page 上面,都解决了问题,但是,由于逻辑上移,导致 App 和 Page 的代码量激增,一个文件放了几千行代码,各种状态,穿插着各种各样的 useEffect、useMemo,还不带重名的,每次 render 的时间隐隐感觉变长了,甚至已经出现了卡顿的感觉。小陈望着屏幕,滚动着深不见底的代码文件,谨慎的改着每一行代码,工作效率越来越低,身心无比疲惫。

// App.tsx

const stateA = ...;
const stateB = ...;
const stateC = ...;
const stateD = ...;
// ... 
// ... 
// 大量逻辑

<ContextA.Provider value={stateA}>
  <ContextB.Provider value={stateB}>
    <ContextC.Provider value={stateC}>
      <ContextD.Provider value={stateD}>
        ...
          <AppContent />
        ...
      </ContextD.Provider>
    </ContextC.Provider>
  </ContextB.Provider>
</ContextA.Provider>

心中无比忐忑的小陈再一次走向了大佬的工位,让他看一看当前的形势。大佬皱了皱眉头,说:“一个 context 玩这么多花样,真亏你能写到现在。应用复杂起来了,状态逻辑已经超出 react 的极限了,你需要使用外部的状态管理库来处理这部分逻辑,接下来几天,你就不要接新的需求了,好好了解一下这方面的知识,重构一下之前的代码逻辑。”

6. 容器化 hooks,unstated-next

为了能够快速重构,并最大程度的复用到现有的代码,逻辑。小陈找到了 unstated-next 这个好帮手。

6.1 示例

unstated-next 使用核心方法的 createContainer ,传入一个自定义 hook,就能创建一个状态容器,使用 container.Provider 即可注入容器状态,底下的组件,即可使用 container.useContainer 获取容器内的共享状态。

import { createContainer } from "unstated-next";
import { useState } from "react";

const CounterContainer = createContainer((init: number = 0) => {
  return useState(init);
});

function Demo() {
  const [cnt, setCnt] = CounterContainer.useContainer();

  return (
    <div>
      <button>{cnt}</button>
      <button onClick={setCnt.bind(null, (a) => a + 1)}>inc</button>
    </div>
  );
}

function App(){
  // 两个 Demo 共享同一状态
  return <CounterContainer.Provider>
    <Demo />
    <Demo />
  </CounterContainer.Provider>
}

为了避免上面的 context 一样的 JSX 嵌套,也可以为容器写一个组合方法

import { Container } from "unstated-next";
const CombineContainer: FC<{ containers: Container<any, any>[] }> = ({
  containers,
  children
}) => {
  return (
    <>
      {containers
        .reverse()
        .reduce(
          (acc, con) => createElement(con.Provider, { children: acc }),
          children
        )}
    </>
  );
};

const App = ()=>{
  return <CombineContainer 
    containers={[
      containerA,
      containerB,
      containerC,
      containerD,
    ]}>
    <AppContent />
  </CombineContainer>
}

6.2 评价 unstated-next

优点

  1. 作为 context 状态管理的一种包装实现,没有额外的理解成本。
  2. 极其轻量,仅使用 hooks,上手成本低,简单易操作。

缺点

  1. 没有状态族(family)。
  2. 不同 container 相对独立,难以描述多个 container 间的状态依赖关系。

7. 军机大臣,redux

完成了一轮重构之后,小陈发现 react 社区的状态管理方案,还是以 redux 为主,为了让自己的项目代码更加贴合社区的主流,小陈还是决定学习、实践一波 redux。

打开 redux 的官网,紫色的底色中,小陈看到了专业的气质,其 github 50k 的 star 数给了小陈很强的安全感,说明社区十分认可 redux 的解决方案,安排了。

7.1 概念

在随后的学习之中,小陈接触到了 redux 当中的 state、reducer、action等概念。有以下核心概念:

  • 单一数据源,状态只保存在一个单一的状态树中,不会有状态复制同步的问题。
  • 状态是只读的,UI 组件只订阅状态,渲染视图,不会直接修改状态树。
  • 函数式更新,组件通过触发事件来让 redux store 执行 reducer 中的状态更新逻辑,不修改原对象,而是返回新的对象。

总结来看就是:state = reducer(state, action)。讲的就是 state 由 reducer 生成,也由 reducer 更新,action 代表通用的事件。事件到达时 state 会被 reducer 更新,通过这种方式不断进行 state 的迭代并推送给订阅者(或组件),视图只能通过 dispatch(action) 的方式向 redux store 请求更新 state,因此 state 只能按照 reducer 知道的方式进行更新,而不给视图随意操作 state 的可能,这就是读写分离。

可算是摸到了军机大臣的套路,该让他干活了。

7.2 示例

那啥,给我拿个数据呗。redux 感到莫名其妙:“懂不懂规矩,兵马未动粮草先行,先把 reducer 给我。”

const listReducer = (state = {list:[], page:0, pages:0}, action)=>{
    switch(action?.type){
        case 'LIST_LOADED':
            return {
              ...state,
              list:action.list,
              pages:action.pages
            }
    }
    return state;
}

行了,拿数据吧。redux:“那啥,钱是有了,但是征兵(异步流程)的事儿,总不能我亲自去干吧,你得把人送到我门口,我来给清点一下。你如果不想做的话,可以再招个官员去做这件事,不过恰好,我认识几个亲戚,它们是专业取数的,它们是 redux-saga、redux-thunk、redux-observable,你挑一个招吧,但是只能招一个,因为它们关系不太好,合不来”。

好家伙,原来军机大臣是个贪官,平时抠门也就算了,还拖家带口的。小陈心中升起了烦躁,就 redux-saga 吧。很快 redux-saga 就开设了自己的府邸,接受 redux 的指示(action),开始了招兵(取数)计划,不得不说,这小伙子手段丰富,一手 yield + 各种手段(effect creators) 专治各种不服,最后把人全都带到(put) redux 门前。redux 看着门前井然有序的队伍,满意地登记入册。

function* fetchList(action) {
   try {
      const user = yield call(Api.fetchList, action.page);
      yield put({type: "LIST_LOADED", user: user});
   } catch (e) {
      yield put({type: "LIST_FAILED", message: e.message});
   }
}

function* mySaga() {
  yield takeEvery("LIST_REQUESTED", fetchUser);
}

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  listReducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(mySaga)

小陈看数据都到位了,赶紧又加派了官员 react-redux 输送士兵(又一个亲戚)前往前线作战,将 context 的党羽替换下去。

const list = useSelect(s=>s);
const dispatch = useDispatch();

const setPage = (n) => dispatch({type: 'LIST_REQUESTED', page:list.page});
return <>
    <Table dataSource={listState.list} columns={columns} />
    <Pagination page={page} pages={listState.pages} onChange={setPage} />
</>

随后几个月,小陈都在与军机大臣 redux 打交道,发现他的风格就是这么谨小慎微,做事一丝不苟,从不露出任何破绽,管不过来了就找了儿子 combineReducers 分着管。小陈还优化了一下 redux 的办事效率,比如 RTK 用于简化 reducer 和 action creator。

7.3 评价 redux

a. 优点

  1. 代码规范,模板化的方式编写状态逻辑,逻辑清晰,不同人也可以写出一致的代码。
  2. 一致性、可预见性、易维护(得益于清晰的分层分离)。
  3. 工具支持好,有 redux-devtool 插件可以查看全局状态,并能回放应用,复现问题。

b. 缺点

  1. 开发效率低,样板需要慢慢写,最难的事情大概就是给 action 起名字。
  2. 性能优化问题需要工具和技巧。
  3. 文档太多太长,副作用领域的玩法太多了。

8. 骠骑大将军,mobx

在一次交际之中,小陈发现了勇士 mobx,它长得十分朴实、做事干净利落。小陈心想,这不就是我要找的良将吗。带上它正好可以压一压军机大臣 redux 的气焰,还不用浪费那么多军粮(模板代码)。

8.1 特点

mobx 与 redux 的风格完全不同,利用了对象属性访问拦截的特性,实现了响应式,通过执行代码访问属性,就可以自动注册副作用。

mobx 身披宝甲,坐镇军营(observer),新来的士兵(observable)一点都不敢偷懒,老老实实上阵地干活。

8.2 示例

// 定义可观察对象
// 全局的 observable
const global = observable({
  cnt: 0,
  inc() {
    this.cnt++;
  }
});

const Demo = observer(() => {
  // 局部的 observable
  const state = useLocalObservable(() => ({
    age: 18,
    name: "Mike"
  }));

  return (
    <div>
      <input
        value={state.name}
        // 直接对 observable 的属性赋值
        onChange={(e) => (state.name = e.target.value)}
      />
      <input
        type="number"
        value={state.age}
        onChange={(e) => (state.age = Number(e.target.value))}
      />
      <button onClick={global.inc.bind(global)}>{global.cnt}</button>
    </div>
  );
});

export default function App() {
  return (
    <div className="App">
      <Demo />
      <Demo />
    </div>
  );
}

得到了如下的结果,局部 state 是隔离的,global 是共享的。 结果

8.3 评价

优点

相比 redux,mobx 操作简洁,可以直接修改对象并获得高效精准的组件更新,易于性能调优。

缺点

  1. 理念 与 react 是相悖的,因此需要时刻谨记这一点,并在两种范式之间切换。
  2. 自由也是有隐患的,mobx 由于是直接操作可观察对象,因此状态更新操作没有强行收敛,当代码不规范时,难以排查问题。

9. 明枪易躲暗箭难防

有一天,后端同学找到小陈,说:“小陈,之前有一个接口,使用用户的 id 去请求用户的头像,你看看这个列表,一页 20 项,你发了 20 次请求了,这样不行,对接方可能会加限制,我做了一个批量传 id 获取头像 url 的接口,你把接口换一下,过几天原来的接口我也要下线了。” 小陈当时没有细思里面的逻辑,于是应了下来,记了个 Todo。

过了几天,小陈终于排到了这个问题。于是他找到之前的头像组件:

const OurAvatar = ({id}) => {
    const {value, loading, error} = 
        useAsync(()=> getAvatar(id), [id]);
    
    return loading ? <PlaceHolderAvatar loading/> :
           value ? <Avatar src={value.url} /> :
           err ? <Error /> 
               : <PlaceHolderAvatar />;
}

看着自己之前写的代码,小陈倍感欣慰,我写的代码,优秀~
于是就开始思考接口替换的事情:请求的逻辑是放在组件里面的,改成批量的…… 小陈细思极恐,难道说,不会吧,不会真的要把批量获取头像的逻辑弄到组件外面去,然后通过 props 下传吧。全局搜索过后,小陈发现这个组件已经被使用了几十次了,如果都要这么改,会在外面对应写上几十次批量查询逻辑…… 也没啥,就感觉头有点秃。

吃过之前像 context 那样上提状态的亏,小陈嗅到了一丝阴谋的气味,不敢再冒然动手了。找了 redux 和 mobx,得到的说法是这不属于全局状态,同时看起来也不像组件状态,也就是说,处理这些市井小混混,不在它们的业务范围内。小陈薅了薅稀疏的头发,觉得后端同学这波操作有点不厚道,一头倒在了工位上,真想撒手不管。目眩神迷之际,小陈看到了镖师 rxjs 的名片,原本灰暗的眼神中闪动着希望的光芒。

10. 金牌镖师,rxjs

在一次偶然的机会中,小陈碰到了 rxjs,其通红的身躯、高冷的气质在告诉自己,这个家伙不同寻常啊。

10.1 基本概念

rxjs 是基于发布订阅模式衍生出来的一套 ReactiveX API 的 js 实现版本。

其异步数据流十分灵活,大量内置的操作符能够大幅减少开发复杂数据流的思维负担,包括流生成、加工、分流、组合。

使用 rxjs 只需要关注 数据源、转换、订阅。

a. 数据源

  • 自定义 Observable,提供了固定逻辑的事件源
  • Subject,又称 EventEmitter,手动式的事件源
    • BehaviorSubject,必有一个值的 subject
    • ReplaySubject,带有一定 buffer 的 subject
  • creator operators,可以将从其他结构(数组,定时器,promise,dom event)中生成 Observable

b. 转换

即各种 operator,从源 observable 中获取元素,经过转换后交给下一个操作符 或者 observer。开启转换的方式是调用 observable.pipe。

自定义操作符,使用柯里化的方式创建的操作符,具有可复用性。

// 1. 使用组合操作符的方式
// 从流中经过map取出一个值,并做相邻去重
export const select = <T = any, R = any>(mapper?: (a: T) => R, deep = true) => (source: Observable<T>): Observable<R> =>
  source.pipe(map(mapper || _.identity), distinctUntilChanged(deep ? _.isEqual : _.eq));
// 2. 使用 new Observable 的方式
// 窗口缓存
export const batchTime = <T>(duration: number) => (observable: Observable<T>): Observable<T[]> =>
  new Observable(observer => {
    let buffer: any[] = [];
    let timer: any = null;

    const subscription = observable.subscribe({
      next(value) {
        buffer.push(value);
        if (!timer) {
          timer = setTimeout(() => {
            timer = null;
            observer.next(buffer);
            buffer = [];
          }, duration);
        }
      },
      error(err) {
        observer.error(err);
      },
      complete() {
        observer.complete();
      },
    });

    return () => {
      subscription.unsubscribe();
      clearTimeout(timer);
    };
  });

c. 订阅

有多种方式可以用于订阅

  • observable.subscribe(observer)
  • observable.toPromise()
  • observable.forEach(callback)

10.2 问题分析&解决

想到这里,小陈立即找到 rxjs,让它分析一下这个问题。“货物分散走位可能会有回不来的情况,集合出行又大动干戈。这样吧,把货分散地送到我这里来,我每隔一段时间为你批量打包集装箱发送一次,货物回来时,我再给你做分拣,不过你自己要让供应商把货物标记好”。

用 rxjs 包装了一个简单的闭包。
(batchTime 是自己实现的操作符,从源接收到数据后,开启一个新的缓存数组,缓存一段时间的元素,然后将数组交给下一个操作处理)

interface createBatchReqParam<P, P2, R, R2> {
  duration?: number;
  mergeParams(params: P[]): P2;
  fetcher(params: P2): Promise<R>;
  resolver(res: R, param: P, resolve: (res: R2) => void, reject: (err: any) => void): boolean | void;
}

// 创建批量请求,对外单独调用,按一定时间将参数合并请求,将返回结果分流
export const createBatchReq = <P, P2, R, R2>({
  duration = 100,
  mergeParams,
  fetcher,
  resolver,
}: createBatchReqParam<P, P2, R, R2>) => {
  const in$ = new Subject<{ param: P; resolve(res: any): void; reject(err: any): void }>();

  in$.pipe(batchTime(duration)).subscribe(async arr => {
    try {
      const res = await fetcher(mergeParams(_.map(arr, e => e.param)));
      arr.forEach(e => {
        resolver(res, e.param, e.resolve, e.reject);
      });
    } catch (err) {
      _.map(arr, item => item.reject(err));
    }
  });

  return (param: P) =>
    new Promise<R2>((res, rej) => {
      in$.next({ param, resolve: res, reject: rej });
    });
};

小陈提供了打包和分拣的逻辑。

const getAvatar = createBatchReq<string, { ids: string[] }, Avatar[], Avatar>({
  duration: 24,
  mergeParams(ids) {
    // 将缓存的数组打包成一个参数
    return { ids };
  },
  fetcher(params) {
    // 用 mergeParams 的结果发起批量请求
    return getBatchAvatars(params).then(res => res.data);
  },
  resolver(res, param, resolve, reject) {
    // 拆开响应,分拣:匹配 id,resolve 结果
    const item = res.find(e => e.id === param);
    if (!item) {
      reject(new Error('not found'));
      return;
    }
    resolve(item);
  },
});

像这样重新实现了 getAvatar,当 OurAvatar 批量挂载的时候,每 24ms 就会有一次批量请求,而 OurAvatar 组件完全没有改动到,nice。

10.3 杂谈

学习的过程中,得知 rxjs 竟能轻易实现 redux 的逻辑。rxjs 实现 redux (Youtube)

10.4 评价

优点

功能强大、灵活,对流的操作是其最大的亮点。

缺点

  1. 学习成本很高,操作符需要多训练方能熟练掌握。
  2. 目前并没有很好的与 react 的整合方案,因此只在一些小方法上利用其灵活的数据流操作简化代码。

11. 仓库管理员兼职采购员 swr

11.1 新的挑战

小陈在进一步的实践中,发现了 OurAvatar 每次挂载都会发请求,哪怕之前已经有一个相同 id 的头像在别的地方挂载过。其实对于 id 查头像这个接口来说,是十分稳定的结果,多次挂载是可以缓存复用之前的数据的。

11.2 方案

就在一筹莫展的时候,useSWR 引起了小陈的注意,它拥有一个很神奇的特质,只要给 useSWR 下好订单(key、fetcher),给个合适的过期时间(dedupingInterval),它那里有货就会直接给出,没货就帮你去拿货(fetch)并保管一段时间。太棒了,小陈心想这就是我要的专业人士,于是照着 useSWR 的规矩,尝试向它下了订单。

const OurAvatar = ({id}) => {
    const {data, isValidating, error} = 
        useSWR(
            ()=>['avatar', id],  // key 
            (_k,id)=> getAvatar(id),  // fetcher
            {
             dedupingInterval: 3600 * 1000 // 缓存时间
            });
    
    return data ? <Avatar src={data.url} /> :
           err ? <Error /> : 
               <PlaceHolderAvatar />;
}

11.3 深入了解

小陈对 useSWR 的这种做事模式十分感兴趣,于是继续向它请教,寻求更加深入的合作可能性。

const { data, error, isValidating, mutate } = 
    useSWR(key, fetcher, options)

其中参数有

  • key,用于缓存键
  • fetcher,获取数据的逻辑
  • options 用于控制状态缓存的行为,其中几个常用的参数
    • initialData,初始状态
    • dedupingInterval,缓存时间
    • isPaused,控制重新获取数据的开关
    • refreshInterval,定时触发 revalidate,重新 fetch 返回的接口有
  • data,当前的数据
  • error,错误信息
  • isValidating,是否在重新运行 fetcher 的过程中
  • mutate,命令式的修改 data

11.4 已知用法

a. 强制更新

调用 mutate 即可

const {data, mutate} = useSWR(key,fetcher,options);

mutate(); // 重新调用 fetcher,不应与 isPaused true 同时使用
mutate(newData, false) // 手动给出新状态
mutate(async(data)=>{ // 计算获得并更新
  ...
  return newData;
},false);

b. 全局状态

const cacheTime = 100000000_000; // 一个超长的缓存时间
function useGlobal() {
  const global = 
      useSWR(
          "global", // key,给状态起个名字
          () => 0,  // 直接返回初始值
          { dedupingInterval: cacheTime });
  return {
    data: global.data,
    // 通过封装 mutate 对外暴露 action creator
    inc: () => global.mutate((d) => d! + 1, false),
    dec: () => global.mutate((d) => d! - 1, false)
  };
}

c. 条件请求 & 状态依赖

这里利用了 key 的几种用法

  • key 可以是 string、数组、[返回string或数组的函数]
  • 函数返回 string 或者数组时正常请求
  • 函数返回 null 或者抛出错误时,不请求
useSWR(()=> null, fetcher, options); // 不请求

useSWR(()=> url , fetcher, options); // fetcher(url)

useSWR(()=> [a,b,c], fetcher, options); // fetcher(a, b, c), 数组会铺进 fetcher 调用中

d. memo

useSWR(()=>[...依赖的状态], ()=>{...同/异步计算逻辑}, options)

e. 附属状态(nice)

  • object 可以作为数组 key 的一部分,其内部会使用 WeakMap 生成 id
  • 实践中发现比较有用的玩法,附属状态 与 obj 相关,但是不能放在 obj 上面
  • 比如服务器返回了一个列表数据,每一项 obj 需要关联一个 UI 状态,如折叠面板组件的打开关闭状态
  • 可以就近管理 UI 状态,并全局缓存,当页面回退时,状态依旧有效
const other = useSWR(() => [obj], {
    initialData: false,
    isPaused(){return true},
}

return {
    state: other.data?.expand,
    toggle: ()=> other.mutate(d=>!d, false),
}

f. 无限滚动加载

useSWR 还有个兄弟叫 useSWRInfinite。可以在一次调用里面去做批量的请求缓存。

g. 名称空间很重要

使用 useSWR 时,最好直接使用函数 key 并返回加上前缀的数组,防止键冲突的发生。实际上,我使用了文件名 + 状态名的方式来管理(傻瓜操作)。

function useUserInfo(id){
    const userInfo = 
        useSWR(() => ['user/info', id],
            (_k,id) => fetchUser(id),
            options);
    return userInfo.data;
}

h. 封装&复用

前面的 useGlobal、useUserInfo 就是一个使用 useSWR 封装 hooks 的例子

  1. useGlobal 中 useSWR 的 key 是确定的,所以是一个全局唯一的状态,使用到的地方都共享了这个状态,调用 mutate 更新时,也都能够通知到所有用到它的组件去 rerender。
  2. useUserInfo 需要传递 id 参数,当传入 id 相同时,组件访问的就是同一个状态,当状态更新时,共享同一状态的组件都会 rerender。
// example
function useAvatar(id){
  return useUserInfo(id)?.avatarUrl;
}

const Avatar :FC<{id}> = ({id}) => {
  const url = useAvatar(id);
  return  url ? <RoundImg src={url} /> : <PlaceholderAvatar />
}

11.5 评价 swr

优点

  1. 声明式的缓存机制。
  2. 对比其他状态管理库,swr 很简洁,学习上手成本很低。
  3. 直接提供了 hooks 接口,贴合 react 的用法,这是面向未来的。
  4. 封装出自定义的 hooks,极易复用、再封装。
  5. 面向资源,无 context 注入,敏捷开发。

缺点

api 过于简洁,缺失了一些功能(生命周期等),以后应该会完善。如果发现有功能满足不了,也可以尝试 react-query。

总结

没想到在过去一年的时间里,学了那么多东西,随着自己对前端的状态交互模式理解的不断加深,技术也在一步步演化迭代,趋向成熟。这些技术都有存在的合理性,在不同的业务场景下,可能会有不同的解法,有些技术,已经不再被纳入我的选择当中,还是需要结合业务选择最适合自己团队的工具、技术栈。面对不断发展的技术,需要多积累、多跟进、多思考,面对未来的技术挑战才能更加从容自信。