【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];
相关推荐
Moment1 小时前
从方案到原理,带你从零到一实现一个 前端白屏 检测的 SDK ☺️☺️☺️
前端·javascript·面试
鱼樱前端1 小时前
Vue3 + TypeScript 整合 MeScroll.js 组件
前端·vue.js
拉不动的猪2 小时前
刷刷题29
前端·vue.js·面试
野生的程序媛2 小时前
重生之我在学Vue--第5天 Vue 3 路由管理(Vue Router)
前端·javascript·vue.js
codingandsleeping2 小时前
前端工程化之模块化
前端·javascript
CodeCraft Studio2 小时前
报表控件stimulsoft操作:使用 Angular 应用程序的报告查看器组件
前端·javascript·angular.js
阿丽塔~2 小时前
面试题之vue和react的异同
前端·vue.js·react.js·面试
烛阴3 小时前
JavaScript 性能提升秘籍:WeakMap 和 WeakSet 你用对了吗?
前端·javascript
yuren_xia4 小时前
eclipse创建maven web项目
前端·eclipse·maven
鱼樱前端4 小时前
Vue 2 与 Vue 3 语法区别完整对比
前端·javascript·vue.js