您的 ”芝士“ 外卖 《React 18》 已送达。速来领取!!!

useTransition

背景介绍

在react18+版本之后新增了一些操作优先级相关的Api。

  • useTransition

    • 主要应用在hooks环境下使用
  • startTransition

    • 在非hooks环境下使用,比如 类组件中、状态库或者各种中间件环境中。

这两个api基于react18的并发模式而产生,可以让用户自行操作任务优先级而达到页面优化效果。

理论简述

其实在react18之前的版本在底层改成用链表的方式进行数据存储时,就支持了任务的优先级调度。但是当时并不支持用户手动调用优先级。它把任务分为以下几个阶段。

优先级等级 类型说明 示例
同步任务 必须立即执行且不能被打断,任务会直接在当前帧完成,不会参与调度机制。 用户事件(如点击按钮,直接触发 setState())、React 生命周期方法(如 componentDidMount)。
连续任务 与用户交互相关的持续性任务,使用并发特性可以被中断并延迟执行,优先级仅次于同步任务。 数据筛选、列表加载(通过 startTransition 触发的任务)。
空闲任务 等待主线程空闲时再执行,完全不紧急的任务,优先级低,可能会被推迟或跳过。 日志记录、预加载数据等后台任务。
默认任务 普通的异步任务,优先级低于同步任务,但高于空闲任务。 组件渲染更新、异步回调中的普通的setState 调用

比如你在执行一些高优先级任务的时候,希望其同样高优先级的任务延后执行,可以使用startTransition进行包裹。

这个时候使用startTransition包裹的任务优先级会被标记为连续任务放到同步任务后面执行。

  • 💡这里有个情况大家需要清楚js线程阻塞和react任务调度优先级是两回事。
  • 🌰举个例子。在你的任务被标记为低优先级的时候,如果里面的同步任务一旦开始执行也是一直占用主线程直到执行完成,这个过程是不能被打断的。
  • 也就是说只有在任务真正放到主线程执行之前,才有可能被react的调度优先级打断。

实际应用

  • 首先你需要一个react 18+的环境并且使用createRoot创建应用

1、我们创建一个输入框在输入的同时做大量的计算任务进行环境模拟

jsx 复制代码
function DropdownSearch() {
  const [query, setQuery] = useState('');
  const [filteredOptions, setFilteredOptions] = useState([]);
  const [isPending, startTransition] = useTransition();
​
  // 模拟一个庞大的下拉选项列表
  const options = useMemo(() => {
    return Array.from({ length: 50000 }, (_, i) => `Option ${i + 1}`);
  }, []);
​
  // 处理输入变更
  const handleInputChange = (e) => {
    const value = e.target.value;
    setQuery(value); // 高优先级更新:维护输入框内容的即时更新
​
    // 使用 startTransition 标记低优先级更新
    startTransition(() => {
      const list = options.filter((item) =>
        item.includes(value)
      );
      setFilteredOptions(list);
    });
  };
​
  return (
    <div style={{ padding: '20px', width: '400px', margin: '0 auto' }}>
      {/* 搜索框 */}
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
        style={{
          width: '200px',
          marginBottom: '20px',
        }}
      />
      {/* 我在这里监测低任务是否在执行 & 可以展示自己想要的ui*/}
      <p>{isPending ? '开始低级任务' : '暂无低级任务'}</p>
      <div>
        {filteredOptions.length > 0 ? (
          <ul>
            {filteredOptions.map((option, index) => (
              <li key={index} style={{ padding: '4px 0' }}>
                {option}
              </li>
            ))}
          </ul>
        ) : (
          <p>无内容</p>
        )}
      </div>
    </div>
  );
}
  • 未使用startTransition降低优先级

输入的时候对应的同步优先级的筛选任务也在执行,可以看出明显的输入卡顿

  • 使用startTransition降低优先级

    把筛选任务的优先级标记为低优先级任务之后,不会影响输入框的输入。

    当高优先级任务执行完成之后,开始执行筛选任务。、

在react18中提到的并发渲染模式的应用

jsx 复制代码
const [isPending, startTransition] = useTransition();

在这里我们使用startTransition进行任务标记之后,可以根据isPending的状态判断任务是否执行完成。

jsx 复制代码
 {/* 我在这里监测低任务是否在执行 & 可以展示自己想要的ui*/}
<p>{isPending ? '开始低级任务' : '暂无低级任务'}</p>
​

这种能力在react18之前实现还是比较麻烦的,通常我们只能在请求服务的时候进行ui同步展示或者是依托Suspense的能力展示一些ui。

那这个功能可以非常细粒化的让我们控制在任务执行阶段,进行ui的同步展示还是比较实用的。

use

简单介绍

1、react 18中新增了use,并且背后也依赖了最新的并发模式。

2、可以用来读取 fullfilled Promise或者context的值,读取context值的时候是可以放到循环语句或者条件语句中进行读取。

有一个点需要注意调用use的地方一定是一个hooks或者一个组件函数。

3、同时结合了 Suspense,在use中的请求挂起时可以显示后备ui fallback的内容。

4、在请求异常时,use可以结合ErrorBoundary 进行错误处理,显示fallback内容。

5、这个api的推出主要应用于 Server Components

  • 💡 React 18 Server Components的use API允许直接从服务端获取数据,并在服务端完成渲染后将HTML返回给客户端。
结合context
jsx 复制代码
const ThemeContext = createContext('');
​
function MyPage() {
  return (
    <ThemeContext.Provider value="hello world">
      <Form show={true}/>
    </ThemeContext.Provider>
  );
}
​
function Form({show}) {

  if (show){
    //可以在这里的条件中读取context
    //这在实际场景中用的情况还是蛮多的
    const theme = use(ThemeContext);
    return <div>value:{theme}</div>;
  }
  return <div>value:{'null'}</div>;
}
​
function App() {
  return (
    <div className="App">
      <Suspense fallback={<div>Loading...</div>}>
        <MyPage />
      </Suspense>
    </div>
  );
}

注意这里在使用use读取context时,查找逻辑和useContext是一致的,都是读取该组件最近的 context provider。

在next中use结合promise

这样在服务端就把html与数据装载好并返回给客户端,有很多优势如下

  • 提高首屏加载速度
  • 优化seo搜索
  • 减少客户端构建压力
jsx 复制代码
// 模拟一个延迟的请求函数
async function fetchDataWithDelay() {
  await new Promise((resolve) => setTimeout(resolve, 2000)); // 模拟2秒延迟
  const response = await fetch("http://localhost:3000/api/example");
  return await response.json();
}
​
​
function Todo() {
  // 使用use读取数据
  const data = use(fetchDataWithDelay());
  return (
    <div>
      <h2>Todo:</h2>
      <p>{data.title}</p>
      <ul>
        {data.data.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}
​
// 包含 Suspense 的主页面
export default function SuspensePage() {
  return (
    <div>
      <h1>Suspense Example in Next.js</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Todo />
      </Suspense>
    </div>
  );
}
在客户端中use结合promise

use用来结合promise请求数据可以省一部分代码。在预加载组件中非常实用!

  • 使用use之前我们的操作一般如下👇
jsx 复制代码
function MessageBefore({fetchThemePromise}) {
​
  const [theme, setTheme] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
​
  useEffect(() => {
    const fn = async () =>{
      try {
        setIsLoading(true);//记录请求挂起状态
        const theme = await fetchThemePromise; //请求数据
        setTheme(theme);
      } catch(err) {}
      finally {
        setIsLoading(false);
      }
    }
    fn()
  }, [])
​
  if (isLoading) {
    return <>
      Loading...
    </>
  }
​
  return <>
    await value:{theme}
  </>
}
​
function App() {
  const fetchThemePromise = updateName('The world', 1000);
​
  return (
    <div className="App">
        <p><MessageBefore fetchThemePromise={fetchThemePromise}/></p>
    </div>
  );
}
  • 使用use之后是下面这样子,结合Suspense可以在请求时将后备ui展示。
jsx 复制代码
function MessageAfter({fetchThemePromise}) {
  const theme = use(fetchThemePromise);
​
  return <>
    use value:{theme}
  </>
}
​
function App() {
  const fetchThemePromise = updateName('The world', 1000);
​
  return (
    <div className="App">
      <Suspense fallback={<div>Loading...</div>}>
        <p><MessageAfter fetchThemePromise={fetchThemePromise}/></p>
      </Suspense>
    </div>
  );
}

💡 注意! 这里并不是说use在请求数据的时候能完全代替useEffect,因为像最开始所说只是在预加载的时候比较实用。普通场景下我们的数据加载还是需要等待dom挂载完毕去请求数据才是正常的操作!

解决了什么问题?

不知道大家有没有思考过在这种预加载组件中可以在组件内部使用await来实现效果,类似下面这样。

jsx 复制代码
async function MessageAfter({fetchThemePromise}) {
  // const theme = use(fetchThemePromise);
  const theme = await fetchThemePromise
​
  return <>
    use value:{theme}
  </>
}
​
function App() {
  const fetchThemePromise = updateName('The world', 1000);
​
  return (
    <div className="App">
      <Suspense fallback={<div>Loading...</div>}>
        <p><MessageAfter fetchThemePromise={fetchThemePromise}/></p>
      </Suspense>
    </div>
  );
}

从页面反馈效果上看起来好像是一样的但是这里有两个问题

  • 1、在react中返回的组件必须是同步的jsx,这样在内部协调的时候才不会出错,如果返回一个 async 函数,页面上会报一个异常
  • 2、使用await看起来可能效果比较相似,但是要注意的是await是同步的也就是说,这样会变成串行执行,而不是并行这样就失去了预加载组件的意义
jsx 复制代码
async function MessageAfter({fetchThemePromise}) {

  const theme = await fetchThemePromise1 
  const theme = await fetchThemePromise2 //这里的执行会取决于上面的await
​
  return <>
    use value:{theme}
  </>
}

useActionState

用法简述

1、接受一个异步函数,提供一套api对函数加载中、错误情况、返回数据等进行处理。

2、这个api和ahooks中的use Request还是比较相似的

3、基本用法,代码如下

jsx 复制代码
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      return null;
    },
    null,
  );
​
  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}
    ​

💡 在早期的 React Canary 版本中,此 API 是 React DOM 的一部分,称为 useFormState

解决了什么问题

现在来看两组实现同样效果逻辑的代码!

  • 示例1
jsx 复制代码
const TodoApp = () => {
  // 状态定义
  const [loading, setLoading] = useState(false);     // 加载中状态
  const [error, setError] = useState(null);         // 错误状态
  const [todos, setTodos] = useState([]);           // Todo 列表
  const [todoInput, setTodoInput] = useState("");   // 用户输入值
​
  // 模拟异步操作:添加新 Todo 的逻辑
  const addTodo = async (newTodo) => {
    setLoading(true);       // 开启加载状态
    await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟延迟任务
​
    if (Math.random() < 0.5) {
      setError("哎呀 出错了"); // 捕获并设置错误信息
    } else {
      // 成功添加 Todo
      setTodos((prevTodos) => [...prevTodos, newTodo]);
    }
    setLoading(false);      // 结束加载状态
  };
​
  // 处理表单提交
  const handleAddTodo = async (e) => {
    e.preventDefault(); // 阻止表单默认行为(页面刷新)
    await addTodo(todoInput); // 异步添加 Todo
  };
​
  return (
    <div style={{ padding: "20px" }}>
      <h1>Todo List</h1>
​
      {/* 显示错误信息 */}
      {error && <p style={{ color: "red" }}>{error}</p>}
​
      {/* 显示加载中状态 */}
      {loading && <p style={{ color: "blue" }}>Loading...</p>}
​
      {/* 表单部分:输入框 + 添加按钮 */}
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          value={todoInput}
          onChange={(e) => setTodoInput(e.target.value)}
          placeholder="Enter a todo item"
          disabled={loading} // 加载时禁用输入框
          style={{ marginRight: "10px" }}
        />
        <button type="submit" disabled={loading}>
          {loading ? "Adding..." : "Add Todo"}
        </button>
      </form>
​
      {/* 渲染 Todo 列表 */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

在上述例子中我们使用三个状态对数据集、错误结果、和加载中进行维护。

其实这种模版代码在现实场景中非常多,也比较繁琐。

  • 示例2
jsx 复制代码
const TodoApp2 = () => {
  // 初始化 Todos 数据
  const [todos, setTodos] = useState([]); // 用于记录 Todo 列表的状态
​
  // 使用 useActionState 实现错误、加载中以及提交逻辑封装
  const [error, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟异步任务
      const newTodo = formData.get('todo');

      if (Math.random() < 0.5) {
        return "哎呀 出错了";
      }

      setTodos((prevTodos) => [...prevTodos, newTodo]); // 更新 todos 列表状态
      return null; // 成功时返回 null
    },
    null // 初始错误状态为 null
  );
​
  return (
    <div style={{ padding: "20px" }}>
      <h1>Todo List</h1>
​
      {/* 显示错误信息 */}
      {error && <p style={{ color: "red" }}>{error}</p>}
​
      {/* 使用 form 和 action */}
      <form action={submitAction}>
        <input
          type="text"
          name="todo"
          placeholder="Enter new todo"
          defaultValue={`Todo ${todos.length + 1}`}
          disabled={isPending}
        />
        <button type="submit" disabled={isPending}>
          {isPending ? "Adding..." : "Add Todo"}
        </button>
      </form>
​
      {/* 渲染 Todo 列表 */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

现在我们可以直接用useActionState来维护这三个状态,减少了很多维护成本。

看下前后两个操作对比

  • 示例1 中错误处理如果用户不手动处理的话即使后面添加成功了也一直会显示在页面中,并不会自动处理
  • 示例2 中使用useActionState维护状态之后,当逻辑正常时会自动重置error状态

useFormStatus

解决了哪些问题?
  • 消除重复的state管理,比如提交中、提交失败、重置等操作都需要state来进行维护,比较繁琐
  • 解耦ui与业务组件,比如要在输入框下面同步loading或者错误状态,以前维护起来比较复杂
例子1
jsx 复制代码
function MyForm() {
  // 1. 自己维护提交状态
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
​
  const [data, setData] = useState(null);
  async function handleSubmit(e) {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);
    // 模拟提交
    const res = await fakeAPICall();
    setData(res);
    setIsSubmitting(false);
  }
​
  return (
    <form onSubmit={handleSubmit}>
      <input name="foo" />
      {/* 2. 按钮要用到 isSubmitting 状态 */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '提交'}
      </button>
      {/* 3. 错误提示也要跟着 isSubmitting/update */}
      {error && <div>{error}</div>}
      {data && <div>{data}</div>}
    </form>
  );
}
用法解析
  • useFormStatus可以在<form>内部获取状态信息,展示相关的状态比如loading和错误状态等
  • 可以使用从 useFormStatus 返回的状态信息中的 data 属性来显示用户正在提交的数据是什么
注意💡
  • useFormStatus 不会返回在同一组件中渲染的 <form> 的状态信息

    • useFormStatus Hook 只会返回父级 <form> 的状态信息,而不会返回在调用 Hook 的同一组件中渲染的任何 <form> 的状态信息,也不会返回子组件的状态信息。
  • 正确的做法是从位于 <form> 内部的组件中调用 useFormStatus,看下面的例子

例子2
jsx 复制代码
function UsernameForm() {
  const {pending, data} = useFormStatus();
  console.log({
    ...useFormStatus(),
    username: data?.get('username')
  }, 'useFormStatus')
  return (
    <div>
      <h3>请求用户名:</h3>
      <input type="text" name="username" disabled={pending}/>
      <button type="submit" disabled={pending}>
        提交  
      </button>
      <button type="reset" disabled={pending}>
        重置  
      </button>
      <br />
      <p>{data ? `请求 ${data?.get("username")}...`: ''}</p>
    </div>
  );
}
​
function App() {
  return (
    <div className="App">
      <form action={async (formData) => {
        await updateName(formData, 1000);
      }}>
      <UsernameForm />
    </form>
    </div>
  );
}
打印日志
  • 可以看到在提交时候pending是true的情况下是可以拿到相关数据的

useOptimistic

简述
  • 在异步操作期间展示你想要展示的内容,从而提升用户体验
  • useOptimistic第一个参数是初始值,第二个参数是更新函数👇
  • 在更新函数中接受一个新参数返回更新后的数据
jsx 复制代码
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
  messages,
  (state, newMessage) => {
    console.log(state, newMessage, 'useOptimistic')
    return [
      {
        text: newMessage,
        sending: true
      },
      ...state,
    ]
  }
);
场景
  • 比如在表单提交过程中,服务真正响应之前可以提前展示输入的内容
  • 还有一些点赞的功能操作可以让用户看起来是即时响应的

看下面的例子

jsx 复制代码
function Thread({ messages, sendMessageAction }) {
  const formRef = useRef();
  function formAction(formData) {
    //1、在实际的请求回来之前可以展示一些我们想要展示的内容
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    //2、实际发起的请求
    startTransition(async () => {
      //降低更新优先级
      await sendMessageAction(formData);
    });
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => {
      console.log(state, newMessage, 'useOptimistic')
      return [
        {
          text: newMessage,
          sending: true
        },
        ...state,
      ]
    }
  );
​
  return (
    <>
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="你好!" />
        <button type="submit">发送</button>
      </form>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small>(发送中......)</small>}
        </div>
      ))}

    </>
  );
}
​
function App() {
  const [messages, setMessages] = useState([
    { text: "你好,在这儿!", sending: false, key: 1 }
  ]);
  async function sendMessageAction(formData) {
    const sentMessage = await updateName(formData.get("message"), 2000);
    startTransition(() => {
      setMessages((messages) => [{ text: sentMessage }, ...messages]);
    })
  }
  return <Thread 
    messages={messages} 
    sendMessageAction={sendMessageAction} 
  />;
}
    ​
demo演示

ref

  • 废弃forwardRef直接从props传递
  • 并且可以直接传多个ref
jsx 复制代码
const TestRef = (ref) => {
  console.log(ref, 'ref')
  return <div>TestRef</div>
}
​
function App() {
  const appRef = useRef(null);
  const appRef1 = useRef(null);
​
  return <TestRef  
    ref={appRef}
    ref1={appRef1}
  />;
}
  • 打印ref对象就可以拿到全部ref了

useDeferredValue

简述

一个用于优化用户界面性能的Hook,可以降低任务处理优先级,使用户体验更流畅。

本质上就是将一些计算量大或者已知优先级比较低的任务都可以使用useDeferredValue来更新延迟处理。

常见场景
  • 举一个实际场景的例子,比如在用户输入的过程中要更新一段list
  • 这里会有个问题如果在输入的过程中list也一直更新的情况下就会阻断输入的流畅性
  • 那我们可以使用useDeferredValue将list内容进行包裹降低优先级

看下面这个例子

jsx 复制代码
function Deferred() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}
​
function NoDeferred() {
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={text} />
    </>
  );
}
​
​
function App() {
​
  return <div style={{display: 'flex' }}>
  {/* 使用 useDeferredValue 来延迟渲染 */}
    <div>
      <p>延迟列表重新渲染</p>
      <Deferred />
    </div>
    <div>
      <p>未使用 useDeferredValue 的列表</p>
      <NoDeferred />
    </div>
  </div>
}

效果演示

  • 从效果中我们可以看到使用useDeferredValue延迟任务的可以不阻塞输入框的流畅性

  • 而没有使用优化手段的在渲染list的时候会阻塞输入框的输入

延迟一个值与防抖有什么不同?
  • 防抖 是指在用户停止输入一段时间(例如一秒钟)之后再更新列表。

useDeferredValue适合在渲染中优化因为这个api本身和react深度集成,并且能够适应设备的性能,无需选择固定延迟时间。

并且useDeferredValue也是支持渲染可中断的,而防抖和要生效的场景时在渲染期间之外的,比如减少鼠标点击次数。减少网络事件请求等。

参考文献:zh-hans.react.dev/blog/2024/1...

相关推荐
去旅行、在路上6 分钟前
chrome使用手机调试触屏web
前端·chrome
Aphasia31135 分钟前
模式验证库——zod
前端·react.js
lexiangqicheng1 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
拉不动的猪2 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js
烛阴2 小时前
Python枚举类Enum超详细入门与进阶全攻略
前端·python
孟孟~2 小时前
npm run dev 报错:Error: error:0308010C:digital envelope routines::unsupported
前端·npm·node.js
孟孟~2 小时前
npm install 报错:npm error: ...node_modules\deasync npm error command failed
前端·npm·node.js
狂炫一碗大米饭2 小时前
一文打通TypeScript 泛型
前端·javascript·typescript
wh_xia_jun2 小时前
在 Spring Boot 中使用 JSP
java·前端·spring boot
二十雨辰3 小时前
[HTML5]快速掌握canvas
前端·html