Jansiel Notes

听说你还不知道React18新特性?看我给你整明白!

前言

目前react的最新版本是18.2.0。React 团队在 2022 年 3 月 29 日正式发布了 React 的第 18 个版本 是 React 框架的最新版本,它主要着眼于解决 React 应用在性能、稳定性、开发体验等方面的问题。本文将介绍 React 18 的升级内容、新特性、新的 API、底层逻辑更新等方面的内容,并通过示例展示其使用效果。 我将在这篇文章里简单介绍 React 18 的新特性,React Concurrent Mode(并发模式)的实现,以及简要的升级指南。

升级

  • react18 已经不支持IE浏览器
  • 新项目: 直接用 npm 或者 yarn 安装最新版依赖即可(如果是js,可以不需要安装types类型声明文件)
  • 改变根节点的挂载方式使用新的 API createRoot,使用旧的 API 仍然兼容,只有在使用 createRoot 了之后才会有 React 18 的新特性。
1ReactDOM.createRoot(document.getElementById('root')!).render(
2  <React.StrictMode>
3    <Provider store={store}>
4      <App/>
5    </Provider>
6  </React.StrictMode>,
7)
8

在这个示例中,我们使用了 ReactDOM.createRoot 方法创建了一个根节点,并使用 render 方法将组件渲染到根节点中。这样可以让 React 应用更快地响应用户操作,提高用户体验。

react18 setState异步同步

在 React 18 中, setState 的行为有一些改变,它将更倾向于以异步方式进行更新,但也提供了一些选项来控制同步更新。下面是关于 React 18 中 setState 的异步和同步行为的解释:

1. 异步更新(默认行为):

在 React 18 中,默认情况下, setState 方法会以异步方式进行更新。这意味着它会将多个状态更新批量处理,并在适当的时机进行合并和应用,以优化性能。这样做可以减少不必要的重渲染,并提高应用程序的响应性。

1
2// 异步更新
3this.setState({ count: this.state.count + 1 });
4

2. 同步更新(使用 flushSync):

尽管 setState 默认以异步方式进行更新,但在某些情况下,您可能需要立即获取更新后的状态。为了实现此目的,React 18 提供了 flushSync 方法,可以强制执行同步更新。

 1
 2import { flushSync } from 'react-dom';
 3
 4// 同步更新
 5
 6flushSync(() => {
 7
 8  this.setState({ count: this.state.count + 1 });
 9
10});
11

通过使用 flushSync 包裹 setState 的调用,您可以确保在执行下一个任务之前立即获取到更新后的状态。请注意,使用 flushSync 可能会对性能产生影响,并且应谨慎使用,以避免阻塞主线程。

需要注意的是,React 18 引入了一种新的异步渲染优先级机制,称为 useTransition。通过使用 useTransition,您可以控制异步更新的优先级。这对于在高优先级工作(例如用户交互)和低优先级工作(例如懒加载数据)之间进行平衡非常有用。然而,它不直接影响 setState 的异步/同步行为,而是影响更新的优先级。

总结一下,在 React 18 中, setState 通常以异步方式进行更新,并且使用 flushSync 可以实现同步更新。此外,您还可以使用 useTransition 提供的优先级控制来平衡不同任务之间的更新。

React18 新增API

React 18 是 React 的一个重要版本,它包含了一些新的特性和改进,其中一些会对应用程序的开发流程、性能和用户体验产生重要影响。以下是 React 18 中新增的一些 API:

1. startTransition

startTransition 是一个新的 React API,旨在帮助开发者优化应用程序的性能和用户体验。这个函数可以告诉 React 在下次重新渲染组件时,应该延迟更新状态。这样,一些较慢的操作(例如异步请求等)就可以在后台执行,不会影响应用程序的交互性能。

 1
 2import { startTransition } from 'react';
 3
 4function App() {
 5
 6  const [searchTerm, setSearchTerm] = useState('');
 7
 8  const [results, setResults] = useState([]);
 9
10  function handleSearch(event) {
11
12    setSearchTerm(event.target.value);
13
14    startTransition(() => {
15
16      fetch(`/api/search?query=${searchTerm}`)
17
18        .then(response => response.json())
19
20        .then(data => setResults(data));
21
22    });
23
24  }
25
26  return (
27
28    <div>
29
30      <input type="text" value={searchTerm} onChange={handleSearch} />
31
32      <ul>
33
34        {results.map(result => (
35
36          <li key={result.id}>{result.title}</li>
37
38        ))}
39
40      </ul>
41
42    </div>
43
44  );
45
46}
47

在上述代码中,我们使用 startTransition 函数将异步请求和状态更新操作包裹起来,以告诉 React 在下一次重新渲染之前应该延迟更新状态。

2. useTransition

useTransitionstartTransition 的 hook 版本。它可以在函数组件中使用,从而让开发者更方便地控制异步操作的状态。

 1
 2import { useState, useTransition } from 'react';
 3
 4function App() {
 5
 6  const [searchTerm, setSearchTerm] = useState('');
 7
 8  const [results, setResults] = useState([]);
 9
10  const [isPending, setIsPending] = useState(false);
11
12  const [startTransition, isPendingTransition] = useTransition({ timeoutMs: 3000 });
13
14  function handleSearch(event) {
15
16    setSearchTerm(event.target.value);
17
18    startTransition(() => {
19
20      setIsPending(true);
21
22      fetch(`/api/search?query=${searchTerm}`)
23
24        .then(response => response.json())
25
26        .then(data => setResults(data))
27
28        .finally(() => setIsPending(false));
29
30    });
31
32  }
33
34  return (
35
36    <div>
37
38      <input type="text" value={searchTerm} onChange={handleSearch} />
39
40      {isPendingTransition ? <p>Loading...</p> : null}
41
42      <ul>
43
44        {results.map(result => (
45
46          <li key={result.id}>{result.title}</li>
47
48        ))}
49
50      </ul>
51
52    </div>
53
54  );
55
56}
57

在上述代码中,我们使用 useTransition hook 来控制异步请求的状态,并在加载数据时显示一个 Loading... 的提示信息。

3. createRoot

createRoot 是一个新的入口函数,用于创建根 React 组件。它可以替代原先的 ReactDOM.render 方法,使得开发者可以将多个根节点渲染到一个页面上。

 1
 2import { createRoot } from 'react-dom';
 3
 4function App() {
 5
 6  return (
 7
 8    <div>Hello, world!</div>
 9
10  );
11
12}
13
14// 原先的使用方式
15
16ReactDOM.render(<App />, document.getElementById('root'));
17
18// 新的使用方式
19
20const rootElement = document.getElementById('root');
21
22createRoot(rootElement).render(<App />);
23

在上述代码中,我们使用 createRoot 函数来创建根 React 组件,并将其渲染到页面上。这样,我们就可以使用多个根节点来构建各种复杂的应用程序界面。

4. useDeferredValue

useDeferredValue 是一个新的 hook,可以将某个状态值的更新延迟一段时间后再执行,从而提高应用程序的性能和用户体验。

 1
 2import { useState, useDeferredValue } from 'react';
 3
 4function App() {
 5
 6  const [searchTerm, setSearchTerm] = useState('');
 7
 8  function handleSearch(event) {
 9
10    setSearchTerm(event.target.value);
11
12  }
13
14  const deferredSearchTerm = useDeferredValue(searchTerm, {
15
16    timeoutMs: 1000
17
18  });
19
20  return (
21
22    <div>
23
24      <input type="text" value={searchTerm} onChange={handleSearch} />
25
26      <p>Search term: {deferredSearchTerm}</p>
27
28    </div>
29
30  );
31
32}
33

在上述代码中,我们使用 useDeferredValue hook 将搜索词的更新延迟了一秒钟。这样,用户在快速输入搜索词时,不会因为频繁的重新渲染而出现卡顿等问题。

5. useTransition

上面已经提到过了,在 React 18 中新增了 useTransition hook,用于帮助开发者控制异步操作的状态。

6. useMutableSource

useMutableSource 是一个新的 hook,用于获取可变数据源,并可以在多个组件之间共享状态。它可以帮助开发者拆分组件逻辑,并使其更加灵活和可复用。

 1
 2import { useMutableSource } from 'react';
 3
 4const myDataSource = {
 5
 6  get: () => ({ count: 0 }),
 7
 8  subscribe: (handleUpdate) => {
 9
10    const intervalId = setInterval(() => {
11
12      handleUpdate({ count: Math.floor(Math.random() * 100) });
13
14    }, 1000);
15
16    return () => clearInterval(intervalId);
17
18  }
19
20};
21
22function Counter() {
23
24  const [dataSource, setDataSource] = useState(() => myDataSource);
25
26  const [count, setCount] = useState(0);
27
28  function handleUpdate(data) {
29
30    setCount(count => count + data.count);
31
32  }
33
34  useEffect(() => {
35
36    const unsubscribe = dataSource.subscribe(handleUpdate);
37
38    return unsubscribe;
39
40  }, [dataSource]);
41
42  return (
43
44    <div>
45
46      <p>Count: {count}</p>
47
48      <button onClick={() => setDataSource(myDataSource)}>Restart</button>
49
50    </div>
51
52  );
53
54}
55
56function App() {
57
58  const [dataSource, setDataSource] = useState(() => myDataSource);
59
60  const [, forceUpdate] = useReducer(x => x + 1, 0);
61
62  function handleRestart() {
63
64    setDataSource(myDataSource);
65
66    forceUpdate();
67
68  }
69
70  const count = useMutableSource(dataSource, ({ count }) => count);
71
72  return (
73
74    <div>
75
76      <p>Count: {count}</p>
77
78      <button onClick={handleRestart}>Restart</button>
79
80    </div>
81
82  );
83
84}
85

在上述代码中,我们使用 myDataSource 作为可变数据源,并将其共享到多个组件中。在 Counter 组件中,我们订阅了数据源的更新,并实时反映出计数器的变化。在 App 组件中,我们使用了 useMutableSource hook 来获取数据源的值,从而实现了多组件之间的状态共享。

总而言之,React 18 中引入了许多有用的新特性和 API,包括 startTransitionuseTransitioncreateRootuseDeferredValueuseMutableSource 等。这些新特性和 API 可以让开发者更方便地构建高性能、灵活和可复用的 React 应用程序。

新增Hooks

React 18 引入了一些新的 hooks,以帮助开发者更好地管理状态和副作用。以下是 React 18 中新增的一些 hooks:

1. useTransition

useTransition 允许开发者在处理潜在的延迟操作时控制异步更新的优先级。它接受一个配置对象,可以设置超时时间和中断标志等选项。

 1
 2import { useTransition } from 'react';
 3
 4function MyComponent() {
 5
 6  const [isPending, startTransition] = useTransition({ timeoutMs: 2000 });
 7
 8  function handleClick() {
 9
10    startTransition(() => {
11
12      // 执行某个需要较长时间的操作
13
14    });
15
16  }
17
18  return (
19
20    <div>
21
22      <button onClick={handleClick}>开始操作</button>
23
24      {isPending && <p>操作进行中...</p>}
25
26    </div>
27
28  );
29
30}
31

在上述代码中,我们使用了 useTransition hook 来控制长时间操作的优先级,并在操作进行中显示一个提示信息。

2. useDeferredValue

useDeferredValue 允许开发者将某个状态的更新推迟到未来的帧中。这对于处理与用户输入相关的操作非常有用,可以避免在频繁输入时产生连续的重渲染。

 1
 2import { useState, useDeferredValue } from 'react';
 3
 4function MyComponent() {
 5
 6  const [searchTerm, setSearchTerm] = useState('');
 7
 8  function handleChange(event) {
 9
10    setSearchTerm(event.target.value);
11
12  }
13
14  const deferredSearchTerm = useDeferredValue(searchTerm, {
15
16    timeoutMs: 500
17
18  });
19
20  return (
21
22    <div>
23
24      <input type="text" value={searchTerm} onChange={handleChange} />
25
26      <p>搜索词: {deferredSearchTerm}</p>
27
28    </div>
29
30  );
31
32}
33

在上述代码中,我们使用了 useDeferredValue hook 来将搜索词的更新推迟了 500ms。这样,在频繁输入时,只有用户停止输入一段时间后,才会执行搜索操作。

3. useMutableSource

useMutableSource 允许开发者访问可变的数据源,并在多个组件之间共享状态。这对于高性能的数据订阅和共享非常有用。

 1
 2import { useMutableSource } from 'react';
 3
 4const myDataSource = {
 5
 6  get: () => ({ count: 0 }),
 7
 8  subscribe: (callback) => {
 9
10    const interval = setInterval(() => {
11
12      callback({ count: Math.floor(Math.random() * 100) });
13
14    }, 1000);
15
16    return () => clearInterval(interval);
17
18  }
19
20};
21
22function MyComponent() {
23
24  const [, forceUpdate] = useState({});
25
26  const count = useMutableSource(myDataSource, source => source.get());
27
28  function handleRestart() {
29
30    forceUpdate({});
31
32  }
33
34  return (
35
36    <div>
37
38      <p>Count: {count}</p>
39
40      <button onClick={handleRestart}>重启</button>
41
42    </div>
43
44  );
45
46}
47

在上述代码中,我们使用了 useMutableSource hook 来获取可变数据源中的值,并在计数器组件中共享该状态。

这些是 React 18 中新增的一些重要 hooks。通过使用这些 hooks,开发者可以更好地管理状态、处理潜在的延迟操作,并实现高性能的数据共享。除了这些新增的 hooks,React 18 也支持其他常用的 hooks,如 useStateuseEffectuseCallback 等。

严格模式

React 严格模式(Strict Mode)是一个开发模式,可以帮助开发者发现一些潜在的问题,以提高应用程序的质量。启用严格模式后,React 会执行额外的检查和警告,以帮助开发者发现一些常见问题,并尽早地解决它们。

启用 React 严格模式可以通过在代码中添加如下代码实现:

 1
 2import React from 'react';
 3
 4import ReactDOM from 'react-dom';
 5
 6ReactDOM.render(
 7
 8  <React.StrictMode>
 9
10    <App />
11
12  </React.StrictMode>,
13
14  document.getElementById('root')
15
16);
17

在上述代码中,我们使用 React.StrictMode 组件来包裹应用程序的顶层组件 <App>。这样,React 将会在严格模式下执行应用程序,并对常见问题进行检查和提示。

React 严格模式主要包含以下几个方面的检查和提示:

  • 识别不安全的生命周期方法,提示开发者修改,这些方法可能会导致意外的副作用或错误。
  • 检测意外的副作用,例如:多余的重新渲染、不符合预期的函数调用等。
  • 检测某些过时的 API 使用,提供更好的替代方案。
  • 检测警告信息,使其更加明显和易于发现。

需要注意的是,React 严格模式只在开发环境下工作,不会影响生产环境下的应用程序。因此,在开发过程中启用严格模式可以帮助开发者及早发现问题,并尽可能将这些问题解决,以提高应用程序的稳定性和质量。

总而言之,React 严格模式是一种非常有用的开发模式,可以帮助开发者发现常见问题并提高应用程序的质量。通过在顶层组件中添加 <React.StrictMode> 包裹,我们可以启用严格模式,并享受其带来的好处。

如何禁用严格模式

在 React 应用中禁用严格模式可以通过以下两种方式实现:

1. 直接移除 <React.StrictMode> 组件

最简单的方法是将应用程序顶层组件中的 <React.StrictMode> 组件直接移除。这样,React 将不会启用严格模式,也不会执行额外的检查和警告。

 1
 2import React from 'react';
 3
 4import ReactDOM from 'react-dom';
 5
 6import App from './App';
 7
 8ReactDOM.render(
 9
10  <App />,
11
12  document.getElementById('root')
13
14);
15

2. 在应用程序启动时禁用严格模式

在一些情况下,移除 <React.StrictMode> 组件可能不太方便,例如:在大型项目中或已经存在大量的 console.log 调用等代码片段。此时,可以在应用程序启动时禁用严格模式。

在应用程序启动文件中,我们可以使用 React 的 unstable_disableDevMode() 函数来禁用严格模式:

 1
 2import React from 'react';
 3
 4import ReactDOM from 'react-dom';
 5
 6import App from './App';
 7
 8React.unstable_disableDevMode();
 9
10ReactDOM.render(
11
12  <React.StrictMode>
13
14    <App />
15
16  </React.StrictMode>,
17
18  document.getElementById('root')
19
20);
21

在上述代码中,我们在调用 ReactDOM.render 之前调用了 React.unstable_disableDevMode() 函数,以禁用严格模式。该函数并不在文档中明确提供支持,因此请谨慎使用。

需要注意的是,禁用严格模式可能会导致一些潜在问题无法被及早发现,因此建议仅在必要时使用。同时,需要确保 React 版本兼容性,并遵循最佳实践和安全规则。

并发模式

React 并发模式(React Concurrent Mode)是 React 的一项新功能,旨在改善在复杂应用程序中的用户体验和性能。在传统的 React 中,更新组件树时会阻塞用户界面的响应,可能导致卡顿和延迟。而并发模式通过将任务分解为多个小步骤,让 React 在执行渲染和布局时可以中断和恢复任务,从而提供更平滑和响应式的用户体验。

在 React 并发模式中,引入了两个主要概念:任务调度和优先级。任务调度器负责决定哪些任务执行、何时执行以及中断和恢复任务。优先级允许 React 根据任务的紧迫性来安排任务的执行顺序,确保响应度更高的任务能够优先执行。

利用并发模式,React 可以将渲染过程分解为多个小任务,并根据优先级来动态调整任务执行的顺序。这样,在浏览器空闲时间或网络请求等异步操作期间,React 可以暂停当前任务,执行其他具有更高优先级的任务,以实现更爽快的用户交互体验。

总而言之,React 并发模式通过任务调度和优先级机制,提供了更好的用户体验和性能,使得 React 应用程序能够更加平滑地响应用户操作。

以下是一个简单的示例代码,展示了 React Concurrent Mode 的基本用法:

 1import React, { useState, useEffect, unstable_ConcurrentMode as ConcurrentMode } from 'react';
 2
 3function App() {
 4
 5  const [count, setCount] = useState(0);
 6
 7  useEffect(() => {
 8
 9    const timer = setInterval(() => {
10
11      setCount((prevCount) => prevCount + 1);
12
13    }, 1000);
14
15    return () => {
16
17      clearInterval(timer);
18
19    };
20
21  }, []);
22
23  return (
24
25    <ConcurrentMode>
26
27      <div>
28
29        <h1>计数器</h1>
30
31        <p>{count}</p>
32
33      </div>
34
35    </ConcurrentMode>
36
37  );
38
39}
40
41export default App;
42

在上面的示例中,我们使用了 unstable_ConcurrentMode 组件来包裹根元素。这表示该组件下的子组件可以享受到并发模式的好处。

App 组件中,我们使用了 useState 来声明一个状态变量 count,并通过 setCount 来更新它的值。在 useEffect 中,我们使用定时器每秒钟增加 count 的值。注意,我们传递了空数组作为第二个参数,表示只在组件挂载时执行一次。

最后,在组件的返回值中,我们使用 <ConcurrentMode> 组件包裹了整个应用程序的 UI。这样,React 将会利用并发模式来处理渲染任务,以提供更平滑和响应式的用户体验。

服务端渲染

React 18 并没有专门针对服务端渲染(SSR)进行大规模的改进,但它仍然提供了一些与 SSR 相关的 API 和改进。以下是一些我们在 React 18 中可以使用的 SSR 相关功能:

1. useOpaqueIdentifier

useOpaqueIdentifier 允许开发者生成与数据不相关的、不透明的标识符,并在 SSR 上使用这些标识符来生成唯一的 DOM ID。

1import { useOpaqueIdentifier } from 'react';
2
3function MyComponent() {
4  const id = useOpaqueIdentifier();
5
6  return <div id={`my-component-${id}`}>My component</div>;
7}
8

在上述代码中,我们使用了 useOpaqueIdentifier hook 来生成一个不透明的标识符,并将其用于组件的 DOM ID 中。由于这个标识符与数据无关,因此在 SSR 上也可以正确地生成唯一的 ID。

2. ReactDOMServer.renderToStringAsync

ReactDOMServer.renderToStringAsync 允许开发者异步地渲染组件并输出 HTML。这样可以避免在 SSR 期间阻塞主线程,在数据加载和计算时保持响应性。

1import ReactDOMServer from 'react-dom/server';
2
3async function renderApp(req, res) {
4  const app = <MyApp />;
5  const html = await ReactDOMServer.renderToStringAsync(app);
6  res.send(html);
7}
8

在上述代码中,我们使用了 ReactDOMServer.renderToStringAsync 方法异步地将 <MyApp /> 组件渲染为 HTML,并在 Express 中将其发送到客户端。

3. Concurrent Mode

Concurrent Mode 是 React 18 中引入的一个新特性,它通过异步渲染和交互优先级控制等方式提升了应用程序的响应性。在 SSR 中,Concurrent Mode 可以帮助开发者更好地处理异步数据加载和渲染等任务。

 1import { unstable_createRoot } from 'react-dom';
 2
 3async function renderApp(req, res) {
 4  const app = <MyApp />;
 5  const root = unstable_createRoot(document.createDocumentFragment());
 6  await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟数据加载延迟
 7  root.render(app);
 8  const html = ReactDOMServer.renderToString(root);
 9  res.send(html);
10}
11

在上述代码中,我们使用了 unstable_createRoot 方法来创建一个 Concurrent Mode 的根节点。在数据加载完成后,我们渲染了应用程序,并将其输出为 HTML。

这些是 React 18 中与 SSR 相关的一些功能和改进。通过使用这些功能,开发者可以更好地处理异步数据加载和渲染,并提升应用程序的响应性。