React 18 完结:最佳实践与注意事项

React 18 完结:最佳实践与注意事项
Photo by Jon Tyson / Unsplash

本文将深入探讨 React 18 的最佳实践和注意事项,从渐进式地采用新特性、管理状态和副作用、优化 Suspense 和 Transition 的使用、调试和测试等方面,结合 React 18 的源码和示例,为开发者提供全面、实用的指导。

渐进式地采用 React 18 的新特性

React 18 引入了许多新的特性和 API,但并非所有的特性都是向后兼容的。为了平稳地过渡到 React 18,我们需要渐进式地采用这些新特性。

启用 Concurrent 模式

Concurrent 模式是 React 18 的核心特性,它为 React 应用带来了更好的性能和响应性。但是,由于 Concurrent 模式下的一些行为与传统的 React 应用不同,我们需要谨慎地启用它。

在 React 18 中,我们可以通过 ReactDOM.createRoot 方法来启用 Concurrent 模式:

import ReactDOM from 'react-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

注意,一旦启用了 Concurrent 模式,就不能再使用传统的 ReactDOM.render 方法。

渐进式地迁移到 Suspense

Suspense 是 React 18 提供的一种处理异步操作的新机制,它允许我们在数据加载时显示一个 Fallback,提供更好的用户体验。

但是,如果我们的应用中已经有了处理异步操作的逻辑 (如使用 isLoading 状态),就需要谨慎地迁移到 Suspense。我们可以先在部分组件中尝试 Suspense,待整个迁移过程稳定后再全面推广。

下面是一个渐进式迁移到 Suspense 的示例:

// 传统的异步数据处理方式
function Profile() {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setIsLoading(true);
    fetchUser().then((user) => {
      setUser(user);
      setIsLoading(false);
    });
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return <div>{user.name}</div>;
}

// 使用 Suspense 处理异步数据
function Profile() {
  const user = useData();

  return <div>{user.name}</div>;
}

function useData() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser().then(setUser);
  }, []);

  if (!user) {
    throw fetchUser();
  }

  return user;
}

在迁移到 Suspense 时,我们需要注意以下几点:

  • Suspense 的 Fallback 应该能够处理多个子组件同时 "suspend" 的情况。
  • 使用 Suspense 加载数据时,要确保数据获取函数 (如上例中的 fetchUser) 在必要时重新获取数据。
  • 对于需要立即显示的关键内容,仍然需要在服务端渲染或使用占位符。

在 Concurrent 模式下管理状态和副作用

Concurrent 模式下,React 的渲染过程变得更加复杂和不可预测。多个版本的 UI 可能同时存在,部分更新可能会被跳过或重复执行。这对于状态管理和副作用处理提出了新的挑战。

使用 useMutableSource 管理外部状态

在 Concurrent 模式下,如果组件依赖于外部的可变状态 (如 Redux store、React Context 等),可能会导致状态不一致的问题。

为了解决这个问题,React 18 提供了一个新的 Hook useMutableSource,用于安全地读取外部状态:

import { useMutableSource } from 'react';

function Example() {
  const value = useMutableSource(
    store,
    () => store.getSnapshot(),
    [store]
  );

  // ...
}

useMutableSource 接受三个参数:

  • source:可变的外部状态源,如 Redux store。
  • getSnapshot:从状态源中获取当前状态的函数。
  • dependencies:当依赖项发生变化时,重新获取状态。

使用 useMutableSource 可以确保组件总是读取到一致的外部状态,即使在渲染过程中状态发生了变化。

使用 useEffect 处理副作用

在 Concurrent 模式下,组件的渲染过程可能会被中断和重复执行。这对于副作用的处理提出了新的要求。

我们需要确保副作用函数是幂等的,即无论执行多少次,都能得到相同的结果。同时,我们还需要正确地处理副作用的取消和清理。

下面是一个处理副作用的示例:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 订阅事件
    const subscription = subscribe();

    // 更新 DOM
    updateDOM();

    // 清理副作用
    return () => {
      unsubscribe(subscription);
    };
  }, [count]); // 只有 count 变化时才重新执行副作用

  // ...
}

在这个示例中,我们使用 useEffect 来处理副作用。我们在副作用函数中订阅了事件、更新了 DOM,并返回一个清理函数来取消订阅和清理副作用。

同时,我们为 useEffect 提供了一个依赖项数组 [count],确保只有在 count 发生变化时才重新执行副作用,避免不必要的重复执行。

优化 Suspense 和 Transition 的使用

Suspense 和 Transition 是 React 18 提供的两个强大的功能,它们可以显著提升应用的性能和用户体验。但是,如果使用不当,也可能适得其反。

选择合适的 Suspense Fallback

Suspense Fallback 是在数据加载时显示的占位内容。一个好的 Fallback 应该能够给用户提供有意义的反馈,同时不会影响整个应用的性能。

常见的 Fallback 包括:

  • 骨架屏 (Skeleton):显示组件的大致轮廓,让用户对即将加载的内容有一个大致的印象。
  • 加载指示器 (Spinner):显示一个旋转的加载图标,告诉用户数据正在加载中。
  • 空状态 (Empty State):显示一个空的占位符,表示暂时没有数据。

我们应该根据具体的使用场景和设计需求,选择合适的 Fallback 类型。

下面是一个使用骨架屏作为 Fallback 的示例:

function ProfilePage() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <ProfileDetails />
      <ProfilePosts />
    </Suspense>
  );
}

function ProfileSkeleton() {
  return (
    <div>
      <div className="skeleton avatar" />
      <div className="skeleton name" />
      <div className="skeleton bio" />
    </div>
  );
}

在这个示例中,我们使用了一个自定义的 ProfileSkeleton 组件作为 Suspense 的 Fallback。该组件渲染了一个骨架屏,展示了用户头像、姓名、简介等元素的占位符。这给了用户一个直观的印象,表示页面正在加载中。

合理使用 Transition

Transition 允许我们将某些状态更新标记为 "非紧急" 的,从而避免它们阻塞用户交互。但是,并非所有的状态更新都适合使用 Transition。

一般来说,对于以下几种情况,我们可以考虑使用 Transition:

  • 需要大量计算或渲染的更新,如复杂的列表渲染、图表绘制等。
  • 优先级较低的更新,如广告、推荐内容等。
  • 用户不太关心更新结果的场景,如点赞数、阅读量等。

相反,对于以下几种情况,我们应该谨慎使用 Transition:

  • 直接影响用户交互的更新,如表单输入、按钮点击等。
  • 需要立即反馈给用户的更新,如错误提示、成功消息等。
  • 对用户体验至关重要的更新,如购物车、支付流程等。

下面是一个合理使用 Transition 的示例:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  useEffect(() => {
    startTransition(() => {
      searchAPI(query).then((results) => {
        setResults(results);
      });
    });
  }, [query]);

  return (
    <div>
      {isPending && <div>Updating results...</div>}
      <ul>
        {results.map((result) => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

在这个示例中,我们使用 Transition 来处理搜索结果的更新。当搜索词 query 发生变化时,我们会触发一个新的搜索请求,并将结果更新到 results 状态中。

由于搜索结果的更新可能需要一定的时间,而且用户也不需要立即看到结果,我们使用 startTransition 将这个更新标记为 "非紧急" 的。这样,即使搜索请求耗时较长,也不会阻塞用户与页面的其他部分的交互。

同时,我们使用 isPending 状态来显示一个 "Updating results" 的提示,告诉用户搜索结果正在更新中。

Concurrent 模式下的调试与测试

Concurrent 模式给调试和测试也带来了一些新的挑战。由于渲染过程变得异步和不可预测,传统的调试和测试方法可能不再适用。

使用 React DevTools 进行调试

React DevTools 是官方提供的一个浏览器扩展,用于调试 React 应用。在 React 18 中,DevTools 也得到了相应的更新,以支持 Concurrent 模式下的调试。

使用 React DevTools,我们可以:

  • 查看组件树的结构和状态。
  • 检查各个组件的 props 和 state。
  • 分析组件的渲染性能和 Suspense 边界。
  • 模拟不同的网络条件和交互事件。

在 Concurrent 模式下,我们可以使用 React DevTools 来分析组件的渲染过程和性能瓶颈,找出可以优化的地方。

编写异步友好的测试用例

在 Concurrent 模式下,组件的渲染过程变得异步和不可预测。这对于测试提出了新的挑战,因为传统的同步测试方法可能无法准确地捕获组件的行为。

为了编写异步友好的测试用例,我们需要:

  • 使用 act 函数包裹交互和断言,确保它们在同一个 "act" 中执行。
  • 使用 findBy 系列的查询函数,等待异步操作完成后再进行断言。
  • 使用 waitFor 函数,等待某个条件满足后再进行断言。

下面是一个异步友好的测试用例示例:

import { render, screen, act, waitFor } from '@testing-library/react';

test('loads and displays greeting', async () => {
  render(<Greeting />);

  await act(async () => {
    await waitFor(() => {
      expect(screen.getByText('Hello, world!')).toBeInTheDocument();
    });
  });
});

在这个示例中,我们使用 @testing-library/react 提供的测试工具来编写测试用例。

我们使用 render 函数渲染了 <Greeting> 组件,然后使用 act 函数包裹了一个异步的交互和断言过程。在 act 中,我们使用 waitFor 函数等待 "Hello, world!" 文本出现在页面中,然后再进行断言。

通过这种方式,我们可以确保测试用例能够正确地处理组件的异步行为,提高测试的可靠性和稳定性。

React 18 与服务端渲染

React 18 对服务端渲染 (SSR) 也提供了更好的支持。得益于 Suspense 和 Streaming SSR 等新特性,我们可以更轻松地实现服务端渲染,并提供更好的用户体验。

使用 Suspense 优化服务端渲染

在服务端渲染时,我们经常需要等待数据获取完成后才能开始渲染。这可能会导致服务端渲染的延迟,影响用户体验。

使用 Suspense,我们可以在服务端渲染时 "暂停" 渲染,直到数据获取完成。这样,我们就可以在数据获取的同时,立即开始渲染其他部分,提高渲染效率。

下面是一个使用 Suspense 优化服务端渲染的示例:

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Nav />
      <Suspense fallback={<Spinner />}>
        <Route path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Suspense>
    </Suspense>
  );
}

async function renderToString() {
  const app = (
    <App />
  );

  const stream = await ReactDOMServer.renderToReadableStream(app);
  return stream;
}

在这个示例中,我们在服务端渲染时使用了 Suspense 组件。外层的 Suspense 用于包裹整个应用,内层的 Suspense 用于包裹路由组件。

当服务端开始渲染时,它会立即开始渲染 <Nav> 组件,同时等待路由组件的数据获取完成。一旦数据获取完成,React 就会继续渲染对应的路由组件。

通过这种方式,我们可以最大限度地利用服务端的渲染时间,尽早地开始渲染,提高用户感知的速度。

使用 Streaming SSR 提高渲染性能

除了 Suspense,React 18 还引入了 Streaming SSR 的概念。Streaming SSR 允许服务端在渲染的同时,就开始向客户端发送已经渲染好的 HTML 片段,而不必等待整个页面渲染完成。

这种 "流式" 的渲染方式可以显著提高首屏渲染的速度,因为客户端可以在接收到部分 HTML 后就开始渲染,而不必等待整个 HTML 文档下载完成。

下面是一个使用 Streaming SSR 的示例:

async function renderToStream() {
  const app = (
    <html>
      <body>
        <Suspense fallback={<Spinner />}>
          <App />
        </Suspense>
      </body>
    </html>
  );

  return await ReactDOMServer.renderToReadableStream(app);
}

app.get('/', (req, res) => {
  const stream = await renderToStream();
  stream.pipe(res);
});

在这个示例中,我们使用 ReactDOMServer.renderToReadableStream 方法将 React 组件渲染为一个可读的流 (Readable Stream)。然后,我们将这个流通过管道 (pipe) 传输给 HTTP 响应对象 (res)。

当客户端接收到这个响应时,它就可以立即开始渲染已经收到的 HTML 片段,而不必等待整个页面加载完成。这种 "流式" 的渲染方式可以显著提高首屏渲染的速度,改善用户体验。

总结

本文详细探讨了 React 18 的各种最佳实践和注意事项,涵盖了从渐进式升级、状态管理、性能优化到调试测试、服务端渲染等各个方面。

在升级到 React 18 时,我们需要注意渐进式地采用新特性,如 Concurrent 模式、Suspense 等,并适当调整现有的代码和实践。

对于状态管理和副作用处理,React 18 提供了新的 API 和机制,如 useMutableSourceuseEffect 等,帮助我们更好地应对 Concurrent 模式下的挑战。

在性能优化方面,Suspense 和 Transition 是两个重要的工具。我们需要合理地选择 Suspense Fallback,并恰当地使用 Transition,以提供更流畅的用户体验。

针对 Concurrent 模式下的调试和测试,我们可以利用 React DevTools 进行可视化分析,并编写异步友好的测试用例,确保应用的质量和稳定性。

在服务端渲染方面,React 18 引入了 Suspense 和 Streaming SSR 等新特性,帮助我们更轻松地实现 SSR,并提供更好的性能和用户体验。

总的来说,React 18 为开发者提供了一系列强大的工具和特性,帮助我们打造更高性能、更可靠、更易维护的应用。通过了解和遵循这些最佳实践和注意事项,我们可以更好地发挥 React 18 的潜力,创造出更优秀的 Web 应用。

Read more

ngrok本地调试原理及Telegram mini app cookie path 问题

ngrok本地调试原理及Telegram mini app cookie path 问题

在现代web开发中,本地调试是一个非常重要的环节。然而,当我们需要将本地开发的应用暴露到公网以便进行测试时,就会遇到一些挑战。本文将详细介绍如何使用ngrok实现内网穿透进行本地调试,特别是在Telegram小程序开发场景中的应用,以及可能遇到的常见问题及其解决方案。 ngrok原理 ngrok是一个反向代理工具,它可以将本地服务器安全地暴露到公网。下面是ngrok的工作原理: 1. 用户启动ngrok客户端,并指定要暴露的本地端口。 2. ngrok客户端与ngrok云服务建立安全的通道。 3. ngrok云服务生成一个公网可访问的URL。 4. 当外部请求到达这个URL时,ngrok云服务将请求通过安全通道转发到本地ngrok客户端。 5. 本地ngrok客户端将请求转发到指定的本地端口。 6. 本地服务器处理请求并返回响应,响应通过相同的路径返回给客户端。 Telegram小程序调试场景 在Telegram小程序开发中,我们经常需要使用ngrok来进行本地调试。以下是具体步骤: 1. 启动本地开发服务器(例如运行在localhost:3000)。

TypeScript:从架构分层设计到IOC和AOP

TypeScript:从架构分层设计到IOC和AOP

TypeScript作为JavaScript的超集,为开发者提供了强大的类型系统和面向对象编程能力。然而,要在大型项目中充分发挥TypeScript的优势,我们需要深入理解软件架构原则和设计模式。本文将探讨如何使用TypeScript构建一个健壮的应用架构,涵盖分层设计、常见设计模式、控制反转(IOC)和面向切面编程(AOP)等高级概念。 分层架构 分层架构是组织大型应用程序的常用方法。它有助于关注点分离,使得每一层都可以独立开发和测试。一个典型的分层架构包括: 1. 表现层(Presentation Layer) 2. 业务逻辑层(Business Logic Layer) 3. 数据访问层(Data Access Layer) 4. 数据库(Database) 让我们使用图表来可视化这个架构: 接下来,我们将探讨每一层中可能使用的设计模式,并通过TypeScript代码示例来说明如何实现这些模式。 表现层 表现层负责处理用户界面和用户交互。在这一层,我们经常使用MVC(Model-View-Controller)或MVVM(Model-View-ViewM

Jotai v2: React状态管理的新篇章

Jotai v2: React状态管理的新篇章

Jotai是一个为React应用设计的轻量级状态管理库。2023年3月,Jotai发布了v2.0版本,带来了许多新特性和改进。本文将深入探讨Jotai v2的使用方法、适用场景、设计理念、源码结构以及核心功能的实现原理。 版本信息 本文讨论的是Jotai v2.0.3版本,发布于2023年5月。你可以通过以下命令安装 npm install [email protected] 基本使用 Jotai的核心概念是"atom"。atom是最小的状态单位,可以存储任何JavaScript值。让我们看一个简单的例子: import { atom, useAtom } from 'jotai' // 创建一个atom const countAtom = atom(0) function Counter() { // 使用atom const [count, setCount] = useAtom(

加密货币交易所十二:安全性和风险控制

加密货币交易所十二:安全性和风险控制

在加密货币合约交易所中,安全性和风险控制是至关重要的。这不仅关系到交易所的声誉和用户的资产安全,也直接影响到整个加密货币生态系统的稳定性。本章将详细探讨合约交易所在安全性和风险控制方面的关键策略和实施方法。 多重签名机制 多重签名(MultiSig)是一种强大的安全机制,要求多个私钥来授权交易,大大降低了单点故障和内部欺诈的风险。 概念解释 多重签名是一种需要多个私钥来签署和授权交易的加密技术。例如,在一个 2-of-3 多重签名设置中,需要三个私钥中的任意两个来完成交易。 在合约交易所中的应用 热钱包管理: * 设置:通常采用 2-of-3 或 3-of-5 的多重签名方案。 * 应用:每次从热钱包转出大额资金时,需要多个管理员的授权。 冷钱包管理: * 设置:可能采用更严格的 3-of-5 或 4-of-7 方案。 * 应用:定期将热钱包中的多余资金转移到冷钱包时使用。 智能合约升级: * 设置:可能需要多个核心开发者和安全审计员的签名。 * 应用:在升级关键智能合约时,确保变更经过充分审核和授权。 实现考虑 密钥管理: * 使用硬件安全