跳到主要内容

11 篇博文 含有标签「React」

查看所有标签

· 阅读需 10 分钟

前言

在本节中,我们将了解 React 中用于 HTML 元素(例如按钮和输入元素)的事件处理程序。你将学习如何使用带有 onClick 事件的按钮,以及如何定义和使用不同类型的事件处理程序。本质上,我们将介绍三种事件处理程序:事件处理程序、内联事件处理程序和回调事件处理程序。

React中的事件处理程序

首先,我们将从 React 中针对特定 onClick 事件处理程序的按钮示例开始。这是关于如何在 React 中使用事件处理程序(也称为事件处理程序函数或处理程序)处理事件的最基本示例。按钮具有接收函数的 onClick 属性。每次触发事件时都会调用此函数(此处:单击按钮时):

import React from 'react';

function App() {
function handleClick() {
console.log('Button click ...');
}

return (
<div>
<button type="button" onClick={handleClick}>
Event Handler
</button>
</div>
);
}

对于其他属性,如 onChange(onChange 事件处理程序)和 onSubmit(onSubmit 事件处理程序),它的工作方式类似。对于初学者来说,onClick 经常不起作用,因为他们没有传递函数,而是直接在 JSX 中调用函数。例如,在下一个版本中,事件处理程序仅在第一次渲染组件时被调用一次。其他每一次单击都不会调用事件处理函数,因为函数的返回值用于 onClick 属性而不是函数本身。所以没有什么可调用的;除非函数返回另一个函数:

import React from 'react';

function App() {
function handleClick() {
console.log('Button click ...');
}

// don't do this
return (
<div>
<button type="button" onClick={handleClick()}>
Event Handler
</button>
</div>
);
}

通过使用 JavaScript 箭头函数,可以使事件处理函数更加简洁。不过,这是一个可选步骤。就个人而言,我喜欢将事件处理程序作为箭头函数:

import React from 'react';

function App() {
const handleClick = () => {
console.log('Button click ...');
};

return (
<div>
<button type="button" onClick={handleClick}>
Event Handler
</button>
</div>
);
}

但是一旦更多的事件处理程序在 React 组件中添加,通过再次给它们函数语句来使它们与其他变量更容易区分是很好的:

import React from 'react';

function App() {
const user = {
id: '123abc',
username: 'Robin Wieruch',
};

function handleUserSignIn() {
// do something
}

function handleUserSignUp() {
// do something
}

function handleUserSignOut() {
// do something
}

...
}

毕竟,onClick 事件的事件处理程序应该实现一些业务逻辑。在本例中,React 的 useState Hook 用于通过 onClick 按钮事件更新某些状态:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

function handleClick() {
setCount(count + 1);
}

return (
<div>
Count: {count}

<button type="button" onClick={handleClick}>
Increase Count
</button>
</div>
);
}

下一个示例向你展示了一个输入字段而不是一个按钮。在那里,我们使用的是始终作为第一个参数传递给事件处理函数的实际事件。该事件是来自 React 的合成事件,它本质上封装了原生 HTML 事件并在其之上添加了一些功能。每次有人使用事件的目标属性输入输入字段时,此事件都会为你提供输入字段的值:

import React from 'react';

function App() {
const [text, setText] = React.useState('');

function handleChange(event) {
setText(event.target.value);
}

return (
<div>
<input type="text" onChange={handleChange} />

{text}
</div>
);
}

以前我们没有使用过该事件,因为在我们的按钮示例中我们不需要它。在输入字段示例中,我们需要它。最后但同样重要的是,不要忘记将值传递给输入元素以使其成为受控组件:

import React from 'react';

function App() {
const [text, setText] = React.useState('');

function handleChange(event) {
setText(event.target.value);
}

return (
<div>
<input type="text" value={text} onChange={handleChange} />

{text}
</div>
);
}

简而言之,这就是事件处理程序。让我们了解 React 中更高级的处理程序。

React 中的内联事件处理程序

内联事件处理程序,也称为内联处理程序,通过直接在 JSX 中使用事件处理程序为我们提供了许多新选项:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

return (
<div>
Count: {count}

<button
type="button"
onClick={function() {
setCount(count + 1);
}}
>
Increase Count
</button>
</div>
);
}

在 JSX 中使用通用函数语句虽然很冗长。因此,JavaScript 箭头函数可以方便地定义更简洁的内联处理程序:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

return (
<div>
Count: {count}

<button
type="button"
onClick={() => setCount(count + 1)}
>
Increase Count
</button>
</div>
);
}

一般来说,开发者都是懒惰的人,所以经常使用内联事件处理程序来避免在 JSX 之外进行额外的函数声明。然而,这会将大量业务逻辑转移到 JSX 中,这使得它的可读性、可维护性和易错性降低。就个人而言,我喜欢在没有内联事件处理程序的情况下保持 JSX 干净,并在 JSX 之外声明事件处理程序。 内联处理程序也用于将参数传递给在 JSX 之外定义的更通用的处理程序:

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

function handleCount(delta) {
setCount(count + delta);
}

return (
<div>
Count: {count}

<button type="button" onClick={() => handleCount(1)}>
Increase Count
</button>
<button type="button" onClick={() => handleCount(-1)}>
Decrease Count
</button>
</div>
);
}

这样,也可以并行传递事件和参数。即使在此示例中不需要它,但你肯定会在将来遇到需要该事件的一种或另一种情况(例如 React Forms 的 preventDefault ):

import React from 'react';

function App() {
const [count, setCount] = React.useState(0);

function handleCount(event, delta) {
setCount(count + delta);
}

return (
<div>
Count: {count}

<button type="button" onClick={event => handleCount(event, 1)}>
Increase Count
</button>
<button type="button" onClick={event => handleCount(event, -1)}>
Decrease Count
</button>
</div>
);
}

因此,当你需要传递事件和参数时,例如当你需要为 onClick 事件提供额外参数时,内联事件处理程序可能会为你提供帮助。然后 JSX 之外的更通用的事件处理程序可以使用这个额外的参数。

React 中的回调事件处理程序

简而言之,有回调事件处理程序或回调处理程序。当子组件需要与父组件通信时使用它们。由于 React props 只在组件树中向下传递,因此使用回调处理程序(其核心是一个函数)进行向上通信:

import React from 'react';

function App() {
const [text, setText] = React.useState('');

// 1
function handleTextChange(event) {
setText(event.target.value); // 3
}

return (
<div>
<MyInput inputValue={text} onInputChange={handleTextChange} />

{text}
</div>
);
}

// 2
function MyInput({ inputValue, onInputChange }) {
return (
<input type="text" value={inputValue} onChange={onInputChange} />
);
}

回调处理程序在某处定义 (1),在其他地方使用 (2),但回调到其定义的位置 (3)。这样,就可以从子组件到父组件进行通信。回调处理程序通过 React props 向下传递,并在调用函数时向上通信。

你已经了解了 React 的事件处理程序、内联事件处理程序和回调事件处理程序,以及如何在按钮中为它们的 onClick 事件和在输入字段中为它们的 onChange 事件使用它们。还有其他事件处理程序,例如表单元素的 onSubmit,实际上需要该事件来阻止本机浏览器行为。无论如何,所有这些事件处理程序都有其特定目的。你的目标应该是让你的代码保持可读性和可维护性,

· 阅读需 16 分钟

前言

本文主题是React事件冒泡和捕获的 。大多数 JavaScript 开发人员可能已经熟悉这个主题,因为它起源于 JavaScript 及其 DOM API。但是,在本文中,我想为 React 中的事件冒泡和捕获整理一些信息。

React 中的事件处理程序可用于侦听特定事件(例如单击事件)。我们将从 React 中的一个函数组件开始,我们使用 React 的 useState Hook 来增加一个计数器:

import * as React from 'react';

function App() {
const [count, setCount] = React.useState(0);

const handleClick = () => {
setCount(count + 1);
};

return (
<button type="button" onClick={handleClick}>
Count: {count}
</button>
);
}

export default App;

在原生 JavaScript 中,这相当于 element.addEventListener('click', handleClick);。 React 中有很多事件。下面显示了鼠标和触摸事件的事件列表:

  • touchstart
  • touchmove
  • touchend
  • mousemove
  • mousedown
  • mouseup
  • click 此特定事件列表按其执行顺序显示。因此,如果在 HTML 元素中添加了 mouseup 和 click 事件侦听器,则 mouseup 事件将在 click 事件之前触发:
import * as React from 'react';

function App() {
const handleClick = () => {
alert('click');
};

const handleMouseUp = () => {
alert('mouseup');
};

return (
<button
type="button"
onClick={handleClick}
onMouseUp={handleMouseUp}
>
Which one fires first?
</button>
);
}

export default App;

在某些情况下,你可能希望在另一个事件触发时阻止其中一个事件。例如,当触摸事件发生并被处理时,你可能希望阻止所有点击事件。

无论如何,在前面的示例中,所有事件都发生在同一个 HTML 元素上。关于事件冒泡或捕获还没有什么可看的。接下来,让我们探索使用多个 HTML 元素的事件冒泡:

import * as React from 'react';

function App() {
const [count, setCount] = React.useState(0);

const handleCount = () => {
setCount((state) => state + 1);
};

return (
<div onClick={handleCount}>
<button type="button" onClick={handleCount}>
Count: {count}
</button>
</div>
);
}

export default App;

在这个例子中,按钮似乎被点击了两次,因为计数器增加了 2 而不是 1。然而,发生的事情是包装容器元素也调用了它的事件处理程序。进入在 React 中事件冒泡。

React 中的事件冒泡

下面的示例显示了两个具有相同样式的 HTML 元素。为了简单起见,我们在这里使用内联样式,但是,你可以随意使用更复杂的方式来设置你的 React 应用程序的样式。

无论如何,让我们进入事件冒泡的话题。如你所见,在下一个示例中,只有外部容器元素侦听单击事件,而不是内部容器元素。但无论你是单击外部元素还是内部元素,都会触发事件处理程序:

import * as React from 'react';

const style = {
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleClick = () => {
alert('click');
};

return (
<div style={style} onClick={handleClick}>
<div style={style}>Click Me</div>
</div>
);
}

export default App;

在 JavaScript 中,这个原理称为事件冒泡。每当在 HTML 元素(例如内部 HTML 元素)上发生事件时,它就会开始运行通过该特定元素的处理程序,然后是其父 HTML 元素(例如外部 HTML 元素,它实际上在其中找到侦听处理程序)的处理程序,然后一直向上遍历每个祖先 HTML 元素,直到它到达文档的根。 在下一个示例中尝试一下,当单击内部 HTML 元素时,两个事件处理程序都会被触发。如果单击外部 HTML 元素,则仅触发外部元素的事件处理程序:

import * as React from 'react';

const style = {
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleOuterClick = () => {
alert('outer click');
};

const handleInnerClick = () => {
alert('inner click');
};

return (
<div style={style} onClick={handleOuterClick}>
<div style={style} onClick={handleInnerClick}>
Click Me
</div>
</div>
);
}

export default App;

换句话说,事件从它们的起源开始冒泡整个文档。通过 React 的 useEffect Hook 在文档上添加一个事件监听器,自己验证这种行为:

import * as React from 'react';

const style = {
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleOuterClick = () => {
alert('outer click');
};

const handleInnerClick = () => {
alert('inner click');
};

React.useEffect(() => {
const handleDocumentClick = () => {
alert('document click');
};

document.addEventListener('click', handleDocumentClick);

return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, []);

return (
<div style={style} onClick={handleOuterClick}>
<div style={style} onClick={handleInnerClick}>
Click Me
</div>
</div>
);
}

export default App;

因此,如果一个事件从其交互元素中冒出整个文档,那么在某些情况下如何停止冒泡呢?在 React 中输入 stopPropagation

React stopPropagation

stopPropagation() 方法是 DOM API 的原生方法。由于 React 将事件包装到称为合成事件的 React 版本中,因此该 API 仍然可用于 React 事件,它还可以用于停止事件的传播:

import * as React from 'react';

function App() {
const [count, setCount] = React.useState(0);

const handleCount = (event) => {
setCount((state) => state + 1);

event.stopPropagation();
};

return (
<div onClick={handleCount}>
<button type="button" onClick={handleCount}>
Count: {count}
</button>
</div>
);
}

export default App;

我们通过对事件使用 stopPropagation() 方法扩展了前面的示例之一。这样,当按钮被点击时,事件不会冒泡,也不会触发周围容器元素的事件处理程序。 反之,当容器元素被显式点击时(在这种情况下不太可能没有任何进一步的样式),只有容器的事件处理程序会触发。这里容器元素上的 stopPropagation() 有点多余,因为它上面没有事件处理程序。

最佳时机:默认情况下不要停止事件传播。例如,如果你将在项目中的每个按钮上使用 stopPropagation(),但稍后你想在文档级别跟踪用户点击,你将不会再收到这些事件。默认情况下使用 stopPropagation() 往往会导致错误,因此仅在必要时使用它。

当停止事件传播有意义时,让我们看看更复杂的场景。例如,可能有一个可点击的标题,可以将用户从任何页面导航到主页,但是,在标题内有一个按钮可以让用户从应用程序中注销。两个元素都应该是可点击的,不会相互干扰:

import * as React from 'react';

const styleHeader = {
padding: '10px',
border: '1px solid black',
boxSizing: 'border-box',
width: '100%',
display: 'flex',
justifyContent: 'space-between',
};

function App() {
const [isActive, setActive] = React.useState(false);

const handleHeaderClick = () => {
alert('header click (e.g. navigate to home page)');
};

const handleButtonClick = (event) => {
alert('button click (e.g. log out user)');

if (isActive) {
event.stopPropagation();
}
};

return (
<>
<div style={styleHeader} onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>

<button type="button" onClick={() => setActive(!isActive)}>
Stop Propagation: {isActive.toString()}
</button>
</>
);
}

export default App;

在不停止传播的情况下,注销按钮将触发它自己的事件处理程序,但也会触发标题上的事件处理程序,因为事件会冒泡到它上面。当 stopPropagation() 被激活时,单击注销按钮不会导致标题上的冒泡事件,因为该事件被阻止冒泡。

总之,只要有一个带有处理程序的元素嵌套在另一个带有处理程序的元素中,两者都在侦听相同的事件(这里:单击事件),使用 stopPropagation() 将有助于将事件委托给正确的处理程序(通过防止他们从冒泡)。

target && currentTarget

当单击带有监听器(事件处理程序)的 HTML 元素时,你可以访问它的事件(在 React 中它是合成事件)。在其他属性中,事件可以访问表示导致事件的元素的目标属性。因此,如果按钮具有事件处理程序并且用户单击此按钮,则该事件将以按钮元素作为目标。

即使这个事件冒泡到另一个事件处理程序,如果一个嵌套元素导致了这个事件,那么目标仍然由这个嵌套元素表示。因此,在所有处理程序中,事件的目标都不会改变。

从事件处理程序到事件处理程序的变化是事件的 currentTarget,因为它表示实际事件处理程序正在运行的元素:

import * as React from 'react';

const style = {
display: 'block',
padding: '10px 30px',
border: '1px solid black',
};

function App() {
const handleDivClick = (event) => {
alert(`
<div /> \n
event.target: ${event.target} \n
event.currentTarget: ${event.currentTarget}
`);
};

const handleSpanClick = (event) => {
alert(`
<span /> \n
event.target: ${event.target} \n
event.currentTarget: ${event.currentTarget}
`);
};

return (
<div style={style} onClick={handleDivClick}>
<span style={style} onClick={handleSpanClick}>
Click Me
</span>
</div>
);
}

export default App;

通常你将与事件的目标进行交互,例如停止事件的传播或阻止默认行为。但是,有时你希望从正在运行的事件处理程序访问元素,因此你可以改用 currentTarget。

React 中的事件捕获

当谈到 JavaScript 中的事件冒泡时,不得不提的是存在事件捕获的概念。实际上两者都是依次发生的:当用户与元素交互时,DOM API 会向下遍历文档(捕获阶段)到目标元素(目标阶段),然后 DOM API 才会再次向上遍历(冒泡阶段)。

在某些情况下,你可能希望在捕获阶段中的事件到达冒泡阶段之前对其进行拦截。然后,你可以使用 onClickCapture 而不是 onClick 来处理 JSX 中的单击事件,或者使用 addEventListener() 方法的第三个参数来激活在捕获阶段而不是冒泡阶段的监听:

function App() {
const handleOuterClick = () => {
alert('outer click');
};

const handleInnerClick = () => {
alert('inner click');
};

React.useEffect(() => {
const handleDocumentClick = () => {
alert('document click');
};

document.addEventListener(
'click',
handleDocumentClick,
true
);

return () => {
document.removeEventListener(
'click',
handleDocumentClick,
true
);
};
}, []);

return (
<div style={style} onClickCapture={handleOuterClick}>
<div style={style} onClickCapture={handleInnerClick}>
Click Me
</div>
</div>
);
}

谈到“某些情况”有点含糊。因此,让我们回到前面的示例,其中我们将注销按钮嵌套在标题元素中。如果单击该按钮以不触发标头的事件处理程序,则该按钮将停止事件的传播。现在,如果你想通过在顶级文档级别引入分析跟踪来扩展此示例,你可以验证自己对于单击按钮,你不会收到分析跟踪,而只会收到标题,因为按钮阻止了从冒泡到文档的事件:

function App() {
const handleHeaderClick = () => {
alert('header click (e.g. navigate to home page)');
};

const handleButtonClick = (event) => {
alert('button click (e.g. log out user)');

// important: stops event from appearing
// in the document's event handler
event.stopPropagation();
};

React.useEffect(() => {
const handleDocumentClick = (event) => {
alert(`
document clicked - \n
run analytics for clicked element: ${event.target}
`);
};

document.addEventListener('click', handleDocumentClick);

return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, []);

return (
<>
<div style={styleHeader} onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>
</>
);
}

凭借我们对捕获阶段的了解,我们可以在实际用户交互冒泡之前对事件进行分析跟踪。在这种情况下,我们通过将第三个参数设置为 true(使用捕获阶段而不是冒泡阶段)在文档上添加事件侦听器:

function App() {
const handleHeaderClick = () => {
alert('header click (e.g. navigate to home page)');
};

const handleButtonClick = (event) => {
alert('button click (e.g. log out user)');

// important: stops event from appearing
// in the document's event handler
event.stopPropagation();
};

React.useEffect(() => {
const handleDocumentClick = (event) => {
alert(`
document clicked - \n
run analytics for clicked element: ${event.target}
`);
};

document.addEventListener(
'click',
handleDocumentClick,
true
);

return () => {
document.removeEventListener(
'click',
handleDocumentClick,
true
);
};
}, []);

return (
<>
<div style={styleHeader} onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>
</>
);
}

当点击注销按钮时,捕获阶段从上到下遍历所有处理程序,从而触发文档级别的处理程序进行分析跟踪。然后它向下遍历元素到目标(此处:按钮),因为没有其他事件处理程序正在侦听捕获阶段(例如,通过使用 onClickCapture 代替)。从那里,事件冒泡并触发按钮的事件处理程序,阻止事件传播到标题的事件处理程序。

在日常工作中,大多数开发人员使用冒泡阶段通过使用事件处理程序来拦截事件,并使用 stopPropagation() 方法阻止事件传播。因此,在开发人员的脑海中,总是有事件模型在 HTML 树中冒泡。然而,正如某些边缘情况所示,了解捕获阶段也是有意义的。

理解 JavaScript 中的事件冒泡对于在 React 中使用它至关重要。每当你有一个复杂的页面,其中带有事件处理程序的伪按钮被包装到其他伪按钮中时,就无法绕过本机 stopPropagation 方法。但是,请谨慎使用它,而不是默认使用它,否则从长远来看,你可能会遇到错误。

· 阅读需 8 分钟

前言

浅比较在 React 开发中无处不在。它在不同的流程中起着关键作用,也可以在 React 组件生命周期的多个地方找到。类组件是否应该更新的机制,React hooks 的依赖数组,通过 React.memo 进行记忆等等。

如果你曾经阅读过 React 的官方文档,你很可能已经经常看到浅比较这个术语。因此,本文将研究浅比较的概念,它到底是什么,它是如何工作的。

什么是浅比较?

理解浅层比较最直接的方法是深入研究它的实现。相应的代码可以在共享子包的 React Github 项目中找到。例如下面的实现:

import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';

/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}

return true;
}

这过程中发生了很多事情,所以让我们将其拆分并逐步执行该功能。

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
}

从函数定义开始,函数接受两个将相互比较的实体。与 TypeScript 不同,此代码使用 Flow 作为类型检查系统。两个函数参数都是使用特殊的混合 Flow 类型键入的,类似于 TypeScript 的未知数。它表明参数可以是任何类型的值,该函数将找出其余的并使其工作。

import is from './objectIs';

function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
// ...
}

其次,首先使用来自 React 内部对象的 is 函数将函数参数相互比较。导入的函数只不过是 JavaScript 的 Object.is 函数的 polyfill 版本。这个比较函数基本上等同于常见的 === 运算符,但有两个例外:

  • Object.is 认为相反的有符号零(+0 和 -0)不相等,而 === 认为它们相等。
  • Object.is 认为 Number.NaN 和 NaN 相等,而 === 认为它们不相等。

基本上,第一个条件语句处理所有简单的情况:如果两个函数参数具有相同的值,对于原始类型,或引用相同的对象,对于数组和对象,那么它们被认为是浅比较相等的。

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}

// ...
}

在处理了两个函数参数值相等或引用同一个对象的所有简单情况之后,我们想要进入更复杂的结构(对象和数组)。但是,如果任何一个参数是原始值,前面的条件语句仍然可以给我们留下原始值。

因此,为了确保我们从现在开始处理两个复杂的结构,代码会检查任一参数是否不是对象类型或等于 null。前一个检查确保我们正在处理对象或数组,而后一个检查是过滤掉空值,因为它们的类型也是对象。如果任一条件成立,我们肯定是在处理不相等的参数(否则前面的条件语句会将它们过滤掉),因此浅比较返回 false。

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// ...
}

现在可以确定我们只处理数组和对象,我们可以专注于浅比较这些数据结构。为此,我们必须深入研究复杂数据结构的值,并在两个函数参数之间进行比较。

但在我们这样做之前,我们可以通过一个简单的检查来确保两个参数具有相同数量的值。如果不是,则通过浅层比较可以保证它们不相等,这可以节省我们一些精力。为此,我们使用参数的键。对于对象,键数组将由实际键组成,而对于数组,键数组将由字符串中原始数组中占用的索引组成。

import hasOwnProperty from './hasOwnProperty';

function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...

// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}

return true;
}

作为最后一步,我们按键迭代两个函数参数的值,并逐个验证它们以确定它们是否等效。为此,代码使用在上一步中生成的键数组,使用 hasOwnProperty 检查键是否实际上是参数的属性,并使用与比较值之前相同的 Object.is 函数。

如果事实证明任何键在两个参数之间没有等效值,那么通过浅比较可以肯定它们不相等。因此,我们缩短了 for 循环,并从 shallowEqual 函数中返回 false。如果所有值都相等,那么我们可以通过浅比较调用函数参数相等并从函数返回 true。

一些有趣的点

现在我们了解了浅层比较及其背后的实现,我们可以从这些知识中学到一些有趣的东西:

  • 浅比较不使用严格相等、=== 运算符,而是使用 Object.is 函数。
  • 通过浅比较,空对象和数组是等价的。
  • 通过浅比较,以索引为键的对象等效于在各个索引处具有相同值的数组。例如。 { 0: 2, 1: 3 } 等价于 [2, 3]
  • 由于 Object.is 优于 === 的使用,通过浅比较,+0 和 -0 不等价,NaN 和 Number.NaN 也不等价。- 如果它们在复杂结构内进行比较,这也适用。 虽然两个内联创建的对象(或数组)通过浅比较相等({} 和 [] 浅相等),但具有嵌套内联对象的内联对象不相等({ someKey: {} } 和 { someKey: [] } 不浅平等的)。

· 阅读需 11 分钟

前言

在 React 18 中发布的另一个重要特性是 Suspense。如果你在之前使用过 React ,那么你就会知道 Suspense 功能并不是特别新。早在 2018 年,Suspense 作为 React 16.6 版的一部分作为实验性功能发布。然后,它主要针对与React.lazy。

但是现在,有了 React 18,Suspense 的正式发布就在我们面前。伴随着并发渲染的发布,Suspense 的真正威力终于解锁了。Suspense 和并发渲染之间的交互为改善用户体验开辟了广阔的世界。

但就像所有功能一样,就像并发渲染一样,从基础开始很重要。Suspense 到底是什么?为什么我们首先需要Suspense ?Suspense 如何解决这个问题?有什么好处?为了帮助你理解这些基础知识,本文将详细介绍这些问题,并为你提供有关 Suspense 的知识基础。

什么是Suspense?

本质上,Suspense 是 React 开发人员向 React 指示组件正在等待数据准备好的一种机制。然后 React 知道它应该等待该数据被获取。同时,将向用户显示一个反馈,并且 React 将继续渲染应用程序的其余部分。数据准备好后,React 会返回到那个特定的 UI 并相应地更新它。

从根本上说,这听起来与 React 开发人员必须实现数据获取流程的当前方式没有太大区别:使用某种状态来指示组件是否仍在等待数据,useEffect开始获取数据,显示加载状态基于数据的状态,并在数据准备好后更新 UI。

但在实践中,Suspense 使这在技术上完全不同。与上面提到的数据获取流程相反,Suspense 与 React 深度集成,允许开发人员更直观地编排加载状态,并避免竞争条件。为了更好地理解这些细节,了解我们为什么需要 Suspense 很重要。

为什么我们需要Suspense?

在没有 Suspense 的情况下,实现数据获取流程的主要方法有两种:渲染时获取渲染后获取。但是,这些传统的数据获取流程存在一些问题。要了解 Suspense,我们必须深入研究这些流程的问题和局限性。

渲染时获取

useEffect大多数人将使用和状态变量来实现前面提到的数据获取流程。这意味着只有在组件呈现时才开始获取数据。所有数据获取都发生在组件的副作用和生命周期方法中。

这种方法的主要问题是:组件仅在渲染时触发数据获取,异步特性迫使组件必须等待其他组件的数据请求。

假设我们有一个ComponentA获取一些数据并具有加载状态的组件。在内部,ComponentA还呈现另一个组件ComponentB,该组件也自己执行一些数据获取。但是由于数据获取的实现方式,ComponentB只有在渲染时才开始获取数据。这意味着它必须等到ComponentA完成获取数据然后渲染ComponentB。

这导致了瀑布式方法,其中组件之间的数据获取顺序发生,这实质上意味着它们相互阻塞。

function ComponentA() {
const [data, setData] = useState(null);

useEffect(() => {
fetchAwesomeData().then(data => setData(data));
}, []);

if (user === null) {
return <p>Loading data...</p>;
}

return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
);
}

function ComponentB() {
const [data, setData] = useState(null);

useEffect(() => {
fetchGreatData().then(data => setData(data));
}, []);

return data === null ? <h2>Loading data...</h2> : <SomeComponent data={data} />;
}

渲染后获取

为了防止组件之间数据获取的顺序阻塞,一种替代方法是尽早开始所有数据获取。因此,与其让组件负责处理渲染时的数据获取,而且数据请求都单独发生,而是在树开始渲染之前启动所有请求。

这种方法的优点是所有数据请求都是一起发起的,因此ComponentB不必等待ComponentA完成。这解决了组件顺序阻塞彼此数据流的问题。但是,它引入了另一个问题,我们必须等待所有数据请求完成,然后才能为用户呈现任何内容。可以想象,这不是最佳体验。

// Start fetching before rendering the entire tree
function fetchAllData() {
return Promise.all([
fetchAwesomeData(),
fetchGreatData()
]).then(([awesomeData, greatData]) => ({
awesomeData,
greatData
}))
}

const promise = fetchAllData();

function ComponentA() {
const [awesomeData, setAwesomeData] = useState(null);
const [greatData, setGreatData] = useState(null);

useEffect(() => {
promise.then(({ awesomeData, greatData }) => {
setAwesomeData(awesomeData);
setGreatData(greatData);
});
}, []);

if (user === null) {
return <p>Loading data...</p>;
}

return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
);
}

function ComponentB({data}) {
return data === null ? <h2>Loading data...</h2> : <SomeComponent data={data} />;
}

Suspense 如何解决数据获取问题?

从本质上讲,fetch-on-render fetch-then-render 的主要问题归结为我们试图强制同步两个不同的流程,即数据获取流程和 React 生命周期。借助 Suspense,我们获得了一种不同类型的数据获取方法,即所谓的 render-as-you-fetch 方法。

const specialSuspenseResource = fetchAllDataSuspense();

function App() {
return (
<Suspense fallback={<h1>Loading data...</h1>}>
<ComponentA />
<Suspense fallback={<h2>Loading data...</h2>}>
<ComponentB />
</Suspense>
</Suspense>
);
}

function ComponentA() {
const data = specialSuspenseResource.awesomeData.read();
return <h1>{data.title}</h1>;
}

function ComponentB() {
const data = specialSuspenseResource.greatData.read();
return <SomeComponent data={data} />;
}

与之前实现的不同之处在于它允许组件在 React 到达它的那一刻启动数据获取。这甚至发生在组件渲染之前,并且 React 并没有就此停止。然后它继续评估组件的子树,并在等待数据获取完成时继续尝试渲染它。

这意味着 Suspense 不会阻塞渲染,这意味着子组件不必等待父组件完成后再发起其数据获取请求。React 尝试尽可能多地渲染,同时启动适当的数据获取请求。请求完成后,React 将重新访问相应的组件并使用新接收的数据相应地更新 UI。

Suspense有什么好处?

  • 尽早开始获取数据。Suspense 引入的 render-as-you-fetch 方法最大和最直接的好处是数据获取尽早启动。这意味着用户必须等待的时间更少,应用程序更快,这对任何前端应用程序都是普遍有益的。
  • 更直观的加载状态。使用 Suspense,组件不必再包含大量的 if 语句或单独跟踪状态来实现加载状态。相反,加载状态被集成到它所属的组件本身中。这使得组件更直观,通过保持加载代码接近相关代码,并且更可重用,因为加载状态包含在组件中。
  • 避免竞争条件。我没有在本文中深入讨论的现有数据获取实现的问题之一是竞争条件。在某些情况下,传统的 fetch-on-render 和 fetch-then-render 实现可能会导致竞争条件,具体取决于时间、用户输入和参数化数据请求等不同因素。主要的潜在问题是我们试图强制同步两个不同的进程,React 和数据获取。但是使用 Suspense,这可以更优雅、更集成地完成,从而避免了上述问题。
  • 更集成的错误处理。使用 Suspense,我们基本上已经为数据请求流创建了边界。最重要的是,由于 Suspense 使其与组件代码的集成更加直观,它允许 React 开发人员还为 React 代码和数据请求实现更集成的错误处理。

总结

React Suspense 已经被关注了 3 年多。但是随着 React 18 的发布,官方发布的时间越来越近了。除了并发渲染,它将是作为 React 版本的一部分发布的最大功能之一。就其本身而言,它可以将数据获取和加载状态实现提升到一个新的直观和优雅水平。

为了帮助你了解 Suspense 的基础知识,本文介绍了几个对其很重要的问题和方面。这涉及到 Suspense 是什么,为什么我们首先需要像 Suspense 这样的东西,它如何解决某些数据获取问题以及 Suspense 带来的所有好处。

· 阅读需 5 分钟

前言

React 18 支持对状态更新的自动批处理支持。 这有助于避免在 PromisesetTimeoutsetInterval、原生事件处理程序以及react事件处理程序中多次渲染状态更新。 因此,由于自动批处理,我们在React 应用程序中获得了开箱即用的性能改进。

什么是(自动)批处理?

批处理是将多个状态更新分组为单个更新的过程。 如果我们有多个调用来设置组件的状态,React 会将这些更新组合在一个更新调用(称为批处理)中,从而导致组件的一次重新渲染。 当 React 自动计算出这一点并批量更新状态时,它被称为自动批处理,下面让我们通过探究更新状态的各种方式来看看它是如何工作的?

React 17 及之前的事件处理程序的状态更新

让我们举个例子来了解在 React 17 中事件处理程序上的状态更新时渲染是如何发生的?

const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);

const handleClick = () => {
setCount(count + 1);
setShowModal((prev) => !prev);
// React renders once at the end (that's batching)
};

console.log('rendered component');

return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}

现在,我们可以看到有两个状态更新调用。 一个用于 setCount,另一个用于 setShowModal。 但是,react 确保最后只调用一个渲染。 如果更新不是批处理的,它会以不成熟方式渲染组件,导致 UI 闪烁。 即我们希望我们的组件仅在更新计数和更新 showModal 标志后才呈现。

React 17 及之前的 Promise 和原生事件处理程序的状态更新

自动批处理不适用于promise/非react处理程序(如 setTimeoutsetInterval 等)中的状态更新。

const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);

const handleClick = () => {
console.log('fetch called');

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => {
setCount(count + 1); // Re-render is called
setShowModal((prev) => !prev); // Re-render is called
})
};

console.log('rendered component');

return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me now</button>
</div>
);
}

通常,我们调用 API 请求来获取某些内容并根据 API 请求的响应在回调中执行状态更新。 正如我们在上面的示例中看到的,有 2 次调用来设置回调中的状态,导致 2 次重新渲染。 这是一个性能瓶颈。 这可能会导致 UI 闪烁,从而呈现部分状态更新的结果。

React 18+ 中 Promise 和原生事件处理程序的状态更新

React 通过为 PromisesetTimeout 和 setInterval、原生事件处理程序以及默认的 react 事件处理程序中的状态更新提供自动批处理支持来解决此问题。 注意:对于这个例子,我们已经更新了如下所示的 reactreact-dom 库版本。

https://unpkg.com/react@18.0.0-beta-24dd07bd2-20211208/umd/react.development.js
https://unpkg.com/react-dom@18.0.0-beta-24dd07bd2-20211208/umd/react-dom.development.js

如果我们采用与上面给出的相同示例,我们可以在下面看到渲染的数量。

const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);

const handleClick = () => {
console.log('fetch called');

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => {
setCount(count + 1);
setShowModal((prev) => !prev);
// React 18 renders once at the end (that's automatic batching)
})
};

console.log('rendered component');

return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}

注意:我们需要将渲染升级为createRoot。 对于上面给出的示例,我们渲染了 Counter 组件,如下所示。 我们使用 ReactDOM.createRoot 创建了根,然后在其上渲染了 Counter 组件。

· 阅读需 14 分钟

前言

目前 React 生态系统中最大的话题是 React 18 及其备受期待的并发渲染功能的完整发布。 2021 年 6 月,React 团队宣布了 React 18 的计划以及即将发生的事情。 2021年 12 月,React Conf 2021 的主题是所有新发布的并发渲染功能。

与 React 18 一起发布的几个新 API 允许用户充分利用 React 的并发渲染功能。 这些hook是:

本文将介绍这三个新 API、它们的用例、它们解决的问题、添加它们的原因以及它们如何集成到并发渲染领域。

the-usesyncexternalstore-hook

在 React v16.14.0 中引入的用于适应并发渲染的 API 之一是 useMutableSource,它旨在允许 React 组件在并发渲染期间安全有效地与外部可变源集成。

Hook 将附加到数据源,等待更改,并相应地安排更新。 所有这一切都会以一种防止撕裂的方式发生,即当出现视觉不一致时,因为同一状态有多个值。

这对于新的并发渲染特性来说是一个特别突出的问题,因为状态流可以很快地交织在一起。 然而,采用 useMutableSource 被证明是困难的,原因如下:

  1. Hook 是异步的 Hook 不知道如果选择器函数的结果值发生变化,它是否可以重用它。 唯一的解决方案是重新订阅提供的数据源并再次检索快照,这可能会导致性能问题,因为它发生在每次渲染上。

对于用户和库(如 Redux),这意味着他们必须记住项目中的每个选择器,并且无法内联定义选择器函数,因为它们的引用不稳定。

  1. 它必须处理外部状态 最初的实现也有缺陷,因为它必须处理 React 之外的状态。 这意味着由于其可变性,状态可能随时更改。

因为 React 试图以异步方式解决这个问题,这有时会导致 UI 的可见部分被替换为备用,从而导致次优的用户体验。

所有这一切都使得库维护者的迁移变得痛苦,并且对开发人员和用户来说都是次优的体验。

使用 useSyncExternalStore 解决这些问题

为了解决这些问题,React 团队更改了底层实现并将 Hook 重命名为 useSyncExternalStore 以正确反映其行为。 这些变化包括:

  • 每次选择器(用于快照)更改时都不会重新订阅外部源——相反,React 将比较选择器的结果值,而不是选择器函数,以决定是否再次检索快照,以便用户可以定义 选择器内联而不会对性能产生负面影响
  • 每当外部存储发生更改时,生成的更新现在始终是同步的,这可以防止 UI 被替换为回退

唯一的要求是 getSnapshot Hook 参数的结果值需要是引用稳定的。 React 在内部使用它来确定是否需要检索新快照,因此它需要是不可变值或记忆/缓存对象。

为了方便起见,React 将提供一个附加版本的 Hook,它自动支持对 getSnapshot 的结果值的记忆。

如何使用 useSyncExternalStore

// Code illustrating the usage of `useSyncExternalStore`.
// Source: <https://github.com/reactwg/react-18/discussions/86>

import {useSyncExternalStore} from 'react';

// React will also publish a backwards-compatible shim
// It will prefer the native API, when available
import {useSyncExternalStore} from 'use-sync-external-store/shim';

// Basic usage. getSnapshot must return a cached/memoized result
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);

// Code illustrating the usage of the memoized version.
// Source: <https://github.com/reactwg/react-18/discussions/86>

// Name of API is not final
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';

const selection = useSyncExternalStoreWithSelector(
store.subscribe,
store.getSnapshot,
getServerSnapshot,
selector,
isEqual
);

The useId Hook

在服务器端运行 React

长期以来,一个 React 项目只在客户端运行。简而言之,这意味着所有代码都被发送到用户的浏览器(客户端),然后浏览器负责向用户呈现和显示应用程序。

React 作为一个整体一直在向服务器端渲染(SSR)领域扩展。在 SSR 中,服务器负责根据 React 代码生成 HTML 结构。而不是所有的 React 代码,只有 HTML 被发送到浏览器。

然后,浏览器只负责采用该结构并通过渲染组件、在其上添加 CSS 并将 JavaScript 附加到它来使其具有交互性。这个过程称为水合作用。

hydration 最重要的要求是服务器和客户端生成的 HTML 结构必须匹配。如果他们不这样做,浏览器就无法确定它应该对结构的特定部分做什么,这会导致不正确地呈现或非交互式 UI。

这在依赖于标识符的特性中尤为突出,因为它们必须在两边都匹配,例如在生成唯一样式类名称和可访问性标识符时。

useID Hook 的演进

为了解决这个问题,React 最初引入了 useOpaqueIdentifier Hook,但不幸的是,它也存在一些问题:

在不同的环境中,Hooks 会产生不同的输出(不透明):

服务器端:它会产生一个字符串 客户端:它会产生一个特殊的对象,必须直接传递给 DOM 属性 这意味着 Hook 只能生成一个标识符,并且不可能动态生成新的 ID,因为它必须遵守 Hook 的规则。 因此,如果您的组件需要 X 个不同的标识符,它必须在不同的时间调用 Hook X,这在实践中显然不能很好地扩展。

// Code illustrating the way `useOpaqueIdentifier` handles the need for N identifiers in a single component, namely calling the hook N times.
// Source: <https://github.com/facebook/react/pull/17322#issuecomment-613104823>

function App() {
const tabIdOne = React.unstable_useOpaqueIdentifier();
const panelIdOne = React.unstable_useOpaqueIdentifier();
const tabIdTwo = React.unstable_useOpaqueIdentifier();
const panelIdTwo = React.unstable_useOpaqueIdentifier();

return (
<React.Fragment>
<Tabs defaultValue="one">
<div role="tablist">
<Tab id={tabIdOne} panelId={panelIdOne} value="one">
One
</Tab>
<Tab id={tabIdTwo} panelId={panelIdTwo} value="one">
One
</Tab>
</div>
<TabPanel id={panelIdOne} tabId={tabIdOne} value="one">
Content One
</TabPanel>
<TabPanel id={panelIdTwo} tabId={tabIdTwo} value="two">
Content Two
</TabPanel>
</Tabs>
</React.Fragment>
);
}

某些可访问性 API(如 aria-labelledby)可以通过空格分隔的列表接受多个标识符,但由于 Hook 的输出被格式化为不透明的数据类型,它总是必须直接附加到 DOM 属性。这意味着无法正确使用上述可访问性 API。

为了解决这个问题,实现已更改并重命名为 useId。这个新的 Hook API 在 SSR 和 hydration 期间生成稳定的标识符以避免不匹配。在服务器渲染的内容之外,它回退到一个全局计数器。

与使用 useOpaqueIdentifier 创建不透明数据类型(服务器中的特殊对象和客户端中的字符串)不同,useId Hook 会在两侧生成非透明字符串。

这意味着如果我们需要 X 个不同的 ID,就没有必要再调用 Hook X 次了。相反,组件可以调用 useId 一次并将其用作整个组件所需的标识符的基础(例如,使用后缀),因为它只是一个字符串。这解决了 useOpaqueIdentifier 中存在的两个问题。

如何使用 useID

下面的代码示例说明了如何根据我们上面讨论的内容使用 useId。 因为 React 生成的 ID 是全局唯一的,并且后缀是本地唯一的,所以动态创建的 ID 也是全局唯一的——因此不会导致任何水合不匹配。

// Code illustrating the improved way in which `useId` handles the need for N identifiers in a single component, namely calling the hook once and creating them dynamically.
// Source: <https://github.com/reactwg/react-18/discussions/111>

function NameFields() {
const id = useId();
return (
<div>
<label htmlFor={id + '-firstName'}>First Name</label>
<div>
<input id={id + '-firstName'} type="text" />
</div>
<label htmlFor={id + '-lastName'}>Last Name</label>
<div>
<input id={id + '-lastName'} type="text" />
</div>
</div>
);
}

The useInsertionEffect Hook . CSS-in-JS 库的问题

最后一个将在 React 18 中添加的 Hook——我们将在这里讨论——是 useInsertionEffect。这个与其他的略有不同,因为它的唯一目的对于动态生成新规则并在文档中插入带有 <style> 标记的 CSS-in-JS 库很重要。

在某些场景下,<style>标签需要在客户端生成或编辑,如果不仔细处理,可能会导致并发渲染的性能问题。这是因为在添加或删除 CSS 规则时,浏览器必须检查这些规则是否适用于现有树。它必须重新计算所有样式规则并重新应用它们——而不仅仅是改变的规则。如果 React 发现另一个组件也生成了新规则,那么同样的过程将再次发生。

这实际上意味着在 React 渲染时,必须针对每一帧的所有 DOM 节点重新计算 CSS 规则。虽然你很有可能不会遇到这个问题,但它的规模并不大。

从理论上讲,有一些方法主要与时间有关。这个时间问题的最佳解决方案是在对 DOM 进行所有其他更改的同时生成这些标签,就像 React 库那样。最重要的是,它应该发生在任何尝试访问布局之前以及所有内容呈现给浏览器进行绘制之前。

这听起来像是 useLayoutEffect 可以解决的问题,但问题是同一个 Hook 将用于读取布局和插入样式规则。这可能会导致不希望的行为,例如在一次通过中多次计算布局或读取不正确的布局。

useInsertionEffect 如何解决并发渲染问题

为了解决这个问题,React 团队引入了 useInsertionEffect Hook。 它与 useLayoutEffect Hook 非常相似,但它无法访问 DOM 节点的 refs。

这意味着只能插入样式规则。 它的主要用例是插入全局 DOM 节点,如 <style> 或 SVGs <defs>。 由于这仅与在客户端生成标签有关,因此 Hook 不会在服务器上运行。

// Code illustrating the way `useInsertionEffect` is used.
// Source: <https://github.com/reactwg/react-18/discussions/110>

function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}

function Component() {
let className = useCSS(rule);
return <div className={className} />;
}

React 18 最令人期待的特性是它的并发渲染特性。 随着团队的宣布,我们收到了新的 API,允许用户根据他们的用例采用并发渲染功能。 虽然有些是全新的,但有些是基于社区反馈的先前 API 的改进版本。

在本文中,我们介绍了三个最新的 API,即 useSyncExternalStore、useId 和 useInsertionEffect Hooks。 我们查看了它们的用例、它们解决的问题、与以前的版本相比为什么需要进行某些更改,以及它们用于并发渲染的目的。

React 18 充满了新功能,绝对值得期待!

· 阅读需 7 分钟

前言

目前 React 领域最热门的话题是 React 18 版本。 特别是,该版本将引入一组所谓的并发渲染功能。 这些特性允许开发者选择 React 的并发渲染机制。 这种机制为 React 开发人员提供了一个全新的机会来控制和优化最终用户的体验。 这绝对是自 hooks 以来,我们在 React 世界中将收到的最令人兴奋的事情之一。

因此,你很可能以前听说过并发渲染。 可能是关于它的文章、围绕它的 API,或者 React 18 将为它带来什么。 但是,你可能会对并发渲染的基础知识感到疑惑。 究竟什么是并发渲染,为什么我们真的需要它?

为了帮助你理解,本文将讨论这些问题。 通过研究它的目的、它试图解决什么问题以及它是如何解决它的,你将获得有关并发渲染主题的基础知识。

为什么我们需要并发渲染?

React 当前形式的问题之一是所有状态更新都是同步的。 这意味着 React 只能一一处理它们。 在许多用例和现实生活场景中,这非常好,不会对用户体验施加任何限制。

但是在 React 想要获取与其当前正在处理的状态更新不同的状态更新的情况下,现在这显然是不可能的。 React 在启动后无法中断、暂停或放弃渲染更新——这是一个阻塞的进程。

从本质上讲,这对优化用户体验的过程设置了上限。 虽然很高,但还是有上限的。 每个状态更新都被视为同等重要,即使这不适用于用户体验。 某些更新可能比其他更新具有更高的优先级或紧迫性。 与可能的情况相比,不能这样做实际上会对用户体验产生巨大的负面影响,这是次优的。

什么是并发渲染?

并发渲染是一组功能,允许你的 React 项目选择所谓的可中断渲染。 与之前 React 被阻塞的渲染过程相反,这使得渲染过程可以从 React 端中断,这正是并发渲染的用武之地。这为 React 开发人员进一步提升 React 应用程序的用户体验开辟了许多新的可能性。

它允许 React 一次处理多个状态更新。 然而,这并不意味着 React 会突然同时执行所有排队状态更新。 相反,选择并发渲染允许 React 考虑其最佳行动方案。 幸运的是,这也是我们作为开发人员可以控制的事情。

假设 React 当前正在处理状态更新并且有一个不同的更新进来,那么 React 可以根据变量的因素做出不同的决定。 如果新的传入状态更新被标记为同等或不那么紧急,那么与之前的渲染过程相比没有任何变化。 React 将像往常一样继续当前状态更新。 完成后,它将获取新的状态更新。

但是如果新传入的状态更新被标记为更紧急,那么 React 可以决定暂停当前状态更新并首先处理传入的更新。 在完成新的更紧急的状态更新后,React 会回到原来的状态更新。 如果它确定有必要恢复它,它会这样做。 如果事实证明状态更新现在无关紧要,它可以决定完全放弃它。

下一步是什么?

本文简要介绍了 React 18 将为 React 开发领域带来的最激动人心的功能之一,即并发渲染,并让你快速了解整个主题。 使用本文中的知识,你应该知道什么是并发渲染,了解它试图解决的问题,并大致了解它的工作原理。

幸运的是,并发渲染并不止于此。 虽然并发渲染还有很多方面需要理解或深入研究,但本文作为介绍以进入整个主题,并允许你从这里开始进一步探索 React 18。

下面准备了一些资料 这里介绍了 React 18 中引入的三个新 API。所有这些 API 都是允许某些开发人员在某些场景中选择并发渲染的hook。 官方的 React 18 公告是了解更多关于 React 18、不同特性、如何采用它以及关于即将发布的 React 版本的所有信息的好地方。 React 工作组是了解更多技术方面、获得更多指导、了解不同 API 和特性背后的思维过程以及总体上更深入地了解 React 18 中所有内容的好地方。 这就是全部! 现在你已经牢牢掌握了并发渲染的主题,在 React 18 中为你打开了一个全新的世界供你探索。走出去,探索并享受这个新的冒险!

· 阅读需 15 分钟

前言

本文我们讨论如何在 React Router 6 中使用搜索参数。搜索参数是一项强大的功能,它使你能够捕获 URL 中的状态。通过在 URL 中包含状态,你可以与其他人实现共享。例如,如果应用程序显示产品目录,开发人员将使用户能够搜索它。在 React 中,这将转换为项目列表(此处为:产品)和用于过滤它们的 HTML 输入字段。

现在,React 开发人员很有可能会使用 React 的 useState Hook 来管理这种搜索状态。这对这个用户来说很好,但不适合与其他用户共享。

因此,一个不错的方式是在 URL 中管理搜索状态,因为这样搜索状态就可以与其他用户共享。如果一个用户按标题(例如“Rust”)搜索项目列表,则搜索参数将作为键值对附加到 URL,例如 /bookshelf?title=Rust,因为可以与另一个用户共享。所以获得链接的其他用户将在其页面上看到相同的过滤项目列表。

React Router 从状态到 URL

首先,我们将实现上一个所设想的那样,其中有一个项目列表,并通过 HTML 输入字段进行搜索。我们不会使用 React 的 useState Hook 来捕获搜索状态,而是使用 React Router 来获取可共享的 URL。 App 组件如下所示,类似于前面提到的 React Router 教程中的 App 组件:

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/bookshelf">Bookshelf</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="bookshelf" element={<Bookshelf />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

虽然 Home 和 NoMatch 组件只是具有任何实现的占位符组件,但我们将关注 Bookshelf 组件,它将Books显示为列表组件。这些Books示例数据可以从远程 API(或模拟 API)获取:

const Bookshelf = () => {
const books = [
{
title: 'The Road to Rust',
isCompleted: false,
},
{
title: 'The Road to React',
isCompleted: true,
},
];

return (
<>
<h2>Bookshelf</h2>

<ul>
{books.map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

为了使用户能够通过不区分大小写的标题匹配来过滤此列表,我们使用 React 的 useState Hook 和 HTML 输入字段。最后,事件处理程序将从输入字段中读取值并将其写入状态:

const byTitle = (title) => (book) =>
book.title.toLowerCase().includes((title || '').toLowerCase());

const Bookshelf = () => {
const books = [...];

const [title, setTitle] = React.useState('');

const handleTitle = (event) => {
setTitle(event.target.value);
};

return (
<>
<h2>Bookshelf</h2>

<input type="text" value={title} onChange={handleTitle} />

<ul>
{books.filter(byTitle(title)).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

这就是就是“在 React 中使用状态”的版本。接下来,我们要使用 React Router 来在 URL 中捕获此状态。React Router 为我们提供了 useSearchParams hook,它几乎可以用来替代 React 的 useState hook:

import * as React from 'react';
import {
Routes,
Route,
Link,
useSearchParams,
} from 'react-router-dom';

...

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useSearchParams();

const handleTitle = (event) => {
setSearch({ title: event.target.value });
};

return (
<>
<h2>Bookshelf</h2>

<input
type="text"
value={search.get('title')}
onChange={handleTitle}
/>

<ul>
{books.filter(byTitle(search.get('title'))).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

由于以下两点,它不能直接替代 React 的 useState Hook。首先,它对一个对象而不是字符串进行操作,因为一个 URL 可以有多个搜索参数(例如 /bookshelf?title=Rust&rating=4),因此每个搜索参数都成为该对象中的一个属性(例如{ title: 'Rust', rating: 4 })。

如果我们将 React 的 useState Hook 与对象而不是字符串一起使用,它本质上与我们之前的实现类似:

const [search, setSearch] = React.useState({ title: '' });

然而,即使 useSearchParams 返回的有状态值是对象类型(typeof search === 'object'),它仍然不能像单纯的 JavaScript 对象数据结构那样访问,因为它是 URLSearchParams 的一个实例。因此我们需要调用它的 getter 方法(例如 search.get('title'))。

其次,React Router 的 useSearchParams Hook 不接受初始状态,因为初始状态来自 URL。因此,当用户与搜索参数(例如 /bookshelf?title=Rust)共享 URL 时,另一个用户将从 React Router 的 Hook 获得 { title: 'Rust' } 作为初始状态。当应用程序将用户导航到带有搜索参数且设置了可选搜索参数的路线时,也会发生同样的情况。

这就是使用状态的 URL 而不是使用 React 的状态管理 Hook 之一。它极大地改善了用户体验,因为 URL 变得更加特定于用户在页面上看到的内容。因此,这个特定的 URL 可以与其他用户共享,他们将看到具有相同 UI 的页面。

URLSEARCHPARAMS 转换为对象

如果你在处理 React Router 的 useSearchParams Hook 时不想使用 URLSearchParams,你可以编写一个自定义hook,它返回一个 JavaScript 对象而不是 URLSearchParams 的实例:

const useCustomSearchParams = () => {
const [search, setSearch] = useSearchParams();
const searchAsObject = Object.fromEntries(
new URLSearchParams(search)
);

return [searchAsObject, setSearch];
};

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useCustomSearchParams();

const handleTitle = (event) => {
setSearch({ title: event.target.value });
};

return (
<>
<h2>Bookshelf</h2>

<input
type="text"
value={search.title}
onChange={handleTitle}
/>

<ul>
{books.filter(byTitle(search.title)).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

然而,这个自定义hook应该有一点不足,因为它不适用于重复键(例如带有 ?editions=1&editions=3 的数组搜索参数)和使用复杂 URL 时的其他边界情况。

一般来说,仅使用 React Router 的 useSearchParams Hook(或这个自定义的 useCustomSearchParams hook)并不能为你提供 URL 状态管理的完整体验,因为它仅可用于字符串而不能用于其他数据类型。我们将在接下来的部分中探讨这一点以及如何解决这个问题。

搜索参数与保留数据类型

并非所有状态都只包含字符串。在前面使用 React Router 的搜索参数的例子中,我们使用了一个字符串(这里是:title),它被编码到 URL 中。当从 URL 解码这个字符串时,我们将默认得到一个字符串——这在我们的例子中有效,因为我们需要一个字符串。但是其他原始数据类型如数字或布尔值呢?更不用说复杂的数据类型,例如数组。

为了探索解决这个,我们将通过实现一个复选框来继续之前的示例。我们将使用这个复选框组件并将其连接到 React Router 的搜索参数:

const bySearch = (search) => (book) =>
book.title
.toLowerCase()
.includes((search.title || '').toLowerCase()) &&
book.isCompleted === search.isCompleted;

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useCustomSearchParams();

const handleTitle = (event) => {
setSearch({ title: event.target.value });
};

const handleIsCompleted = (event) => {
setSearch({ isCompleted: event.target.checked });
};

return (
<>
<h2>Bookshelf</h2>

<input
type="text"
value={search.title}
onChange={handleTitle}
/>

<Checkbox
label="Is Completed?"
value={search.isCompleted}
onChange={handleIsCompleted}
/>

<ul>
{books.filter(bySearch(search)).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

在浏览器中实验以下。你将看到对 isCompleted 布尔值的搜索不起作用,因为来自我们的搜索对象的 isCompleted 被表示为一个字符串,如“true”或“false”。我们可以通过增强我们的自定义hook来规避这一点:

const useCustomSearchParams = (param = {}) => {
const [search, setSearch] = useSearchParams();
const searchAsObject = Object.fromEntries(
new URLSearchParams(search)
);

const transformedSearch = Object.keys(param).reduce(
(acc, key) => ({
...acc,
[key]: param[key](acc[key]),
}),
searchAsObject
);

return [transformedSearch, setSearch];
};

const PARAMS = {
BooleanParam: (string = '') => string === 'true',
};

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useCustomSearchParams({
isCompleted: PARAMS.BooleanParam,
});

...

return (...);
};

本质上,新版本的自定义hook采用具有可选转换功能的对象。它遍历每个转换函数,如果找到转换函数和搜索参数之间的匹配项,则将该函数应用于搜索参数。在这种情况下,我们将字符串布尔值(“true”或“false”)转换为实际的布尔值。如果没有找到匹配项,它只返回原始搜索参数。因此我们不需要标题的转换函数,因为它是一个字符串并且可以继续为字符串。

通过拥有自定义hook的实现细节,我们还可以创建其他转换器函数(例如 NumberParam),从而填补缺失数据类型转换(例如数字)的空白:

const PARAMS = {
BooleanParam: (string = '') => string === 'true',
NumberParam: (string = '') => (string ? Number(string) : null),
// other transformation functions to map all data types
};

开源组件中use-query-params这个库完美的解决这个问题。

React Router 使用搜索参数

use-query-params 库非常适合将复杂的 URL 用作超越字符串的状态的用例。在本节中,我们将探索 use-query-params 库,从而摆脱我们自定义的 useSearchParams hook。

自己按照库的安装说明进行操作。你需要在命令行上安装该库并在 React 项目的根级别实例化它:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';

import App from './App';

ReactDOM.render(
<BrowserRouter>
<QueryParamProvider ReactRouterRoute={Route}>
<App />
</QueryParamProvider>
</BrowserRouter>,
document.getElementById('root')
);

然而, use-query-params 还没有正确适应 React Router 6。因此,你可能会看到以下错误弹出:“<Route> 仅用作 <Routes> 元素的子元素,永远不会直接呈现。请将你的 <Route> 包装在 <Routes> 中。”。因此,在根级别调整你的代码:

import React from 'react';
import ReactDOM from 'react-dom';
import {
BrowserRouter,
useNavigate,
useLocation,
} from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';

import App from './App';

const RouteAdapter = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();

const adaptedHistory = React.useMemo(
() => ({
replace(location) {
navigate(location, { replace: true, state: location.state });
},
push(location) {
navigate(location, { replace: false, state: location.state });
},
}),
[navigate]
);
return children({ history: adaptedHistory, location });
};

ReactDOM.render(
<BrowserRouter>
<QueryParamProvider ReactRouterRoute={RouteAdapter}>
<App />
</QueryParamProvider>
</BrowserRouter>,
document.getElementById('root')
);

现在你可以使用 use-query-params 在 React 中进行强大的 URL 状态管理。你所要做的就是使用新的 useQueryParams 钩子。另请注意,与我们的自定义钩子相比,你还需要“转换”字符串搜索参数:

import * as React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import {
useQueryParams,
StringParam,
BooleanParam,
} from 'use-query-params';

...

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useQueryParams({
title: StringParam,
isCompleted: BooleanParam,
});

...

return (...);
};

你还可以提供合理的默认值。例如,此时在没有搜索参数的情况下导航到 /bookshelf 时,title 和 isComplete 将是未定义的。但是,如果你希望它们至少是标题的空字符串和 isComplete 的 false,你可以提供这些默认值,例如:

import * as React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import {
useQueryParams,
StringParam,
BooleanParam,
withDefault
} from 'use-query-params';

...

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useQueryParams({
title: withDefault(StringParam, ''),
isCompleted: withDefault(BooleanParam, false),
});

...

return (...);
};

还有一件值得注意的事情要提到:目前,use-query-params 使用默认的“push in”模式,这意味着每次附加搜索参数时,它不会覆盖其他搜索参数。因此,你在更改其中之一的同时保留所有搜索参数。但是,如果这不是你想要的行为,你还可以更改模式(例如,更改为“push”),这样将不再保留以前的搜索参数(尽管这在我们的场景中没有意义):

const Bookshelf = () => {
...

const handleTitle = (event) => {
setSearch({ title: event.target.value }, 'push');
};

const handleIsCompleted = (event) => {
setSearch({ isCompleted: event.target.checked }, 'push');
};

...

return (...);
};

除了我们在这里使用的两种数据类型转换之外,还有对数字、数组、对象等的转换。例如,如果你希望在 React 中有一个可选择的表,你可能希望将表中的每个选定行表示为数组中的标识符(在 use-query-params 中,它是 ArrayParam 转换)映射到实际 URL .然后你可以与另一个用户共享此 URL,该用户将从所选行开始。

使用 URL 作为状态是改善用户体验的方式。在处理单个或多个字符串状态时,React Router 的搜索参数为你提供了一个很好的体验。但是,一旦你想保留映射到 URL 的数据类型,你可能希望使用诸如 use-query-params 之类的库在 React 中进行复杂的 URL 状态管理。

参考

use-query-params

· 阅读需 11 分钟

前言

本文教你如何在 React Router 6 中使用嵌套路由。嵌套路由是一个强大的功能。虽然大多数人认为 React Router 只会在页面之间路由使用,但它也允许用户根据当前路由交换视图的特定片段。例如,在用户页面上,会显示多个选项卡(例如个人资料、帐户)以浏览用户信息。通过单击这些选项卡,浏览器中的 URL 会发生变化,但不会替换整个页面,只会替换选项卡的内容。 下面我们将使用 React Router 重新创建这个场景。为了说明这是如何工作的,以及如何自己在 React 中逐步实现嵌套路由,我们将从以下示例开始:

import { Routes, Route, Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user" element={<User />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

在这个函数组件中,我们使用了 React Router 中的 Link 和 Route 组件,用于 home/ 和 user/ 路由。此外,我们有一个加载了 Home 组件的所谓的索引路由和一个加载了 NoMatch 组件的所谓的 No Match 路由。两者都作为备选路线。从这里开始,我们将了解嵌套路由的概念。

React Router 中的嵌套路由

我们将继续处理 User 组件,这是我们希望通过选项卡进行嵌套路由的地方。因此,我们将实例化一组新的 Link 组件(将是我们的无样式选项卡),用于将用户导航到他们的个人资料和他们的帐户。

const User = () => {
return (
<>
<h1>User</h1>

<nav>
<Link to="/user/profile">Profile</Link>
<Link to="/user/account">Account</Link>
</nav>
</>
);
};

我们在这里使用绝对路径将用户从他们的个人资料导航到他们的帐户,反之亦然,但是,我们也可以使用相对路径作为最佳实践。因为 User 组件位于 /user 路由中,所以 Link 组件可以预测它们的父路由(这里是:/user),并且只需将相对路径(这里:profile 和 account)附加到它(例如 /user/profile):

const User = () => {
return (
<>
<h1>User</h1>

<nav>
<Link to="profile">Profile</Link>
<Link to="account">Account</Link>
</nav>
</>
);
};

此时,当我们尝试在 React 应用程序中单击这些链接之一时,我们将被困在我们的 No Match Route 中。这告诉我们,我们还没有将这些路由(此处:/user/profile 和 /user/account)映射到任何实际的路由组件。因此,我们将这两个新路由作为所谓的嵌套路由添加到我们的 /user 路由中:

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user" element={<User />}>
<Route path="profile" element={<Profile />} />
<Route path="account" element={<Account />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

Route 组件现在以一对一的关系映射到 Link 组件。但是,可以有多个 Link 组件链接到同一个路由,因此它实际上是一对多的关系。

在浏览器中对此进行测试时,我们将看到仅显示 User 组件,而不会显示其嵌套的 Profile 组件,也不会显示其嵌套的 Account 组件。我们缺少 React Router 的关键 Outlet 组件:

import { Routes, Route, Link, Outlet } from 'react-router-dom';

...

const User = () => {
return (
<>
<h1>User</h1>

<nav>
<Link to="profile">Profile</Link>
<Link to="account">Account</Link>
</nav>

<Outlet />
</>
);
};

Outlet 组件从父 Routes 的 Route 组件集合中使用其各自的组件(此处为 Profile 或 Account 组件)呈现匹配的子路由。 如果没有 /profile 和 /account 路由匹配(例如 /user/settings),你将只看到 User 组件出现。为避免这种情况,你可以添加索引和无匹配路由的组合。之后,默认路由将是 /profile 路由:

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user" element={<User />}>
<Route index element={<Profile />} />
<Route path="profile" element={<Profile />} />
<Route path="account" element={<Account />} />
<Route path="*" element={<NoMatch />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

虽然 User 组件总是将选项卡呈现为导航,但其内容(Outlet)被匹配的嵌套路由(基于 /user/profile 或 /user/account 路由的 Profile 或 Account 组件)替换。如果在访问 /user 路由时这些路由都不匹配,应用程序将显示 Profile 组件(如果路由与 /user 完全匹配)或 NoMatch 组件(如果路由不匹配,例如 /user/setting)出现。

React router 中的 动态嵌套路由

在嵌套路由的下一个示例中,我们将从 App 组件中开始。这次我们不想像之前那样渲染静态嵌套路由(例如 /user/profile),而是基于标识符的动态嵌套路由(例如 /users/1 用于显示具有标识符 1 并因此匹配此路由的用户)。因此,我们将示例从单用户路由 (/user) 调整为多用户路由 (/user)。

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>

<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</>
);
};

Users 组件成为 React 中的列表组件,因为它遍历每个用户并为其返回 JSX。在这种情况下,它不仅仅是一个列表,因为我们将 React Router 的 Link 组件添加到组合中。 Link 组件中的相对路径提示相应的嵌套(此处:/${user.id} 嵌套在 /users 中)但动态(此处:/${user.id})路由:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>
{users.map((user) => (
<li key={user.id}>
<Link to={user.id}>
{user.fullName}
</Link>
</li>
))}
</ul>
</>
);
};

通过拥有这个新的动态嵌套路由,我们需要在 App 组件中为它创建一个匹配的嵌套路由组件。首先,由于它是 /users 路由的所谓嵌套路由(或子路由),我们可以将它嵌套在相应的父路由组件中。此外,由于它是所谓的动态路由,它使用定义为 :userId 的动态路由,而用户的标识符则动态匹配(例如,id 为 '1' 的用户将与 /users/1 匹配):

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<h1>React Router</h1>

<nav>...</nav>

<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />}>
<Route path=":userId" element={<User />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

这样,User 组件就变成了 Users 组件的嵌套路由。因此,再次在 Outlet 组件的帮助下渲染其嵌套路由是用户组件的责任——再次渲染匹配的子路由:

import { Routes, Route, Link, Outlet } from 'react-router-dom';

...

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>...</ul>

<Outlet />
</>
);
};

接下来,我们将声明缺少的 User 组件,只要用户的标识符在 URL 中匹配,该组件就会通过 Users 组件中的 Outlet 嵌套。在这个新组件中,我们可以使用 React Router 的 useParams Hook 从 URL 中获取相应的 userId(等于 :userId):

import {
...
useParams,
} from 'react-router-dom';

...

const User = () => {
const { userId } = useParams();

return (
<>
<h2>User: {userId}</h2>

<Link to="/users">Back to Users</Link>
</>
);
};

我们已经看到了如何通过将一个 Route 组件(或多个 Route 组件)嵌套在另一个 Route 组件中来创建嵌套路由。前者是嵌套的子路由,后者是渲染封闭组件的父路由,该组件必须使用 Outlet 组件来渲染实际匹配的子路由。

此外,我们还看到了如何通过在路由的路径属性中使用冒号(例如:userId)来创建动态路由。本质上, :userId 充当任何标识符的星号。在我们的例子中,我们使用 Link 组件将用户导航到 /users/:userId 路由,其中​​ :userId 代表实际用户的标识符。最后,我们总是可以通过使用 React Router 的 useParams Hook 从 URL 中获取动态路径(称为参数或 params)。

如果你碰巧将 React Router 用于你的 React 应用程序,嵌套路由可以通过让你的用户访问你的应用程序非常特定的部分,同时将这些部分作为 URL 共享,从而极大地提升你的用户体验。

· 阅读需 22 分钟

前言

不久前,React Router 库更新到了第 6 版,随之而来的是一些有趣的变化,本文将讲述React Router 6的一些新特性及使用的案例。 接下来是一些准备工作:

  1. 首先需要创建一个新的 React 项目(例如 create-react-app)。然后,按照官方文档安装 React Router。
    yarn add react-router-dom@latest    
    我们这里安装的是6.0.2版本。
  2. 第一个实现细节将告诉我们的 React 应用程序我们想要使用 React Router。因此,在 React 项目的顶级文件(例如 index.js)中导入 Router 组件,其中 React 使用 ReactDOM API 挂载到 HTML:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import App from './App';

ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);

从这里开始,我们将在 App.js 文件中继续我们的实现。

匹配路由

首先,我们将使用 React Router 的 Link 组件在我们的 App 组件中实现导航。我不建议使用内联样式,因此请根据你的 React 项目选择合适的样式策略和样式方法:

import { Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>
<Navigation />
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

当你在浏览器中启动 React 应用程序时,你应该能够单击两个 Link 组件,这些组件应该将你导航到各自的路由。单击这些链接时,可通过检查浏览器的当前 URL 来确认。接下来,我们需要使用 React Router 的 Route 组件将路由映射到实际渲染:

import { Routes, Route, Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<Navigation />

<Routes>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Routes>
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

你可以通过检查它们各自的 to 和 path 属性来查看 Link 和 Route 组件之间的直接匹配。当路由匹配时,每个 Route 组件都会渲染一个 React 元素。由于我们在这里渲染一个 React 元素,我们也可以传递 React props。缺少的是相应功能组件的声明:

const Home = () => {
return (
<main style={{ padding: '1rem 0' }}>
<h2>Home</h2>
</main>
);
};

const Users = () => {
return (
<main style={{ padding: '1rem 0' }}>
<h2>Users</h2>
</main>
);
};

返回浏览器时,你应该能够在看到 Home 和 Users 组件的同时从一个页面导航到另一个页面(此处:从 /home 到 /users 路由)。基本上这就是 React Router 的本质:设置 Link 组件并将它们与 Route 组件匹配。链接与路由是多对一的关系,因此你的应用程序中可以有多个链接链接到同一个路由。

布局路由、索引路由、无匹配路由

接下来,你将看到新的 Home 和 Users 组件如何共享相同的布局。作为 React 开发人员,直觉上我们会从 Home 和 Users 组件中提取一个带有样式的新组件,以避免重复。在这个新组件中,我们将使用 React 的 children 属性将组件组合在一起。第一步,将样式提取到它自己的组件中:

const Home = () => {
return (
<>
<h2>Home</h2>
</>
);
};

const Users = () => {
return (
<>
<h2>Users</h2>
</>
);
};

const Layout = ({ children }) => {
return <main style={{ padding: '1rem 0' }}>{children}</main>;
};

其次,在 App 组件中渲染它。通过使用 React 的子级,Layout 组件应该渲染匹配的封闭子路由:

import { Routes, Route, Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<Navigation />

<Routes>
<Layout>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Layout>
</Routes>
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

const Home = () => {
return (
<>
<h2>Home</h2>
</>
);
};

const Users = () => {
return (
<>
<h2>Users</h2>
</>
);
};

const Layout = ({ children }) => {
return <main style={{ padding: '1rem 0' }}>{children}</main>;
};

但是你会看到这在 React Router 中是不允许的,你会得到一个异常说:<Routes> 的所有组件子项必须是 <Route><React.Fragment>。解决此问题的一种常见方法是在每个组件中单独使用 Layout 组件(类似于我们之前使用的)或在每个 Route 组件中(如下例所示):

const App = () => {
return (
<>
...

<Routes>
<Route path="home" element={<Layout><Home /></Layout>} />
<Route path="users" element={<Layout><Users /></Layout>} />
</Routes>
</>
);
};

然而,这给 React 应用程序增加了不必要的冗余。因此,我们将使用所谓的 Layout Route,而不是复制 Layout 组件,它不是实际的路由,而只是一种方法,可以让一组 Route 中的每个 Route 组件的元素具有相同的周围样式:

const App = () => {
return (
<>
...

<Routes>
<Route element={<Layout />}>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
</>
);
};

如你所见,可以将 Route 组件嵌套在另一个 Route 组件中——而前者成为所谓的嵌套路由。现在不再在 Layout 组件中使用 React 的子组件,而是使用 React Router 的 Outlet 组件作为等效组件:

import { Routes, Route, Outlet, Link } from 'react-router-dom';

...

const Layout = () => {
return (
<main style={{ padding: '1rem 0' }}>
<Outlet />
</main>
);
};

本质上,Layout 组件中的 Outlet 组件插入了父路由(这里:Layout 组件)的匹配子路由(这里:Home 或 Users 组件)。毕竟,使用 Layout Route 可以帮助你为集合中的每个 Route 组件提供相同的布局(例如,CSS 样式,HTML 结构)。

从这里开始,你可以更进一步,将 App 组件的所有实现细节(标题、导航)移动到这个新的 Layout 组件中。此外,我们可以与 NavLink 组件交换链接,以实现所谓的活动链接——向用户显示当前活动的路线。因此,当将新的 NavLink 组件与函数一起使用时,我们可以访问其style和 className props中的 isActive 标志:

import { Routes, Route, Link, NavLink, Outlet } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<Navigation />

<Routes>
<Route element={<Layout />}>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

const Home = () => {
return (
<>
<h2>Home</h2>
</>
);
};

const Users = () => {
return (
<>
<h2>Users</h2>
</>
);
};

const Layout = () => {
const style = ({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
});

return (
<>
<h1>React Router</h1>

<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<NavLink to="/home" style={style}>Home</NavLink>
<NavLink to="/users" style={style}>Users</NavLink>
</nav>

<main style={{ padding: '1rem 0' }}>
<Outlet />
</main>
</>
);
};

接下来你可能已经注意到这个 React 应用程序缺少一个基本路由。虽然我们有 /home 和 /users 路由,但没有 / 路由。你也会在浏览器的开发人员工具中看到此警告:没有路由匹配位置“/”。因此,每当用户访问 / 路由时,我们都会创建一个所谓的索引路由作为回退。此回退路由的元素可以是新组件或任何已匹配的路由(例如,Home 应为路由 / 和 /home 呈现,如下例所示):

const App = () => {
return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
);
};

当父路由匹配但没有子路由匹配时,你可以将索引路由视为默认路由。接下来,如果用户导航到不匹配的路由(例如 /about),我们将添加一个所谓的 No Match Route(也称为 Not Found Route),它相当于网站的 404 页面:

const App = () => {
return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

const NoMatch = () => {
return (<p>There's nothing here: 404!</p>);
};

到目前为止,在使用 Routes 组件作为 Route 组件集合的容器时,通过使用 Layout Routes、Index Routes 和 No Match Routes 展示了 React Router 的其他最佳实践。如你所见,也可以将 Route 组件嵌套到 Route 组件中。下面我们接着了解有关嵌套路由的更多信息。

动态且嵌套的路由

接下来我们将用实现细节来装饰用户组件。首先,我们将在我们的 App 组件中初始化一个项目列表(这里是:用户)。该列表只是示例数据,但它也可以在 React 中从远程 API 获取。其次,我们将用户作为props传递给用户组件:

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

Users 组件成为 React 中的列表组件,因为它遍历每个用户并为其返回 JSX。在这种情况下,它不仅仅是一个列表,因为我们将 React Router 的 Link 组件添加到组合中。 Link 组件中的相对路径提示相应的动态(此处:/${user.id})尚未嵌套(此处:/${user.id} 嵌套在 /users 中)路由:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>
{users.map((user) => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>
{user.fullName}
</Link>
</li>
))}
</ul>
</>
);
};

通过拥有这个新的动态嵌套路由,我们需要在 App 组件中为它创建一个匹配的嵌套路由组件。首先,由于它是 /users 路由的所谓嵌套路由(或子路由),我们可以将它嵌套在相应的父路由组件中。此外,由于它是所谓的动态路由,它使用定义为 :userId 的动态路由,而用户的标识符则动态匹配(例如,id 为 '1' 的用户将与 /users/1 匹配):

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />}>
<Route path=":userId" element={<User />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

之前,当我们介绍将 /home 和 /users 路由作为其子路由的父布局路由时,我们已经了解了嵌套路由。当我们进行此更改时,我们必须使用父路由中的 Outlet 组件来渲染匹配的子路由。同样的情况在这里再次发生,因为用户组件也必须渲染它的嵌套路由:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>...</ul>

<Outlet />
</>
);
};

接下来,我们将声明缺少的 User 组件,只要用户的标识符在 URL 中匹配,该组件就会通过 Users 组件中的 Outlet 嵌套。因此,我们可以使用 React Router 的 useParams Hook 从 URL 中获取相应的 userId(等于 :userId):

import {
...
useParams,
} from 'react-router-dom';

...

const User = () => {
const { userId } = useParams();

return (
<>
<h2>User: {userId}</h2>

<Link to="/users">Back to Users</Link>
</>
);
};

我们再次看到了如何通过将一个 Route 组件(或多个 Route 组件)嵌套在另一个 Route 组件中来创建嵌套路由。前者是嵌套的子路由,后者是渲染封闭组件的父路由,该组件必须使用 Outlet 组件来渲染实际匹配的子路由。

我们还看到了如何通过在路由的路径属性(例如:userId)中使用冒号来创建动态路由。本质上, :userId 充当任何标识符的星号。在我们的例子中,我们使用 Link 组件将用户导航到 /users/:userId 路由,其中​​ :userId 代表实际用户的标识符。最后,我们总是可以通过使用 React Router 的 useParams Hook 从 URL 中获取动态路径(称为参数或 params)。

React Router中的相关链接

最新版本的 React Router 带有所谓的相对链接。我们将通过查看用户组件及其用于链接组件的绝对 /users/${user.id} 路径来研究这个概念。在之前版本的 React Router 中,需要指定整个路径。但是,在此版本中,你可以仅使用嵌套路径作为相对路径:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>
{users.map((user) => (
<li key={user.id}>
<Link to={user.id}>
{user.fullName}
</Link>
</li>
))}
</ul>
</>
);
};

由于 Users 组件用于 /users 路由,因此 Users 组件中的 Link 知道其当前位置,不需要创建绝对路径的整个顶级部分。相反,它知道 /users 并且只是附加 :userId 作为它的相对路径。

声明式和程序式导航

到目前为止,我们只在使用 Link 或 NavLink 组件时使用了声明式导航。但是,在某些情况下,你希望能够通过 JavaScript 以编程方式导航用户。我们将通过实现一个可以在 User 组件中删除用户的功能来展示这个场景。在删除后,用户应该从 User 组件导航到 Users 组件(从 /users/:userId 到 /users)。

我们将通过使用 React 的 useState Hook 创建一个有状态的 users 值来开始这个实现,然后实现一个事件处理程序,该处理程序使用标识符从用户中删除用户:

import * as React from 'react';
...

const App = () => {
const [users, setUsers] = React.useState([
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
]);

const handleRemoveUser = (userId) => {
setUsers((state) => state.filter((user) => user.id !== userId));
};

return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />}>
<Route
path=":userId"
element={<User onRemoveUser={handleRemoveUser} />}
/>
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

在我们将事件处理程序作为回调处理程序传递给 User 组件后,我们可以在那里使用它作为内联处理程序来通过标识符删除特定用户:

const User = ({ onRemoveUser }) => {
const { userId } = useParams();

return (
<>
<h2>User: {userId}</h2>

<button type="button" onClick={() => onRemoveUser(userId)}>
Remove
</button>

<Link to="/users">Back to Users</Link>
</>
);
};

一旦用户被删除,我们可以使用 React Router 的 useNavigate Hook,它允许我们以编程方式将用户导航到另一个路由(这里:/users):

import * as React from 'react';
import {
...
useNavigate,
} from 'react-router-dom';

const App = () => {
const navigate = useNavigate();

const [users, setUsers] = React.useState([
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
]);

const handleRemoveUser = (userId) => {
setUsers((state) => state.filter((user) => user.id !== userId));

navigate('/users');
};

return (...);
};

在这种情况下,删除操作是同步发生的,因为用户只是客户端的一个有状态值。但是,如果用户是数据库中的实体,则必须发出异步请求才能删除它。一旦这个操作成功(例如:promise是 resolved时),用户就会被导航到 /users 路由。你可以通过在 React 中设置一个虚假的 API 来自己尝试这个场景,而不使用实际的服务器。

搜索参数

浏览器中的 URL 不仅包含路径,还包含一个可选的查询字符串(在 React Router 中称为搜索参数),它以键/值对的形式出现在 ? URL 中的分隔符。例如,/users?name=robin 将是一个带有一对搜索参数的 URL,其中键是名称,值是 robin。以下示例将其显示为实现:

import * as React from 'react';
import {
...
useSearchParams,
} from 'react-router-dom';

...

const Users = ({ users }) => {
const [searchParams, setSearchParams] = useSearchParams();

const searchTerm = searchParams.get('name') || '';

const handleSearch = (event) => {
const name = event.target.value;

if (name) {
setSearchParams({ name: event.target.value });
} else {
setSearchParams({});
}
};

return (
<>
<h2>Users</h2>

<input
type="text"
value={searchTerm}
onChange={handleSearch}
/>

<ul>
{users
.filter((user) =>
user.fullName
.toLowerCase()
.includes(searchTerm.toLocaleLowerCase())
)
.map((user) => (
<li key={user.id}>
<Link to={user.id}>{user.fullName}</Link>
</li>
))}
</ul>

<Outlet />
</>
);
};

首先,我们使用 React Router 的 useSearchParams Hook 从 URL 中读取当前搜索参数(请参阅 searchParams 上的 get() 方法),同时还将搜索参数写入 URL(请参阅 setSearchParams() 函数)。虽然我们使用前者按键获取搜索参数(此处:“name”)来控制输入字段,但我们使用后者在 URL 中按键设置搜索参数。在输入字段中键入。在其核心,React Router 的 useSearchParams Hook 与 React 的 useState Hook 相同,区别在于该状态是 URL 状态,而不是 React 中的本地状态。最后我们使用搜索参数来过滤用户的实际列表以完成此功能。

毕竟,在你的 URL 中包含搜索参数可以让你与他人共享更具体的 URL。如果你在一个搜索黑色鞋子的电子商务网站上,你可能希望共享整个 URL(例如 myecommerce.com/shoes?color=black)而不仅仅是路径(例如 myecommerce.com/shoes)。

总结

React Router 是 React 最常用的第三方库之一。它的核心功能是将 Link 组件映射到 Route 组件,这使开发人员无需向 Web 服务器发出请求即可实现客户端路由。然而,除了这个核心功能之外,它还是一个成熟的路由库,它支持声明式嵌套路由、动态路由、导航、活动链接,还可以通过 URL 进行编程导航和搜索。

参考

react-router CRA