useDeferredValue 和 useTransition

干嘛用的

useDeferred的注释了写明了使用路径 用户输入获取数据

加上官网也写了useDeferredValue是一个和Suspense结合使用的方法。

Suspense

我们可以先回顾一下<Suspense>如何使用的

父组件

javascript 复制代码
const Demo = () => {
  const [value, setValue] = useState("");
  return (
    <>
      <input
        value={value}
        onChange={(v) => {
          setValue(v.target.value);
        }}
      />
      <Suspense fallback={<>loading</>}>
        <SearchResults value={value} />
      </Suspense>
    </>
  );
};

子组件

javascript 复制代码
function SearchResults(props: { value: string }) {
  const random = use(fetchData(props.value));
  return <div>{random + "  " + props.value}</div>;
}

use钩子

use钩子接受一个promise对象,在本文中promise对象来自下面的fetchData方法。在use中,我们会为这个promise对象增加三个属性,这三个属性分别就对应了promise本身的状态。当我们的promise的状态不为fulfilled时,就向外throw promise或者error,来触发外层的suspense渲染fallback。当promise的状态为fulfilled时,则返回promise实际的value。

typescript 复制代码
function use(
  promise: Promise<unknown> & {
    status: string;
    value: unknown;
    reason: unknown;
  }
) {
  if (promise.status === "fulfilled") {
    return promise.value;
  } else if (promise.status === "rejected") {
    throw promise.reason;
  } else if (promise.status === "pending") {
    throw promise;
  } else {
    promise.status = "pending";
    promise.then(
      (result) => {
        promise.status = "fulfilled";
        promise.value = result;
      },
      (reason) => {
        promise.status = "rejected";
        promise.reason = reason;
      }
    );
    throw promise;
  }
}

fetchData方法

我们通过延迟三秒来模拟接口返回的情况,在实际的业务场景中,就是我们请求数据的方法

typescript 复制代码
const map = new Map();
const fetchData = (str: string) => {
  if (!map.has(str)) {
    map.set(
      str,
      new Promise((resolve) => setTimeout(() => resolve(Math.random()), 3000))
    );
  }
  return map.get(str);
};

简单来说,我们通过use钩子对请求方法返回的promise进行二次包装,目的就是为了当promise处于未完成的状态时,保证<Suspense>渲染fallback。<Suspense>这个组件本身会在其children第一次throw promise时,为这个promise注册一个回调,在promise完成时重新渲染children

知道了Suspense怎么用,我们再看看如何结合deferredValue。

useDeferredValue

其实非常简单,我们修改一下父组件看看效果。

ini 复制代码
const Deferred = () => {
  const [value, setValue] = useState("");
  const deferred = useDeferredValue(value);
  return (
    <>
      <input
        value={value}
        onChange={(v) => {
          setValue(v.target.value);
        }}
      />
      <Suspense fallback={<>fallback</>}>
        <SearchResults value={deferred} />
      </Suspense>
    </>
  );
};

随着我们的输入,不断发起请求,然后进入到fallBack中

可以看到随着用户输入不再会渲染Suspense的fallback了。可以理解为用户的输入的优先级较高,因此input中的onChange会高优先级执行,导致了函数组件的重新执行,但是deferred由于被useDeferredValue包裹,因此会延迟更新,通过下面的动图可以看到每次输入都会引起组件的更新,但是并不会引起deferred的值的更新。当我们停止输入后最终deferredvalue的值同步了。

这时候会发现,把Suspense去掉也是一样的效果。其实是react帮我们兜底了,大家可以试一下替换SearchResults。当我们向外throw promise时,react也会帮我们进行处理,这个行为与Suspense无关

javascript 复制代码
let a = true;
export default function SearchResults(props: { value: string }) {
  // const random = use(fetchData(props.value));
  if (a) {
    throw new Promise((res) =>
      setTimeout(() => {
        a = false;
        res(1999);
      }, 5000)
    );
  }

  return <div>{"random" + "  " + props.value}</div>;
}

useTransition

useTransition则不需要传递参数,返回[isPending, setTransition]其中setTransition使用时需要传递一个函数,这个函数会以低优先级的方式执行,并且具有合并执行的特效(多次调用只生效一次)。乍一听和useDeferredValue类似。

实际使用时setTransition则更加灵活。因为我们在使用useDeferredValue时,react帮我们决定了何时为这个deferred值附新值,但是我们可以通过setTransition来直接控制何时进行低优先级操作。而useDeferredValue的使用场景更多的是原值在组件中不可控,比如说我们上面的例子中子组件接受props,把props通过useDeferredValue进行包裹,因为props的控制权不在子组件中。

同样的,受控组件的onchange也最好使用useState的set方法。

ini 复制代码
const Transition = () => {
  const [value, setValue] = useState("");
  const [transedValue, setTransedValue] = useState("");
  const [, setTransition] = useTransition();
  return (
    <>
      <input
        value={value}
        onChange={(v) => {
          setValue(v.target.value);
          setTransition(() => {
            setTransedValue(v.target.value);
          });
        }}
      />
      <Suspense fallback={<>fallback</>}>
        <SearchResults value={transedValue} />
      </Suspense>
    </>
  );
};

上面这段代码和使用useDeferredValue时的效果应该是一样的

接下来再看一下useTransition的批处理特性。

ini 复制代码
const Transition = () => {
  const [value, setValue] = useState("");
  const [transedValue, setTransedValue] = useState("");
  const [, setTransition] = useTransition();
  return (
    <>
      <input
        value={value}
        onChange={(v) => {
          setTransition(() => {
            setValue(v.target.value);
            setTransedValue(v.target.value);
          });
        }}
      />
      <Suspense fallback={<>fallback</>}>
        <SearchResults value={transedValue} />
      </Suspense>
    </>
  );
};

可以看到我们不停的输入,但是由于批处理,很多输入被忽略了。这也是为什么受控组件不适合useTransition的原因。但是如果我们在setTransition中增加一些输出,会发现输出并没有被合并,因为函数其实是照常执行的,只是如果在执行时有高优先级的任务,那么会被高优先级的任务中断。

相关链接:react.dev/reference/r... react.dev/reference/r...

相关推荐
AlienZHOU10 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc13 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师14 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen15 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒15 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow16 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81816 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007816 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明16 小时前
第 6 章 组件进阶
前端·vue.js
江华森16 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端