头图

React 19是如何助力打造更快网站的?

原文链接:How React 19 can help you make faster websites
作者:Emmanuel Odioko
译者:倔强青铜三

前言

大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

React 作为前端开发领域的热门框架,不断推陈出新以满足开发者对性能和开发效率的追求。React 19 的发布带来了诸多令人兴奋的新特性和改进,本文将带你深入了解这些变化,探讨它们如何帮助你构建更快的网站。

React 19的变革理由

变革是进步和创新的必然产物,作为React开发者,我们已习惯于适应这些改进。React 19给我留下了深刻印象,其相较于以往版本有诸多亮点。

React 19的一个重大变化是引入了编译器。我们看到顶级框架相互借鉴重要变革,就像Svelte一样,React现在也包含了一个编译器。该编译器可将React代码编译成普通JavaScript,从而大幅提升性能。编译器已在Instagram的生产环境中投入使用,它能减少不必要的重新渲染,并为懒加载和代码分割等提供自动化支持,这意味着我们不再需要使用React.lazy

自动记忆化

对于不了解记忆化的人来说,它是一种优化组件以避免不必要的重新渲染的方法。以往,我们会使用useMemo()useCallback()钩子来实现这一目的,但这一过程颇为繁琐。现在,记忆化实现了自动化,编译器在这方面将发挥更大作用,因为在大型应用中,确定何处使用useMemo()会变得愈发复杂。因此,我们终于可以告别useMemo()useCallback()钩子了。

use()钩子

use()钩子意味着我们无需再使用useContext()钩子,在某些情况下它还能暂时取代useEffect()钩子。该钩子允许我们读取并异步加载资源,如承诺或上下文,也可用于某些情况下的数据获取,以及在循环和条件语句中使用,这与其他钩子不同。

use clientuse server指令

如果你是Next.js的忠实粉丝,那么对use clientuse server指令一定不会陌生。如果你是首次接触,只需记住use client指令告诉Next.js该组件应在浏览器端运行,而use server指令则告知Next.js代码应在服务器端运行。这些指令已在Canary版本中存在一段时间,如今我们可以在React中使用它们。这样做带来的好处包括增强SEO、加快加载速度以及更便捷地在服务器端获取数据。一个基本的实现示例如下:

use client

'use client';

import React, { useState } from 'react';

const ClientComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default ClientComponent;

use server

'use server';

export async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

Actions

如果你使用过Remix或Next,可能对action API并不陌生。在React 19之前,你需要在表单上设置提交处理函数,并从函数中发起请求。现在,我们可以在表单上使用action属性来处理提交。它可用于服务器端和客户端应用,并且可以同步和异步地工作。虽然我们可能不会告别旧钩子,但我们将迎来如UseFormStatus()useActionState()等新钩子,稍后我们会深入探讨它们。

Actions会在transition()内自动提交,这将保持当前页面的交互性。在处理action期间,我们可以在transitions中使用async await,这将允许你展示追加的UI,借助transition的isPending()状态。

useOptimistic钩子

这个钩子终于不再是实验性的了。它非常用户友好,用于在等待服务器响应时为UI设置临时的乐观更新。就像你在WhatsApp上发送消息,但由于网络故障消息尚未显示双勾,useOptimistic钩子正是为此场景而简化操作的。结合actions使用此钩子,你可以在客户端乐观地设置数据的状态。更多详情可查阅React中的useOptimistic钩子。

文档元数据

你是否有偏好的React SEO和元数据包?不过,随着React 19的到来,这些包可能要被遗忘了,因为React 19内置了对诸如标题、描述和关键词等元数据的支持,你可以将它们放置在组件的任何位置,包括服务器端和客户端代码:

function BlogPost({ post }) {
  return (
    <article>
      {/* Post Header */}
      <header>
        <h1>{post.title}</h1>
        <p>
          <strong>Author:</strong>
          <a href="https://twitter.com/joshcstory/" rel="author" target="_blank">
            Josh
          </a>
        </p>
      </header>

      {/* Meta Tags for SEO */}
      <head>
        <title>{post.title}</title>
        <meta name="author" content="Josh" />
        <meta name="keywords" content={post.keywords.join(", ")} />
      </head>

      {/* Post Content */}
      <section>
        <p>
          {post.content || "Eee equals em-see-squared..."}
        </p>
      </section>
    </article>
  );
}

React 19中的每个元标签都将为浏览器、搜索引擎和其他网络服务提供关于页面的特定信息。

其他更新和特性

除了上述内容,React 19还有一些其他有趣的更新。

资源加载

你可能遇到过重新加载页面时出现未样式化内容的情况,这确实令人困惑。幸运的是,在React 19中,这一问题将不复存在,因为资产加载将与Suspense集成,确保高分辨率图像在显示前已就绪。

消除forwardRef

ref现在将作为常规属性传递。我们偶尔会使用forwardRef,它允许你的组件通过ref将dom节点暴露给父组件,这一用法不再需要,因为ref将直接作为常规属性。

最后,React将更好地支持Web组件,这将帮助你构建可重用的组件。接下来,让我们通过实际用例深入了解React 19将如何帮助我们构建更快的网站。

探索React 19钩子

首先介绍的是use()。以下钩子的解释顺序并无特殊原因,我仅尝试以更易理解的方式进行阐述。继续阅读,我们将看到它们的用法:

使用use()进行数据获取

你可以通过以下示例了解如何使用use()创建一个笑话应用,该应用在刷新页面时获取新的笑话。

首先,我们使用useEffect()钩子,之后我将展示使用use()时代码的简洁性:

使用useEffect()

import { useEffect, useState } from "react";

const JokeItem = ({ joke }) => {
  return (
    <div className="bg-gradient-to-br from-orange-100 to-blue-100 rounded-2xl shadow-lg p-8 transition-all duration-300">
      <p className="text-xl text-gray-800 font-medium leading-relaxed">
        {joke.value}
      </p>
    </div>
  );
};

const Joke = () => {
  const [joke, setJoke] = useState(null);
  const [loading, setLoading] = useState(true);

  const fetchJoke = async () => {
    setLoading(true);
    try {
      const res = await fetch("https://api.chucknorris.io/jokes/random");
      const data = await res.json();
      setJoke(data);
    } catch (error) {
      console.error("Failed to fetch joke:", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchJoke();
  }, []);

  const refreshPage = () => {
    window.location.reload();
  };

  return (
    <div className="min-h-[300px] flex flex-col items-center justify-center p-6">
      <div className="w-full max-w-2xl">
        {loading ? (
          <h2 className="text-2xl text-center font-bold mt-5">Loading...</h2>
        ) : (
          <JokeItem joke={joke} />
        )}
        <button
          onClick={refreshPage}
          className="mt-8 w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-bold py-3 px-6 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2"
        >
          Reload To Fetch New Joke
        </button>
      </div>
    </div>
  );
};

export default Joke;

在上述代码中,我们使用useEffect钩子获取随机笑话,每当页面重新加载时,就会获取一个新的笑话。

使用use(),我们可以完全摒弃useEffect()isloading状态,因为我们将会使用Suspense边界来处理加载状态。代码如下:

import { use, Suspense } from "react";

const fetchData = async () => {
  const res = await fetch("https://api.chucknorris.io/jokes/random");
  return res.json();
};

let jokePromise = fetchData();

const RandomJoke = () => {
  const joke = use(jokePromise);
  return (
    <div className="bg-gradient-to-br from-orange-100 to-blue-100 rounded-2xl shadow-lg p-8 transition-all duration-300">
      <p className="text-xl text-gray-800 font-medium leading-relaxed">
        {joke.value}
      </p>
    </div>
  );
};

const Joke = () => {
  const refreshPage = () => {
    window.location.reload();
  };

  return (
    <>
      <div className="min-h-[300px] flex flex-col items-center justify-center p-6">
        <div className="w-full max-w-2xl">
          <Suspense
            fallback={
              <h2 className="text-2xl text-center font-bold mt-5">
                Loading...
              </h2>
            }
          >
            <RandomJoke />
          </Suspense>
          <button
            onClick={refreshPage}
            className="mt-8 w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-bold py-3 px-6 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2"
          >
            Refresh to Get New Joke
          </button>
        </div>
      </div>
    </>
  );
};

export default Joke;

我们使用use()钩子解包fetchData函数返回的承诺,并在RandomJoke组件中直接访问解析后的笑话数据。这是处理React中异步数据获取的更直接方式,效果如下:

img

很有趣,对吧?

use()钩子取代useContext()钩子

use()出现之前,如果我想实现暗黑和浅色模式主题,我会使用useContext()钩子来管理和提供组件中的主题状态。我还需要创建一个ThemeContext,用于保存当前主题和切换函数,然后将应用包裹在ThemeProvider中,以便在组件中访问上下文。

例如,一个名为ThemedCard的组件会调用useContext(ThemeContext)来首先访问主题,并根据用户的交互调整样式:

import { createContext, useState, useContext } from "react";

// Create a context object
const ThemeContext = createContext();

// Create a provider component
const ThemeProvider = ({ children }) => {
  // State to hold the current theme
  const [theme, setTheme] = useState("light");

  // Function to toggle theme
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  };

  return (
    // Provide the theme and toggleTheme function to the children
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const ThemedCard = () => {
  // Access the theme context using the useContext hook
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
      <div
        className={`max-w-md mx-auto shadow-md rounded-lg p-6 transition-colors duration-200 ${
          theme === "light"
            ? "bg-white text-gray-800"
            : "bg-gray-800 text-white"
        }`}
      >
        <h1 className="text-2xl font-bold mb-3">Saying Goodbye to UseContext()</h1>
        <p
          className={`${theme === "light" ? "text-gray-600" : "text-gray-300"}`}
        >
          The use() hook will enable us to say goodbye to the useContext() hook
          and could potentially replace the useEffect() hook in a few cases.
          This hook lets us read and asynchronously load a resource such as a
          promise or a context. This can also be used in fetching data in some
          cases and also in loops and conditionals, unlike other hooks.
        </p>
        {/* Toggle button */}
        <button
          onClick={toggleTheme}
          className={`mt-4 px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-opacity-50 transition-colors duration-200 ${
            theme === "light"
              ? "bg-gray-600 hover:bg-blue-600 text-white focus:ring-blue-500"
              : "bg-yellow-400 hover:bg-yellow-500 text-gray-900 focus:ring-yellow-500"
          }`}
        >
          {theme === "light" ? "Switch to Dark Mode" : "Switch to Light Mode"}
        </button>
      </div>
    </div>
  );
};

const Theme = () => {
  return (
    <ThemeProvider>
      <ThemedCard />
    </ThemeProvider>
  );
};

export default Theme;

使用use(),你可以做同样的事情,但这次只需将UseContext()替换为use()钩子:

// Replace useContext() hook
import { createContext, useState, useContext } from "react";

// Access the theme context directly using the use() hook
const { theme, toggleTheme } = use(ThemeContext);

暗黑主题:

img

浅色主题:

img

Action用例

以创建帖子为例来说明Action的用法。我们将有一个帖子表单,允许我们提交关于读过的书、度假经历或我个人最爱的对工作中干扰我的bug的吐槽等简单更新。以下是使用Actions实现的目标:

img

以下是使用Action实现该功能的代码:

// PostForm component
const PostForm = () => {
  const formAction = async (formData) => {
    const newPost = {
      title: formData.get("title"),
      body: formData.get("body"),
    };
    console.log(newPost);
  };

  return (
    <form
      action={formAction}
      className="bg-white shadow-xl rounded-2xl px-8 pt-6 pb-8 mb-8 transition-all duration-300 hover:shadow-2xl"
    >
      <h2 className="text-3xl font-bold text-indigo-800 mb-6 text-center">
        Create New Post
      </h2>
      <div className="mb-6">
        <label
          className="block text-gray-700 text-sm font-semibold mb-2"
          htmlFor="title"
        >
          Title
        </label>
        <input
          className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300"
          id="title"
          type="text"
          placeholder="Enter an engaging title"
          name="title"
        />
      </div>
      <div className="mb-6">
        <label
          className="block text-gray-700 text-sm font-semibold mb-2"
          htmlFor="body"
        >
          Body
        </label>
        <textarea
          className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300"
          id="body"
          rows="5"
          placeholder="Share your thoughts..."
          name="body"
        ></textarea>
      </div>
      <div className="flex items-center justify-end">
        <button
          className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-3 px-6 rounded-full focus:outline-none focus:shadow-outline transition-all duration-300 flex items-center"
          type="submit"
        >
          <PlusIcon className="mr-2 h-5 w-5" />
          Create Post
        </button>
      </div>
    </form>
  );
};

export default PostForm;

如果你曾经使用过PHP,那么在表单中使用action会非常相似,我们有点回归本源了。我们有一个简单的表单,并为其附加了action。我们可以随意命名它,在我的例子中是formAction

接下来我们创建formAction函数。由于这是一个action,我们将能够访问formData。我们创建一个名为newPost的对象,并将其设置为一个包含标题和正文的对象,我们可以通过get方法访问它们。现在,如果我们打印newPost,我们应该能够获取输入值,即标题和帖子内容:

imgimg

差不多就这样了!我无需创建onClick并添加事件处理函数,只需添加一个action。以下是其余代码:

import { useState } from "react";
import { PlusIcon, SendIcon } from "lucide-react";

// PostItem component
const PostItem = ({ post }) => {
  return (
    <div className="bg-gradient-to-r from-purple-100 to-indigo-100 shadow-lg p-6 my-8 rounded-xl transition-all duration-300 hover:shadow-xl hover:scale-105">
      <h2 className="text-2xl font-extrabold text-indigo-800 mb-3">
        {post.title}
      </h2>
      <p className="text-gray-700 leading-relaxed">{post.body}</p>
    </div>
  );
};

// PostForm component
const PostForm = ({ addPost }) => {
  const formAction = async (formData) => {
    const newPost = {
      title: formData.get("title"),
      body: formData.get("body"),
    };
    addPost(newPost);
  };

  return (
    <form
      action={formAction}
      className="bg-white shadow-xl rounded-2xl px-8 pt-6 pb-8 mb-8 transition-all duration-300 hover:shadow-2xl"
    >
      <h2 className="text-3xl font-bold text-indigo-800 mb-6 text-center">
        Create New Post
      </h2>
      <div className="mb-6">
        <label
          className="block text-gray-700 text-sm font-semibold mb-2"
          htmlFor="title"
        >
          Title
        </label>
        <input
          className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300"
          id="title"
          type="text"
          placeholder="Enter an engaging title"
          name="title"
        />
      </div>
      <div className="mb-6">
        <label
          className="block text-gray-700 text-sm font-semibold mb-2"
          htmlFor="body"
        >
          Body
        </label>
        <textarea
          className="shadow-inner appearance-none border-2 border-indigo-200 rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:border-indigo-500 transition-all duration-300"
          id="body"
          rows="5"
          placeholder="Share your thoughts..."
          name="body"
        ></textarea>
      </div>
      <div className="flex items-center justify-end">
        <button
          className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-3 px-6 rounded-full focus:outline-none focus:shadow-outline transition-all duration-300 flex items-center"
          type="submit"
        >
          <PlusIcon className="mr-2 h-5 w-5" />
          Create Post
        </button>
      </div>
    </form>
  );
};

// Posts component
const Posts = () => {
  const [posts, setPosts] = useState([]);

  const addPost = (newPost) => {
    setPosts((posts) => [...posts, newPost]);
  };

  return (
    <div className="container mx-auto px-4 py-8 max-w-4xl">
      <h1 className="text-4xl font-extrabold text-center text-indigo-900 mb-12">
        Logrocket Blog
      </h1>
      <PostForm addPost={addPost} />
      {posts.length > 0 ? (
        posts.map((post, index) => <PostItem key={index} post={post} />)
      ) : (
        <div className="text-center text-gray-500 mt-12">
          <p className="text-xl font-semibold mb-4">No posts yet</p>
          <p>Be the first to create a post!</p>
        </div>
      )}
    </div>
  );
};

export default Posts;

useFormStatus()钩子

上述表单可以工作,但我们可以借助useFormStatus更进一步。这样,我们可以在表单实际提交时让提交按钮显示禁用状态或执行其他操作。

需要注意两点:首先,这个钩子仅返回父表单的状态信息,而不返回同一组件中渲染的任何表单的状态信息;其次,这个钩子是从React-Dom导入的,而不是从React导入。

在上述表单中,我们将按钮提取到一个名为SubmitFormButton()的单独组件中,然后从useFormStatus获取pending状态,该状态为true或false。接着我们编写pending时的逻辑。逻辑可以很简单,比如“如果pending,则显示‘正在创建帖子...’,否则显示‘创建帖子’”,我们还可以添加一点延迟以便看到变化。看看代码中的实现:

提交组件:

// SubmitButton component
const SubmitButton = () => {
  const { pending } = useFormStatus();
  console.log(pending);

  return (
    <button
      className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-3 px-6 rounded-full focus:outline-none focus:shadow-outline transition-all duration-300 flex items-center"
      type="submit"
      disabled={pending}
    >
      <PlusIcon className="mr-2 h-5 w-5" />
      {pending ? "Creating Post..." : "Create Post"}
    </button>
  );
};

在表单提交中模拟延迟:

// PostForm component
const PostForm = ({ addPost }) => {
  const formAction = async (formData) => {
    // Simulate a delay of 3 seconds
    await new Promise((resolve) => setTimeout(resolve, 3000));
    const newPost = {
      title: formData.get("title"),
      body: formData.get("body"),
    };
    addPost(newPost);
  };

接下来我们渲染PostForm组件,应该就能看到如下效果,按钮同时也会被禁用,直到帖子创建完成。

useActionState钩子

我们可以使用useActionState()钩子重构代码。useActionState钩子将表单提交逻辑、状态管理和加载状态整合到一个单元中。

这样做可以自动处理表单提交过程中的pending状态,就像useFormStatus钩子一样,我们可以轻松地禁用提交按钮、显示加载消息以及展示成功或错误消息。

useFormStatus不同,useActionState将从React导入,使用方法如下:

  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      // Simulate a delay of 3 seconds
      await new Promise((resolve) => setTimeout(resolve, 3000));
      const title = formData.get("title");
      const body = formData.get("body");

      if (!title || !body) {
        return { success: false, message: "Please fill in all fields." };
      }

      const newPost = { title, body };
      addPost(newPost);
      return { success: true, message: "Post created successfully!" };
    }
  );

在上述代码中,useActionState用于处理提交,它通过formData API提取标题和正文,然后验证输入并返回成功和错误状态。效果如下:

结论

React 19的主要贡献在于帮助开发者构建更快的网站,我很高兴能够向大家介绍这一新版本。欢迎在下方提问,也欢迎大家对这个新版本发表看法。

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

倔强青铜三
28 声望0 粉丝