likes
comments
collection
share

优化React项目中的异步数据处理、提升性能与用户体验

作者站长头像
站长
· 阅读数 15

引言

最近和@Bolt ᶘ ᵒᴥᵒᶅ 聊到一些React项目中异步数据的处理优化,结合着搜了一些资料,有些收获,予以记录。

问题

  1. 对于接口数据,即使使用 类似 swr 的工具,我们也有很多的状态要写在逻辑层进行处理,这种工作非常无趣
import useSWR from "swr";
function App() {
  const fetcher = (...args) => fetch(...args).then((res) => res.json());

  // fetch data
  const { data, error } = useSWR(
    "https://jsonplaceholder.typicode.com/todos/1",
    fetcher
  );

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;

  // render data
  return (
    <div className="App">
      <h2>{data.title}</h2>
      <p>UserId: {data.userId}</p>
      <p>Id: {data.id}</p>
      {data.completed? <p>Completed</p> : <p>Not Completed</p>}
    </div>
  );
}

export default App;
  1. 有些接口数据之间存在依赖关系,有些并没有,但业务中我们常常是集中处理接口情况,会造成不必要的等待,无法实现完美的按需 loading 提示。 「懒得写例子了」
useEffect(() => {
	Promise.all([a, b, c])
	...
})

只消费 a

消费 b、c

方案

新React hook: use

依赖注意

  "dependencies": {
    "react": "experimental",
    "react-dom": "experimental"
  },
import { Suspense, useMemo, use } from "react"

function Comp({ data: prosData }) {
  const data = use(prosData)
  return <div>{JSON.stringify(data)}</div>
}

const mockApi = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "John",
      })
    }, 3000)
  })

function App() {
  const promiseData = useMemo(() => mockApi(), [])

  return (
    <Suspense fallback={<div>loading ++</div>}>
      <Comp data={promiseData} />
    </Suspense>
  )
}

export default App

行为描述: use(prosData) 会告知 最近的 Suspense「A」 数据情况,若处于 pending 状态,则走 A 的 fallback 行为 无视常规 hook 要求的规则,如不能 condition

  • 缺点
    • 处于 experimental 阶段
  • 优点
    • React 官方支持,用起来非常爽

Router Await

限定下问题:穿入数据为 Promise,自行会 loading 我们不难写出如下通用组件来完成

export function Await(props: AwaitProps) {
  const { children, resolve } = props
  const [resolved, setResolved] = useState(false)
  const [res, setRes] = useState(null)
  useEffect(() => {
    if (resolve instanceof Promise) {
      resolve.then((res) => {
        setResolved(true)
        setRes(res)
      })
    } else {
      setResolved(true)
    }
  }, [resolve])

  if (!resolved) {
    return <div>loading</div>
  }
  return children(res)
}


// 用法
          <Await resolve={comp1Data}>
            {(res) => {
              return <Comp data={res} />
            }}
          </Await>

大致是这样的思路,React Router 已经干的很好了

import { Await, useLoaderData } from "react-router-dom";

function Book() {
  const { book, reviews } = useLoaderData();
  return (
    <div>
      <h1>{book.title}</h1>
      <p>{book.description}</p>
      <React.Suspense fallback={<ReviewsSkeleton />}>
        <Await
          resolve={reviews}
          errorElement={
            <div>Could not load reviews 😬</div>
          }
          children={(resolvedReviews) => (
            <Reviews items={resolvedReviews} />
          )}
        />
      </React.Suspense>
    </div>
  );
}

declare function Await(
  props: AwaitProps
): React.ReactElement;

interface AwaitProps {
  children: React.ReactNode | AwaitResolveRenderFunction;
  errorElement?: React.ReactNode;
  resolve: TrackedPromise | any;
}

interface AwaitResolveRenderFunction {
  (data: Awaited<any>): React.ReactElement;
}

Note: <Await> expects to be rendered inside of a <React.Suspense> or <React.SuspenseList> parent to enable the fallback UI.

和自己实现的弱鸡版本相比,React Router 的 Await 可以利用 React 的 Suspense

你可能会好奇,咋做到通知 Suspense 的呢?

对于这个问题,推荐看看这篇文章 我摘要部分

  • Suspense: 可以看做是 react 提供用了加载数据的一个标准,当加载到某个组件时,如果该组件本身或者组件需要的数据是未知的,需要动态加载,此时就可以使用 Suspense。Suspense 提供了加载 -> 过渡 -> 完成后切换这样一个标准的业务流程。
  • lazy: lazy 是在 Suspense 的标准下,实现的一个动态加载的组件的工具方法。

与 lazy 同理 React Router 中的 Await 实现遵循了 Suspense 加载数据的标准,利用在 pending 状态下,向外 throw promise 完成对 Suspense 的告知

// 模拟请求 promise
function mockApi(){
  return delay(5000).then(() => "data fetched")
}

// 处理请求状态变更
function fetchData(){
  let status = "uninit"
  let data = null
  let promise = null
  return () => {
    switch(status){
      // 初始状态,发出请求并抛出 promise
      case "uninit": {
        const p = mockApi()
          .then(x => {
            status = "resolved"
            data = x
          })
          status = "loading"
          promise = p
        throw promise
      };
      // 加载状态,直接抛出 promise
      case "loading": throw promise;
      // 如果加载完成直接返回数据
      case "resolved": return data;
      default: break;
    }
  }
}

const reader = fetchData()

function TestDataLoad(){
  const data = reader()
  return (
    <p>{data}</p>
  )
}

function App() {
  const [count, setCount] = useState(1)
  useEffect(() => {
    setInterval(() => setCount(c => c > 100 ? c: c + 1), 1000)
  }, [])
  return (
     <>
        <Suspense fallback={"loading"}>
          <TestDataLoad/>
        </Suspense>
        <p>count: {count}</p>
     </>
  )
}
  • 优点
    • 现在就能用上
  • 缺点
    • 还是没有 use 用起来优雅

总结

业务使用中,可以把业务数据请求结果抽到合理的上层,向下传递 Promise 合理放置 Suspense,减少不必要的 rerender

其他

2021年12月9日 React 18 Keynote 就提过本文讲的内容 因为 use 这个在官方文档还找不到,推荐去 rfcs 里看下出处