【React】React 19 新特性

use

  • 作用: 帮你获取 Context 或者 Promise 的值

1、获取 Context

替代 useContext

ts 复制代码
import { createContext, use } from 'react';

const ThemeContext = createContext('light');

// 省去 Provider 的逻辑

function Button() {
  // 不需要使用 useContext,直接用 use
  const theme = use(ThemeContext);
  // 原先是
  const theme = useContext(ThemeContext)
  return <button className={theme}>按钮</button>;
}

2、获取 Promise

本质是替代 useEffect 和 useStata,但是也有限制,比如直接获取使用 use 获取 Promise 会报错, 比如

ts 复制代码
// api.ts
export function fetchData(shouldFail = false) {
  return new Promise((resolve, reject) => {
      if (shouldFail) {
        reject(mockData.error);
      } else {
        resolve(mockData.success);
      }
  });
} 
ts 复制代码
import { Suspense,use } from 'react';
import { fetchData } from './api'

export function App(){
  const dataPromise = use(fetchData())
  
}

我原本以为也是这么用得,发现包这个错误

这是因为客户端组件不支持直接使用 async/await 或 Promise,React 19 区分了服务端组件和客户端组件的能力。use 只能获取已经 resolve 的值,对于是 Pending 的状态会报错,所以我们要结合 Suspense 来使用。

在不用 Suspense 前,我们可以等 Promise 请求结束后使用 use

ts 复制代码
 let [loading, setLoading] = useState(true);

  const promise = useRef(
    fetchData().then((res) => {
      setLoading(false);
      return res;
    })
  );

  if (!loading) {
    const data = use(promise.current);
     // 正常能够获取到值
    console.log('===data===', data);
  }

使用 Suspense 和 use

ts 复制代码
// 服务器端组件
import { Suspense, use } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import './styles.css';
import { fetchData } from './api';
import Data from './Data';

// 处理加载状态
export default function App() {
  const promise = fetchData();

  return (
    <div className="server-page">
      <h1>服务端组件示例</h1>
        <Suspense fallback={<div>加载中...</div>}>
          <Data promise={promise} />
        </Suspense>
    </div>
  );
}
ts 复制代码
// 客户端组件
import { use } from 'react';

export default function App({ promise }) {
  const data = use(promise);
  
  return (
    <div className="data-content">
      <h3>{data.title}</h3>
      <p>{data.content}</p>
    </div>
  );
} 

服务器组件将 Promise 通过 prop 传给客户端组件,客户端组件通过 use API 来接收 Promise 已经 resolve 的结果。Promise resolve 之前的状态呢,在 Suspense 里面的 fallback 处理好了

使用 async await

以上的写法属于在客户端使用 use 解析 Promise,也可以在服务器端使用 async await解析 Promise

ts 复制代码
// 服务器端组件
export default async function App() {  
  const data = await fetchData();  
   return <Data initData={data}/>  
}

但是在服务器端使用 async await 会在 await 之前阻塞渲染,如果将 Promise 传给客户端则不会

之前的写法

ts 复制代码
export default function App() {
  const promise = fetchData();
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    promise.then((res) => {
      setData(res);
      setLoading(false);
    });
  }, []);
 } 

之前的这种写法,通常是封装一个 hooks

ts 复制代码
// useFetch
function useFetch() {
  const [content, update] = useState({value: ''})
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchData().then(res => {
      setLoading(false)
      update(res)
    })
  }, [])

  return {content, loading}
}

实际场景1:点击更新数据

ts 复制代码
export default function App() {
  // 这个地方要赋 Promise 初值,否则会报错
  const [promise, update] = useState(Promise.resolve([]));
  const [isLoading, setIsLoading] = useState(false);

   function updateData() {
    const newPromise = fetchData3();
    update(newPromise);
  }

  return (
    <div>
      <div className="text-right mb-4">
        <button className="button" onClick={updateData} >
          <div>更新数据</div>
        </button>
      </div>
      <ErrorBoundary fallback={<div>加载失败</div>}>
        <Suspense fallback={<div>加载中...</div>}>
        <Data promise={promise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}
   

实际场景 2:初始化数据,然后点击更新

ts 复制代码
export default function App() {
  //  在这里改变
  const [promise, update] = useState(fetchData());
  const [isLoading, setIsLoading] = useState(false);

   function updateData() {
    const newPromise = fetchData();
    update(newPromise);
  }

  return (
    <div>
      <div className="text-right mb-4">
        <button className="button" onClick={updateData} >
          <div>更新数据</div>
        </button>
      </div>
      <ErrorBoundary fallback={<div>加载失败</div>}>
        <Suspense fallback={<div>加载中...</div>}>
        <Data promise={promise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

原先是

ts 复制代码
 useEffect(() => {
    updateData();
  }, []);

  const updateData = async () => {
    const data = await fetchData2();
    update(data);
  };

实际场景 3: 把 loading 状态放在 button 上面

也需要把 loading 状态单独存起来

ts 复制代码
export default function ServerComponent() {
  const [loading, setLoading] = useState(false);
  const [promise, setPromise] = useState(initialPromise);

  const updateData = () => {
    setLoading(true);
    // 创建新的 Promise
    const newPromise = fetchData2();
    setPromise(newPromise);
    
    // 当 Promise 完成时更新 loading 状态
    newPromise.then(() => {
      setLoading(false);
    }).catch(() => {
      setLoading(false);
    });
  };

  return (
    <div className="server-page">
      <h1>服务端组件示例</h1>
      
      <div className="button-container">
        <button 
          className={`update-button ${loading ? 'loading' : ''}`} 
          onClick={updateData} 
          disabled={loading}
        >
          {loading ? (
            <>
              <span className="spinner-small"></span>
              <span>加载中...</span>
            </>
          ) : (
            <span>更新数据</span>
          )}
        </button>
      </div>
      
      <ErrorBoundary fallback={<div className="error-message">加载失败</div>}>
        <Suspense>
          <Data promise={promise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

实际场景 4:新增数据

ts 复制代码
export default function App() {
  const [loading, setLoading] = useState(false);
  const [promise, setPromise] = useState(fetchData4());

  // 修复按钮点击处理函数
  const updateData = () => {
    setLoading(true);
    
    // 创建新数据
    const newData = {
      id: Date.now(),
      title: `新数据 ${Date.now()}`,
      content: "这是新添加的数据"
    };
    
    // 创建新的 Promise
    const newPromise = fetchData4(newData);
    
    // 更新 Promise 状态
    setPromise(newPromise);
    
    // 完成后更新加载状态
    newPromise.then(() => {
      setLoading(false);
    }).catch(() => {
      setLoading(false);
    });
  };

  return (
    <div className="server-page">
      <h1>服务端组件示例</h1>
      
      <div className="button-container">
        <button 
          className={`update-button ${loading ? 'loading' : ''}`} 
          onClick={updateData} 
          disabled={loading}
        >
         <div>{loading ? '加载中...' : '添加新数据'}</div>
        </button>
      </div>
      
      <ErrorBoundary fallback={<div className="error-message">加载失败</div>}>
        <Suspense fallback={
          <div className="loading">
            <div className="loading-spinner"></div>
            <p>加载中...</p>
          </div>
        }>
          <Data promise={promise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

并发 API

useDeferredValue

useDeferredValue 在 React 19 中增加一个 initialValue?的属性

看一个使用 useDeferredValue 处理搜索的案例

ts 复制代码
// index.tsx
import { Suspense, useDeferredValue, useState } from "react";
import { Input } from "~/components/ui/input";
import { SearchResults } from "./SearchResults";
import { getUsersInfo } from "~/api";

export default function SearchComponent() {

  const [promise, setPromise] = useState(getUsersInfo());
  
  const deferred = useDeferredValue(promise);
  const isStale = promise !== deferred;

  const handleSearch = (value: string) => {
    setPromise(getUsersInfo(value));
  };

  return (
    <div className="search-container p-4 max-w-3xl mx-auto">
      <h1 className="text-2xl font-bold mb-4">文章搜索</h1>
      <div className="mb-4 relative">
        <Input
          className="w-full p-2 border border-gray-300 rounded"
          onChange={(e) => handleSearch(e.target.value)}
          placeholder="搜索文章标题或内容..."
        />
      </div>

      <Suspense fallback={<div className="text-center p-4">加载中...</div>}>
       
        <div
          style={{
            opacity: isStale ? 0.5 : 1,
            transition: isStale
              ? "opacity 0.2s 0.2s linear"
              : "opacity 0s 0s linear",
          }}
        >
          <SearchResults promise={deferred} />
        </div>
      </Suspense>
    </div>
  );
}
typescript 复制代码
// searchResult.tsx
import { use } from "react";

// 定义帖子类型
interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

export function SearchResults({ promise }: { promise: Promise<any> }) {
  const posts = use(promise) as Post[];

  if (!posts || posts.length === 0) {
    return <div className="text-center p-4">没有找到匹配的文章</div>;
  }

  return (
    <div className="user-results">
      <h2 className="text-xl font-semibold mb-2">搜索结果 ({posts.length})</h2>
      <ul className="divide-y divide-gray-200">
        {posts.map((post) => (
          <li key={post.id} className="py-3">
            <div className="flex flex-col">
              <div className="font-medium">{post.title}</div>
              <div className="text-sm text-gray-500">{post.body}</div>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}
typescript 复制代码
// api.ts
export function getUsersInfo(query: string = "") {
  return new Promise(async (resolve, reject) => {
    try {
      // 添加一个假延迟来让等待更加明显
      await new Promise((r) => {
        setTimeout(r, 500);
      });

      console.log("搜索关键词:", query);
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/posts"
      );
      const posts = await response.json();

      if (query && query.trim() !== "") {
        const lowercaseQuery = query.toLowerCase();
        const filteredPosts = posts.filter(
          (post: any) =>
            post.title.toLowerCase().includes(lowercaseQuery) ||
            post.body.toLowerCase().includes(lowercaseQuery)
        );
        resolve(filteredPosts);
      } else {
        resolve(posts);
      }
    } catch (error) {
      console.error("获取数据失败:", error);
      reject(error);
    }
  });
}

这个 hook 在场景搜索输入的时候很有用诶,一般来说我们实时输入检索,会通过实时防抖节流频繁进行网络请求,如果是减少网络请求次数,这些也是有用的。但是搜索是一边输入一边渲染结果,由于防抖节流并能不能中断渲染,所以它的渲染进程是阻塞的

  • 防抖 是指在用户停止输入一段时间(例如一秒钟)之后再更新列表。
  • 节流 是指每隔一段时间(例如最多每秒一次)更新列表

实现原理: 使用 useDeferredValue(value),首先会为该值创建一个延迟副本,视为优先级低的渲染任务,当用户再次实时输入的时候,这个时候的渲染任务是优先级别高的,useDeferredValue 会中断优先级别比较低的任务,去渲染优先级别高的任务。

优势:

  • 处理不必要的渲染,通过延迟更新低优先,可以直接跳过中间态,直接渲染终态
  • 减少闪烁:通过平滑的过渡,比如在页面中的半透明变化

useTransition

useDeferredValue 在内部实际上使用了类似 useTransition 的机制,但有一些关键区别:

  • useTransition 用于标记整个状态更新为低优先级。
  • useDeferredValue 专门用于创建某个值的低优先级副本
复制代码

废弃 forwardRef

现在

ts 复制代码
// MyInput
import { forwardRef } from "react";

const MyInput = forwardRef<HTMLInputElement, { placeholder?: string }>(function MyInput(props, ref) {
    const { placeholder } = props;
    return <input placeholder={placeholder} ref={ref} />;
  });

export default MyInput; 
  
// 父组件
<div>
<MyInput ref={inputRef} placeholder="输入内容" />
 <button onClick={() => inputRef.current?.focus()}>
 聚焦输入框
</button>

之后

ts 复制代码
// 父组件
 <div>
  <MyInput ref={inputRef} placeholder="输入内容" />
  <button onClick={() => inputRef.current?.focus()}>
    聚焦输入框
  </button>
 </div>
          
// MyInput
import type { RefObject } from "react";

const MyInput = (props:{placeholder:string,ref:RefObject<HTMLInputElement | null>}) => {
  return <input placeholder={props.placeholder} ref={props.ref} />;
};

export default MyInput;

Context 直接作为 Provider, 而不是之前的 ThemeContext.Provider, 读取的时候直接用 use(ThemeContext), 而且直接可以在条件语句显示

ts 复制代码
const ThemeContext = createContext('');  

function App({children}) {  
 return (  
   <ThemeContext value="dark">  
    {children}  
   </ThemeContext>  
 );  
}

React Compiler

React Compiler(以前叫 React Forget),减轻开发者手动优化组件的负担,比如 useCallback、 useMemo、React.memo。 React 通过静态分析,自动在编译的时候优化这些代码,让开发者专注业务逻辑,而不是性能优化

核心概念

  • 自动优化:就是针对于以前要进行 useCallback、 useMemo、 React.memo 手动优化的地方变成自动
  • 细粒度的跟踪:能够精确地识别组件中哪些依赖需要更新
  • 编译时优化:在构建过程中进行优化,不会增加运行时的负担,生成的代码报了所有的优化

这个网站可以看到 React playground 的代码,

关键词: Symbol.for

form actions

支持将函数作为 form 的 action 属性

ts 复制代码
<form action={actionFunction}>
  <div class="flex items-center">
   <label className="w-[100px]">邮箱: </label>
   <input type="text" name="email"/>
  </div>
  <div class="flex items-center">
   <label className="w-[100px]">名字: </label>
   <input type="text" name="name"/>
  </div>
</form>

function  actionFunction(formData:FormData){
   const email = formData.get("email");
   const name = formData.get("name");  
}

useFormStaus

kotlin 复制代码
const { pending, data, method, action } = useFormStatus();

注意:

  • useFormStatus只能在 form 的子组件中使用
  • useFormStatus 不接受任何参数

useActionState - 简化数据操作状态管理

useActionState 是一个可以根据某个表单动作的结果更新 state 的 Hook。 React.useActionState 在 Canary 版本中曾被称为 ReactDOM.useFormState

使用方法

ts 复制代码
useActionState(action, initialState, permalink?)
javascript 复制代码
const [formState, formAction] = useActionState(submit, {});
return (
   <Form action={formAction} />
)

// action.ts
const submit = (state,formData)=>{
}
  • 使用 useActionState 包括 action 的时候, action它的第一个参数是 state, useFormStatus 的 action 第一个参数是 formdata

useOptimistic - 用于乐观更新 UI

什么是乐观更新

  • 传统方式
rust 复制代码
   用户操作 -> 发送请求 -> 等待响应 -> 更新 UI
  • 乐观更新
rust 复制代码
 用户操作 ->  更新 UI -> 发送请求 -> 等待响应,失败回滚,成功更新
 

优势

  • 及时反馈
  • 更好的用户体验

使用场景

  • 社交媒体的点赞关注
  • 待办事项的添加/删除
  • 评论的发布
  • 购物车操作
  • 。。。

注意事项

  • 错误处理:需要妥善处理错误情况下的回滚机制,最好是服务器成功的概率很大
  • 状态管理:需要良好的状态管理处理临时状态,乐观更新的数据不是传给后端的,需要临时存数据的地方
  • 数据一致性:和服务器数据一致
  • 能提升用户体验,但是需要权衡使用场景和复杂度

API

javascript 复制代码
  export function useOptimistic<State, Action>(
        passthrough: State,
        reducer: (state: State, action: Action) => State,
  ): [State, (action: Action) => void];
相关推荐
Qrun4 分钟前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp4 分钟前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.1 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl3 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫5 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友5 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理6 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻6 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front7 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰7 小时前
纯flex布局来写瀑布流
前端·javascript·css