Skip to content

React官网阅读笔记

现代 JavaScript 教程 https://zh.javascript.info/

React组件

React 组件名称必须始终以大写字母开头,而 HTML 标签必须以小写字母开头

Instead of copy-pasting, React’s component architecture allows you to create a reusable component to avoid messy, duplicated code. React 的组件架构允许你创建一个可重用的组件,以避免混乱、重复的代码,而不是复制粘贴。

标记语法(markup syntax) JSX

JSX is stricter than HTML. You have to close tags like <br />. Your component also can’t return multiple JSX tags. You have to wrap them into a shared parent, like a <div>...</div> or an empty <>...</> wrapper:

JSX 比 HTML 更严格。 您必须关闭 <br /> 您的组件也无法返回多个 JSX 标签。 您必须将它们包装到共享父级中,例如 <div>...</div><>...</> 空包装器

JSX 允许您将标记放入 JavaScript 中。大括号可以让你“转义回”到 JavaScript 中,这样你就可以从代码中嵌入一些变量并将其显示给用户。

要从 JSX “转义到 JavaScript”,您需要大括号。

Each React component is a JavaScript function that may contain some markup that React renders into the browser. 每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。

React components use a syntax extension called JSX to represent that markup. React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。

JSX looks a lot like HTML, but it is a bit stricter and can display dynamic information. JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。

JSX 规则

  1. 只能返回一个根元素 如果想要在一个组件中包含多个元素,需要用一个父标签把它们包裹起来 如果你不想在标签中增加一个额外的 <div>,可以用 <> 和 </> 元素来代替 React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。

为什么多个 JSX 标签需要被一个父元素包裹?

JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象, 你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。 这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。

  1. 标签必须闭合

  2. 使用驼峰式命名法给 ~~ 所有~~ 大部分属性命名! JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。 在你自己的组件中,经常会遇到需要用变量的方式读取这些属性的时候。 但 JavaScript 对变量的命名有限制。 例如,变量名称不能包含 - 符号或者像 class 这样的保留字。

    这就是为什么在 React 中,大部分 HTML 和 SVG 属性都用驼峰式命名法表示。例如,需要用 strokeWidth 代替 stroke-width。 由于 class 是一个保留字,所以在 React 中需要用 className 来代替。

JSX and React are two separate things. They’re often used together, but you can use them independently of each other. JSX and React 是相互独立的 东西。但它们经常一起使用,但你 可以 单独使用它们中的任意一个 JSX is a syntax extension, while React is a JavaScript library. JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。

可以在哪使用大括号?

在 JSX 中,只能在以下两种场景中使用大括号:

用作 JSX 标签内的文本:<h1>{name}'s To Do List</h1> 是有效的,但是 <{tag}>Gregorio Y. Zara's To Do List</{tag}> 无效。 用作紧跟在 = 符号后的 属性:src={avatar} 会读取 avatar 变量,但是 src="{avatar}" 只会传一个字符串 {avatar}。

使用 “双大括号”:JSX 中的 CSS 和 对象

当你下次在 JSX 中看到 时,就知道它只不过是包在大括号里的一个对象罢了!

在一些情况下,你不想有任何东西进行渲染

这种情况下,你可以直接返回 null。 实际上,在组件里返回 null 并不常见,因为这样会让想使用它的开发者感觉奇怪。 通常情况下,你可以在父组件里选择是否要渲染该组件。

切勿将数字放在 && 左侧.

JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。 然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。

例如,一个常见的错误是 messageCount && <p>New messages</p>。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0。

为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>

当一个相似的 JSX 树结构在两个情况下都返回的时候,最好将它们写成一个单独的 JSX

请记住,如果两个不同的 JSX 代码块描述着相同的树结构,它们的嵌套(第一个 <div> → 第一个 <img>)必须对齐。

否则切换 isActive 会再次在后面创建整个树结构并且 重置 state。 这也就是为什么当一个相似的 JSX 树结构在两个情况下都返回的时候,最好将它们写成一个单独的 JSX。

呈现组件列表

you will rely on JavaScript features like for loop and the array map() function to render lists of components. 您将依靠 for loop 和 array map() 函数等 JavaScript 功能来呈现组件列表

注意:对于列表中的每个项目,应传递一个字符串或数字,以在其同级中唯一标识该项目。 通常,密钥应该来自您的数据,例如数据库 ID。 React 使用您的密钥来了解以后插入、删除或重新排序项目时会发生什么。

In most cases, you’d need the actual array elements, but to render a list of moves you will only need indexes. 在大多数情况下,您需要实际的数组元素,但要呈现移动列表,您只需要索引。

列表项的唯一键key

When a list is re-rendered, React takes each list item’s key and searches the previous list’s items for a matching key. If the current list has a key that didn’t exist before, React creates a component. If the current list is missing a key that existed in the previous list, React destroys the previous component. If two keys match, the corresponding component is moved.

当列表被重新渲染时,React 会获取每个列表项的键,并在上一个列表的项中搜索匹配的键。 如果当前列表有一个以前不存在的键,React 会创建一个组件。 如果当前列表缺少上一个列表中存在的键,React 会销毁上一个组件。 如果两个键匹配,则移动相应的组件。

Keys tell React about the identity of each component, which allows React to maintain state between re-renders. If a component’s key changes, the component will be destroyed and re-created with a new state.

键告诉 React 每个组件的身份,这允许 React 在重新渲染之间保持状态。 如果组件的键发生更改,则该组件将被销毁并使用新状态重新创建。

Keys do not need to be globally unique; they only need to be unique between components and their siblings. 密钥不需要是全局唯一的;它们只需要在组件及其同级之间是唯一的。

key is a special and reserved property in React. When an element is created, React extracts the key property and stores the key directly on the returned element. Even though key may look like it is passed as props, React automatically uses key to decide which components to update. There’s no way for a component to ask what key its parent specified. key 是 React 中的特殊保留属性。 创建元素时,React 会提取 key 属性并将键直接存储在返回的元素上。 尽管它 key 看起来像是作为道具传递的,但 React 会自动使用它 key 来决定要更新哪些组件。 组件无法询问其父级指定了什么 key 。

It’s strongly recommended that you assign proper keys whenever you build dynamic lists. If you don’t have an appropriate key, you may want to consider restructuring your data so that you do.

强烈建议您在构建动态列表时分配适当的键。 如果没有适当的密钥,则可能需要考虑重构数据,以便这样做。

简短的 <>...</> Fragment 语法不允许你传递密钥,因此您需要将它们分组到一个 <div> ,或者使用稍长且更明确的 <Fragment> 语法

The short <>...</> Fragment syntax won’t let you pass a key, so you need to either group them into a single <div>, or use the slightly longer and more explicit<Fragment>syntax: 简短的 <>...</> Fragment 语法不允许你传递密钥,因此您需要将它们分组到一个 <div> ,或者使用稍长且更明确的 <Fragment> 语法

如何设定 key 值

不同来源的数据往往对应不同的 key 值获取方式:

来自数据库的数据: 如果你的数据是从数据库中获取的,那你可以直接使用数据表中的主键,因为它们天然具有唯一性。

Locally generated data: If your data is generated and persisted locally (e.g. notes in a note-taking app), use an incrementing counter, crypto.randomUUID() or a package like uuid when creating items. 本地产生数据: 如果你数据的产生和保存都在本地(例如笔记软件里的笔记),那么你可以使用一个自增计数器或者一个类似 uuid 的库来生成 key。

key 需要满足的条件

Keys must be unique among siblings. However, it’s okay to use the same keys for JSX nodes in different arrays. key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。

Keys must not change or that defeats their purpose! Don’t generate them while rendering. key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。

React 中为什么需要 key?

设想一下,假如你桌面上的文件都没有文件名,取而代之的是,你需要通过文件的位置顺序来区分它们———第一个文件,第二个文件,以此类推。 也许你也不是不能接受这种方式,可是一旦你删除了其中的一个文件,这种组织方式就会变得混乱无比。 原来的第二个文件可能会变成第一个文件,第三个文件会成为第二个文件……

React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。它们(key 和文件名)都让我们可以从众多的兄弟元素中唯一标识出某一项(JSX 节点或文件)。 而一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它。

你可能会想直接把数组项的索引当作 key 值来用,实际上,如果你没有显式地指定 key 值,React 确实默认会这么做。 但是数组项的顺序在插入、删除或者重新排序等操作中会发生改变,此时把索引顺序用作 key 值会产生一些微妙且令人困惑的 bug。

与之类似,请不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。 这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建。 这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。 所以,使用能从给定数据中稳定取得的值才是明智的选择。

有一点需要注意,组件不会把 key 当作 props 的一部分。 Key 的存在只对 React 本身起到提示作用。 如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件: <Profile key={id} userId={id} />

因为箭头函数会隐式地返回位于 => 之后的表达式,所以你可以省略 return 语句。

tsx
const listItems = chemists.map(person =>
  <li>...</li> // 隐式地返回!
);

不过,如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!

tsx
const listItems = chemists.map((person) => { // 花括号
  return <li>...</li>;
});

Arrow functions containing => { are said to have a “block body”. They let you write more than a single line of code, but you have to write a return statement yourself. If you forget it, nothing gets returned!

箭头函数 => { 后面的部分被称为 “块函数体”,块函数体支持多行代码的写法,但要用 return 语句才能指定返回值。 假如你忘了写 return,那这个函数什么都不会返回!

事件处理程序函数 event handler functions

请注意 onClick={handleClick} ,末尾没有括号!不要调用事件处理程序函数:你只需要向下传递它。当用户单击按钮时,React 将调用事件处理程序。

事件处理程序函数通常用于制作交互式组件 Making an interactive component

使用箭头函数定义函数

In React, it’s conventional to use onSomething names for props which represent events and handleSomething for the function definitions which handle those events. 在 React 中,通常使用 onSomething 表示事件的 props 和 handleSomething 处理这些事件的函数定义的名称。

Event handlers are your own functions that will be triggered in response to interactions like clicking, hovering, focusing form inputs, and so on. 事件处理函数为自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发。

事件处理函数有如下特点:

通常在你的组件 内部 定义。 名称以 handle 开头,后跟事件名称。

传递给事件处理函数的函数应直接传递,而非调用。

如果你想要定义内联事件处理函数,请将其包装在匿名函数中

text
<button onClick={() => alert('你点击了我!')} />

这里创建了一个稍后调用的函数,而不会在每次渲染时执行其内部代码。

通常,我们会在父组件中定义子组件的事件处理函数。

内置组件(<button> 和 <div>)仅支持 浏览器事件名称,例如 onClick。 但是,当你构建自己的组件时,你可以按你个人喜好命名事件处理函数的 prop。 按照惯例,事件处理函数 props 应该以 on 开头,后跟一个大写字母。

确保为事件处理程序使用适当的 HTML 标签。

例如,要处理点击事件,请使用 <button onClick={handleClick}> 而不是 <div onClick={handleClick}>。 使用真正的浏览器 <button> 启用内置的浏览器行为,如键盘导航。 如果你不喜欢按钮的默认浏览器样式,并且想让它看起来更像一个链接或不同的 UI 元素,你可以使用 CSS 来实现。

事件传播

事件处理函数还将捕获任何来自子组件的事件。 通常,我们会说事件会沿着树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播。

在 React 中所有事件都会传播,除了 onScroll,它仅适用于你附加到的 JSX 标签。

如果你想阻止一个事件到达父组件,你需要像下面 Button 组件那样调用 e.stopPropagation()

不要混淆 e.stopPropagation() 和 e.preventDefault()。它们都很有用,但二者并不相关:

e.stopPropagation() 阻止触发绑定在外层标签上的事件处理函数。 e.preventDefault() 阻止少数事件的默认浏览器行为。

事件处理函数是执行副作用的最佳位置

Event handlers are the best place for side effects.

Unlike rendering functions, event handlers don’t need to be pure, so it’s a great place to change something—for example, change an input’s value in response to typing, or change a list in response to a button press. However, in order to change some information, you first need some way to store it. In React, this is done by using state, a component’s memory. 与渲染函数不同,事件处理函数不需要是 纯函数,因此它是用来 更改 某些值的绝佳位置。 例如,更改输入框的值以响应键入,或者更改列表以响应按钮的触发。 但是,为了更改某些信息,你首先需要某种方式存储它。 在 React 中,这是通过 state(组件的记忆) 来完成的

使用 useState记住一些信息,并更新它

Often, you’ll want your component to “remember” some information and display it. For example, maybe you want to count the number of times a button is clicked. To do this, add state to your component. 通常,您会希望组件“记住”一些信息并显示它。例如,您可能想要计算单击按钮的次数。为此,请向组件添加状态。

You’ll get two things from useState: the current state (count), and the function that lets you update it (setCount). You can give them any names, but the convention is to write [something, setSomething]. 您将从 useState 以下位置获得两件事:当前状态 ( count ) 和允许您更新它的函数 ( setCount )。 你可以给他们起任何名字,但惯例是写 [something, setSomething] .

如果多次渲染同一组件,则每个组件都会获得自己的状态

To “remember” things, components use state. 为了“记住”事物,组件使用状态。

In fact, always try to avoid redundant state. Simplifying what you store in state reduces bugs and makes your code easier to understand.

事实上,总是尽量避免冗余状态。 简化存储状态的内容可以减少错误,并使代码更易于理解。

state状态仅保留用于交互性,即随时间变化的数据。 To make the UI interactive, you need to let users change your underlying data model. You will use state for this. 若要使 UI 具有交互性,需要允许用户更改基础数据模型。为此,您将使用 state。

State:组件的记忆

组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。 组件需要“记住”某些东西:当前输入值、当前图片、购物车。 在 React 中,这种组件特有的记忆被称为 state。

当一个组件需要在多次渲染间“记住”某些信息时使用 state 变量。

useState Hook 提供了两个功能:

State 变量 用于保存渲染间的数据。 State setter 函数 更新变量并触发 React 再次渲染组件。

如果它们不相关,那么存在多个 state 变量是一个好主意

但是,如果你发现经常同时更改两个 state 变量,那么最好将它们合并为一个。 例如,如果你有一个包含多个字段的表单,那么有一个值为对象的 state 变量比每个字段对应一个 state 变量更方便。

State 是隔离且私有的

State 是屏幕上组件实例内部的状态。 换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state! 改变其中一个不会影响另一个。

两个事件处理函数中均添加了一个保护条件,并在需要时禁用了按钮

State 变量仅用于在组件重渲染时保存信息。

A state variable is only necessary to keep information between re-renders of a component. Within a single event handler, a regular variable will do fine.

Don’t introduce state variables when a regular variable works well. 在单个事件处理函数中,普通变量就足够了。当普通变量运行良好时,不要引入 state 变量

state 如同一张快照

设置 state 会触发渲染

渲染会及时生成一张快照

当 React 重新渲染一个组件时:

  • React 会再次调用你的函数
  • 函数会返回新的 JSX 快照
  • React 会更新界面以匹配返回的快照

Setting state only changes it for the next render. 设置 state 只会为下一次渲染变更 state 的值。

A state variable’s value never changes within a render, even if its event handler’s code is asynchronous. 一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。

Its value was “fixed” when React “took the snapshot” of the UI by calling your component. 它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。

React keeps the state values “fixed” within one render’s event handlers. You don’t need to worry whether the state has changed while the code is running. React 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。

Variables and event handlers don’t “survive” re-renders. Every render has its own event handlers. 变量和事件处理函数不会在重渲染中“存活”。每个渲染都有自己的事件处理函数。

React 会对 state 更新进行批处理

React waits until all code in the event handlers has run before processing your state updates. React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。

Setting state does not change the variable in the existing render, but it requests a new render. 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。

批处理

React processes state updates after event handlers have finished running. This is called batching. React 在事件处理程序完成运行后处理状态更新。这称为批处理。

This lets you update multiple state variables—even from multiple components—without triggering too many re-renders. 这让你可以更新多个 state 变量——甚至来自多个组件的 state 变量——而不会触发太多的 重新渲染。

But this also means that the UI won’t be updated until after your event handler, and any code in it, completes. 但这也意味着只有在你的事件处理函数及其中任何代码执行完成 之后,UI 才会更新。

This behavior, also known as batching, makes your React app run much faster. 这种特性也就是 批处理,它会使你的 React 应用运行得更快。

It also avoids dealing with confusing “half-finished” renders where only some of the variables have been updated. 它还会帮你避免处理只更新了一部分 state 变量的令人困惑的“半成品”渲染。

React does not batch across multiple intentional events like clicks—each click is handled separately. React 不会跨 多个 需要刻意触发的事件(如点击)进行批处理——每次点击都是单独处理的。

Rest assured that React only does batching when it’s generally safe to do. 请放心,React 只会在一般来说安全的情况下才进行批处理。

This ensures that, for example, if the first button click disables a form, the second click would not submit it again. 这可以确保,例如,如果第一次点击按钮会禁用表单,那么第二次点击就不会再次提交它。

更新函数

To update some state multiple times in one event, you can use setNumber(n => n + 1) updater function. 要在一个事件中多次更新某些状态,可以使用 setNumber(n => n + 1) 更新程序函数。

当你将它传递给一个 state 设置函数时:

React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。

setState(x) 实际上会像 setState(n => x) 一样运行,只是没有使用 n!

After the event handler completes, React will trigger a re-render. 事件处理函数执行完成后,React 将触发重新渲染。

During the re-render, React will process the queue. 在重新渲染期间,React 将处理队列。

Updater functions run during rendering, so updater functions must be pure and only return the result. 更新函数会在渲染期间执行,因此 更新函数必须是 纯函数 并且只 返回 结果。

Don’t try to set state from inside of them or run other side effects. In Strict Mode, React will run each updater function twice (but discard the second result) to help you find mistakes.

不要尝试从它们内部设置 state 或者执行其他副作用。 在严格模式下,React 会执行每个更新函数两次(但是丢弃第二个结果)以便帮助你发现错误。

更新 state 中的对象

你不应该直接修改存放在 React state 中的对象。 相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。

你应该 把所有存放在 state 中的 JavaScript 对象都视为只读的。 你应该把在渲染过程中可以访问到的 state 视为只读的

为了真正地 触发一次重新渲染,你需要创建一个新对象并把它传递给 state 的设置函数

对于大型表单,将所有数据都存放在同一个对象中是非常方便的——前提是你能够正确地更新它!

请注意 ... 展开语法本质是是“浅拷贝”——它只会复制一层。 这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。

请记住,你不应该在 state 中改变对象,包括 Set 中。

使用 Immer 编写简洁的更新逻辑

如果你的 state 有多层的嵌套,你或许应该考虑 将其扁平化。但是,如果你不想改变 state 的数据结构,你可能更喜欢用一种更便捷的方式来实现嵌套展开的效果。 Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。 通过使用 Immer,你写出的代码看起来就像是你“打破了规则”而直接修改了对象

更新 state 中的数组

在 JavaScript 中,数组只是另一种对象。 同对象一样,你需要将 React state 中的数组视为只读的。 这意味着你不应该使用类似于 arr[0] = 'bird' 这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如 push() 和 pop()。

相反,每次要更新一个数组时,你需要把一个新的数组传入 state 的 setting 方法中。 为此,你可以通过使用像 filter() 和 map() 这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。

虽然 slice 和 splice 的名字相似,但作用却迥然不同

slice 让你可以拷贝数组或是数组的一部分。 splice 会直接修改 原始数组(插入或者删除元素)。

在 React 中,更多情况下你会使用 slice(没有 p !),因为你不想改变 state 中的对象或数组。

即使你拷贝了数组,你还是不能直接修改其内部的元素。 这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。 因此,如果你修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。

使用 Hooks

Hooks are more restrictive than other functions. You can only call Hooks at the top of your components (or other Hooks). If you want to use useState in a condition or a loop, extract a new component and put it there.

钩子比其他函数更具限制性。您只能调用组件顶部的 Hooks(或其他 Hook)。 如果要在条件或循环中使用 useState ,请提取一个新组件并将其放在那里

Hooks are special functions that are only available while React is rendering Hook 是特殊的函数,只在 React 渲染时有效

Hooks—functions starting with use—can only be called at the top level of your components or your own Hooks. You can’t call Hooks inside conditions, loops, or other nested functions.

Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。

Hooks are functions, but it’s helpful to think of them as unconditional declarations about your component’s needs. You “use” React features at the top of your component similar to how you “import” modules at the top of your file.

Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。 在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。

提升状态 “lifting state up”

By moving state up, you’ve shared it between components. 通过向上移动状态,您可以在组件之间共享它

To collect data from multiple children, or to have two child components communicate with each other, declare the shared state in their parent component instead. The parent component can pass that state back down to the children via props. This keeps the child components in sync with each other and with their parent.

若要从多个子组件收集数据,或者让两个子组件相互通信,请改为在其父组件中声明共享状态。 父组件可以通过 props 将该状态传递回子组件。 这使子组件彼此之间以及与父组件保持同步。

Lifting state into a parent component is common when React components are refactored. 在重构 React 组件时,将状态提升到父组件是很常见的。

单向数据流 one-way data flow

This is called one-way data flow because the data flows down from the top-level component to the ones at the bottom of the tree. 因为数据从顶层组件向向树底部的组件。

return 意味着后面的任何内容都作为返回给函数的调用方

The return JavaScript keyword means whatever comes after is returned as a value to the caller of the function. JavaScript 关键字 return 意味着后面的任何内容都作为返回给函数的调用方。

React 组件需要返回单个 JSX 元素 您可以使用 Fragments ( <> 和 </> ) 来包装多个相邻的 JSX 元素

如果你的标签和 return 关键字不在同一行,则必须把它包裹在一对括号中 没有括号包裹的话,任何在 return 下一行的代码都 将被忽略!

react-dom/client

React’s library to talk to web browsers (React DOM) 用于与 Web 浏览器对话的 React 库 (React DOM)

函数闭包 closures

JavaScript supports **closures ** which means an inner function (e.g. handleClick) has access to variables and functions defined in a outer function (e.g. Board). JavaScript 支持 闭包,这意味着内部函数(例如 handleClick )可以访问外部函数(例如 )中定义的变量和函数 Board

不变性很重要 immutability is important

调用 .slice() 以创建 squares 数组的副本,而不是修改现有数组。

通常有两种方法可以更改数据。 第一种方法是通过直接更改数据的值来改变数据。 第二种方法是将数据替换为具有所需更改的新副本。

The result is the same but by not mutating (changing the underlying data) directly, you gain several benefits. 结果是相同的,但通过不直接变异(更改基础数据),您可以获得一些好处。

不直接变异的好处

  1. 不可变性使复杂的功能更容易实现

    Avoiding direct data mutation lets you keep previous versions of the data intact, and reuse them later. 通过避免直接数据突变,您可以保持以前版本的数据不变,并在以后重复使用它们。

  2. 不可变性使得组件比较其数据是否已更改的成本非常低

    By default, all child components re-render automatically when the state of a parent component changes. This includes even the child components that weren’t affected by the change. Although re-rendering is not by itself noticeable to the user (you shouldn’t actively try to avoid it!), you might want to skip re-rendering a part of the tree that clearly wasn’t affected by it for performance reasons. Immutability makes it very cheap for components to compare whether their data has changed or not. 不变性还有另一个好处。默认情况下,当父组件的状态发生更改时,所有子组件都会自动重新渲染。这甚至包括不受更改影响的子组件。 尽管重新渲染本身对用户来说并不明显(你不应该主动尝试避免它!),但出于性能原因,你可能希望跳过重新渲染树中明显不受其影响的部分。

React组件化开发思维

React 组件定义

a React component is a JavaScript function that you can sprinkle with markup React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数

React components are regular JavaScript functions except: React 组件是常规的 JavaScript 函数,除了:

Their names always begin with a capital letter. 他们的名字总是以大写字母开头。[] They return JSX markup. 它们返回 JSX 标记。

Components are a handy way to organize UI code and markup, even if some of them are only used once. 组件是组织 UI 代码和标签的一种快捷方式,即使其中一些组件只使用了一次。

in a React app, every piece of UI is a component. 在 React 应用程序中,每一个 UI 模块都是一个组件,万物皆组件。

React 组件是常规的 JavaScript 函数,但 组件的名称必须以大写字母开头,否则它们将无法运行! 组件可以渲染其他组件,但是 请不要嵌套他们的定义, 因为这样非常慢,并且会导致 bug 产生。因此,你应该在顶层定义每个组件 当子组件需要使用父组件的数据时,你需要 通过 props 的形式进行传递,而不是嵌套定义。 你在相同位置渲染的是 不同 的组件,所以 React 将其下所有的 state 都重置了。 这样会导致 bug 以及性能问题。为了避免这个问题, 永远要将组件定义在最上层并且不要把它们的定义嵌套起来。 https://react.docschina.org/learn/preserving-and-resetting-state#different-components-at-the-same-position-reset-state

步骤 1:将 UI 分解为组件层次结构

Step 1: Break the UI into a component hierarchy

If your JSON is well-structured, you’ll often find that it naturally maps to the component structure of your UI. That’s because UI and data models often have the same information architecture—that is, the same shape. Separate your UI into components, where each component matches one piece of your data model.

如果你的 JSON 结构良好,你通常会发现它自然地映射到 UI 的组件结构。 这是因为 UI 和数据模型通常具有相同的信息体系结构,即相同的形状。 将 UI 分成多个组件,其中每个组件与数据模型的一部分匹配。

第 2 步:在 React 中构建静态版本

Step 2: Build a static version in React The most straightforward approach is to build a version that renders the UI from your data model without adding any interactivity… yet! 最直接的方法是构建一个版本,该版本从数据模型呈现 UI,而无需添加任何交互性......是的!

先不要使用交互,不需要使用事件处理函数

It’s often easier to build the static version first and add interactivity later. Building a static version requires a lot of typing and no thinking, but adding interactivity requires a lot of thinking and not a lot of typing. 先构建静态版本,然后再添加交互性通常更容易。 构建静态版本需要大量的打字,不需要思考,但添加交互性需要大量的思考,而不是大量的打字。

To build a static version of your app that renders your data model, you’ll want to build components that reuse other components and pass data using props. Props are a way of passing data from parent to child.

若要生成呈现数据模型的应用的静态版本,需要构建可重用其他组件并使用 prop 传递数据的组件。 Props是一种将数据从父级传递到子级的方式。

先不要使用state

(If you’re familiar with the concept of state, don’t use state at all to build this static version. State is reserved only for interactivity, that is, data that changes over time. Since this is a static version of the app, you don’t need it.) (如果你熟悉状态的概念,请不要使用状态来构建这个静态版本。 状态仅保留用于交互性,即随时间变化的数据。 由于这是应用程序的静态版本,因此您不需要它。)

在较大的项目中,自下而上构建组件更容易

您可以“自上而下”地构建,首先在层次结构中构建较高的组件,也可以通过从较低的组件开始“自下而上”地构建。

在更简单的例子中,自上而下通常更容易,而在较大的项目中,自下而上更容易。

步骤 3:查找 UI 状态的最小但完整的表示形式

Step 3: Find the minimal but complete representation of UI state。

To make the UI interactive, you need to let users change your underlying data model. You will use state for this. 若要使 UI 具有交互性,需要允许用户更改基础数据模型。为此,您将使用 state。

Think of state as the minimal set of changing data that your app needs to remember. The most important principle for structuring state is to keep it DRY (Don’t Repeat Yourself). Figure out the absolute minimal representation of the state your application needs and compute everything else on-demand.

将状态视为应用需要记住的最小更改数据集。构建状态的最重要原则是保持干燥(不要重复自己)。 找出应用程序所需状态的绝对最小表示,并按需计算其他所有内容。

For example, if you’re building a shopping list, you can store the items as an array in state. If you want to also display the number of items in the list, don’t store the number of items as another state value—instead, read the length of your array.

例如,如果您正在构建购物清单,则可以将项目存储为状态中的数组。 如果还想显示列表中的项数,请不要将项数存储为另一个状态值,而是读取数组的长度

第 4 步:确定您的state应该放在哪里

Step 4: Identify where your state should live

After identifying your app’s minimal state data, you need to identify which component is responsible for changing this state, or owns the state. Remember: React uses one-way data flow, passing data down the component hierarchy from parent to child component. 确定应用的最小状态数据后,需要确定哪个组件负责更改此状态,或者哪个组件拥有该状态。 请记住:React 使用单向数据流,将数据从父组件向下传递到子组件。

最佳实践

为你应用程序中的每一个 state:

  • 验证每一个基于特定 state 渲染的组件。
  • 找它们最近并且共同的父组件——在层级结构中,一个凌驾于它们所有组件之上的组件。
  • 决定 state 应该被放置于哪里:
    • 通常情况下,你可以直接放置 state 于它们共同的父组件。
    • 你也可以将 state 放置于它们父组件上层的组件。
    • 如果你找不到一个有意义拥有这个 state 的地方,单独创建一个新的组件去管理这个 state,并将它添加到它们父组件上层的某个地方。

总是一起出现的东西,将它们放在同一个位置是有意义的。

步骤 5:添加反向数据流

Step 5: Add inverse data flow

组件的导入导出

通常,文件中仅包含一个组件时,人们会选择默认导出,而当文件中包含多个组件或某个值需要导出时,则会选择具名导出。 无论选择哪种方式,请记得给你的组件和相应的文件命名一个有意义的名字。 我们不建议创建未命名的组件,比如 export default () => {},因为这样会使得调试变得异常困难。

Props

给 prop 指定一个默认值

默认值仅在缺少 size prop 或 size={undefined} 时生效。 但是如果你传递了 size={null} size={0},默认值将 不 被使用。

请克制地使用展开语法。

如果你在所有其他组件中都使用它,那就有问题了。 通常,它表示你应该拆分组件,并将子组件作为 JSX 传递。

将 JSX 作为子组件传递

可以将带有 children prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”。 你会经常使用 children prop 来进行视觉包装:面板、网格等等。

Any JSX you put inside of a component’s tag will be passed as the children prop to that component. 放入组件标签内的任何 JSX 都将作为 children prop 传递给该组件。

Props 并不总是静态的!

一个组件可能会随着时间的推移收到不同的 props。

然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。 当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象! 它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。

Props are read-only snapshots in time: every render receives a new version of props. Props 是只读的时间快照:每次渲染都会收到新版本的 props。

你不能改变 props。当你需要交互性时,你可以设置 state。

You can’t change props. When you need interactivity, you’ll need to set state. 不要尝试“更改 props”。 当你需要响应用户输入(例如更改所选颜色)时,你可以“设置 state”

保持组件纯粹 Keeping Components Pure

In computer science (and especially the world of functional programming), a pure function is a function with the following characteristics: 在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:

It minds its own business. It does not change any objects or variables that existed before it was called. 只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。

Same inputs, same output. Given the same inputs, a pure function should always return the same result. 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。

React is designed around this concept. React assumes that every component you write is a pure function. This means that React components you write must always return the same JSX given the same inputs React 就是围绕这个概念设计的。 React 假设你编写的每个组件都是一个纯函数。 这意味着你编写的 React 组件必须始终返回相同的 JSX,给定相同的输入

each component should only “think for itself”, and not attempt to coordinate with or depend upon others during rendering. 每个组件都应该“独立思考”,而不是在渲染过程中试图与其他组件协调或依赖其他组件

使用 StrictMode 检测不纯计算

Detecting impure calculations with StrictMode

可能引起副作用的地方

Where you can cause side effects

Rendering is a calculation, it shouldn’t try to “do” things 渲染是一种 计算过程 ,它不应该试图“做”其他事情。

Remember that React does not guarantee that component functions will execute in any particular order, so you can’t communicate between them by setting variables. All communication must happen through props. React 无法保证组件函数以任何特定的顺序执行,因此你无法通过设置变量在它们之间进行通信。所有的交流都必须通过 props 进行。

It is useful to remember which operations on arrays mutate them, and which don’t. For example, push, pop, reverse, and sort will mutate the original array, but slice, filter, and map will create a new one. 记住数组上的哪些操作会修改原始数组、哪些不会,这非常有帮助。 例如,push、pop、reverse 和 sort 会改变原始数组,但 slice、filter 和 map 则会创建一个新数组。

副作用的定义

While functional programming relies heavily on purity, at some point, somewhere, something has to change. That’s kind of the point of programming! These changes—updating the screen, starting an animation, changing the data—are called side effects. They’re things that happen “on the side”, not during rendering. 虽然函数式编程在很大程度上依赖于纯度,但在某些时候,某个地方,有些东西必须改变。 这就是编程的意义所在!这些更改(更新屏幕、启动动画、更改数据)称为 副作用。 它们是“额外”发生的事情,而不是在渲染过程中发生的。

事件处理程序无需是纯函数

In React, side effects usually belong inside event handlers. Event handlers are functions that React runs when you perform some action—for example, when you click a button. Even though event handlers are defined inside your component, they don’t run during rendering! So event handlers don’t need to be pure. 在 React 中,副作用通常属于事件处理程序。 事件处理程序是 React 在执行某些操作(例如,单击按钮时)运行的函数。 即使事件处理程序在组件中定义,它们也不会在渲染期间运行! 因此,事件处理程序不需要是纯粹的。

If you’ve exhausted all other options and can’t find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort. 如果您已经用尽了所有其他选项,并且找不到适合您的副作用的事件处理程序,您仍然可以通过在组件中 useEffect 调用将其附加到返回的 JSX。 这告诉 React 稍后在渲染后,当允许副作用时执行它。 但是,这种方法应该是您最后的手段。

When possible, try to express your logic with rendering alone. You’ll be surprised how far this can take you! 如果可能的话,尝试仅用渲染来表达你的逻辑。 你会惊讶于这能带你走多远!

React 为何侧重于纯函数?

Why does React care about purity?

编写纯函数需要遵循一些习惯和规程。但它开启了绝妙的机遇:

你的组件可以在不同的环境下运行 — 例如,在服务器上!由于它们针对相同的输入,总是返回相同的结果,因此一个组件可以满足多个用户请求。

你可以为那些输入未更改的组件来 跳过渲染,以提高性能。这是安全的做法,因为纯函数总是返回相同的结果,所以可以安全地缓存它们。

如果在渲染深层组件树的过程中,某些数据发生了变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全地停止计算。

From data fetching to animations to performance, keeping components pure unlocks the power of the React paradigm. 从数据获取到动画再到性能,保持组件的纯性可以释放 React 范式的力量。

渲染和提交

Before your components are displayed on screen, they must be rendered by React. 组件显示到屏幕之前,其必须被 React 渲染。

在一个 React 应用中,一次屏幕更新都会发生以下三个步骤: 触发 渲染 提交

步骤 1: 触发一次渲染

There are two reasons for a component to render: 有两种原因会导致组件的渲染:

It’s the component’s initial render. 组件的 初次渲染。

The component’s (or one of its ancestors’) state has been updated. 组件(或者其祖先之一)的 状态发生了改变。

Initial render 初始渲染

When your app starts, you need to trigger the initial render. Frameworks and sandboxes sometimes hide this code, but it’s done by calling createRoot with the target DOM node, and then calling its render method with your component 当应用启动时,会触发初次渲染。 框架和沙箱有时会隐藏这部分代码,但它是通过调用目标 DOM 节点的 createRoot,然后用你的组件调用 render 函数完成的

Re-renders when state updates 状态更新时重新渲染

Once the component has been initially rendered, you can trigger further renders by updating its state with the set function.

Updating your component’s state automatically queues a render. 一旦组件被初次渲染,你就可以通过使用 set 函数 更新其状态来触发之后的渲染。 更新组件的状态会自动将一次渲染送入队列。

步骤 2: React 渲染你的组件

After you trigger a render, React calls your components to figure out what to display on screen. “Rendering” is React calling your components. 在你触发渲染后,React 会调用你的组件来确定要在屏幕上显示的内容。 “渲染中” 即 React 在调用你的组件。

On initial render, React will call the root component. 在进行初次渲染时, React 会调用根组件。

For subsequent renders, React will call the function component whose state update triggered the render. 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。

这个过程是递归的: 如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。 这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。

Rendering must always be a pure calculation: 渲染必须始终是纯计算:

Same inputs, same output. Given the same inputs, a component should always return the same JSX. 相同的输入,相同的输出。给定相同的输入,组件应始终返回相同的 JSX。

It minds its own business. It should not change any objects or variables that existed before rendering. 它只关心自己的事情。它不应更改渲染之前存在的任何对象或变量。

Otherwise, you can encounter confusing bugs and unpredictable behavior as your codebase grows in complexity. When developing in “Strict Mode”, React calls each component’s function twice, which can help surface mistakes caused by impure functions.

否则,随着代码库复杂性的增加,你可能会遇到令人困惑的错误和不可预测的行为。 在 “严格模式” 下开发时,React 会调用每个组件的函数两次,这可以帮助发现由不纯函数引起的错误。

性能优化

The default behavior of rendering all components nested within the updated component is not optimal for performance if the updated component is very high in the tree. 如果更新的组件在树中的位置非常高,渲染更新后的组件内部所有嵌套组件的默认行为将不会获得最佳性能。

Don’t optimize prematurely! 不要过早进行优化!

第 3 步:React 提交对 DOM 的更改

在渲染(调用)你的组件之后,React 将会修改 DOM

对于初次渲染, React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。

React only changes the DOM nodes if there’s a difference between renders. React 仅在渲染之间存在差异时才会更改 DOM 节点。

For example, here is a component that re-renders with different props passed from its parent every second. 例如,有一个组件,它每秒使用从父组件传递下来的不同属性重新渲染一次。

尾声:浏览器绘制

After rendering is done and React updated the DOM, the browser will repaint the screen. Although this process is known as “browser rendering”, we’ll refer to it as “painting”

在渲染完成并且 React 更新 DOM 之后,浏览器就会重新绘制屏幕。 尽管这个过程被称为“浏览器渲染”(“browser rendering”),但我们还是将它称为“绘制”(“painting”)

状态管理 Managing State

Redundant or duplicate state is a common source of bugs. 冗余或重复的状态往往是缺陷的根源

React provides a declarative way to manipulate the UI. React 控制 UI 的方式是声明式的。

Instead of manipulating individual pieces of the UI directly, you describe the different states that your component can be in, and switch between them in response to the user input. 你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换。

声明式地考虑 UI

步骤 1:定位组件中不同的视图状态

同时展示大量的视图状态

如果一个组件有多个视图状态,你可以很方便地将它们展示在一个页面中, 类似这样的页面通常被称作“living styleguide”或“storybook”。

步骤 2:确定是什么触发了这些状态的改变

你可以触发 state 的更新来响应两种输入:

人为输入。比如点击按钮、在表单中输入内容,或导航到链接。 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。

以上两种情况中,你必须设置 state 变量 去更新 UI

注意,人为输入通常需要 事件处理函数!

可视化流程状态

为了可视化这个流程,请尝试在纸上画出圆形标签以表示每个状态,两个状态之间的改变用箭头表示。 你可以像这样画出很多流程并且在写代码前解决许多 bug。

步骤 3:通过 useState 表示内存中的 state

接下来你会需要在内存中通过 useState 表示组件中的视图状态。 诀窍很简单:state 的每个部分都是“处于变化中的”,并且你需要让“变化的部分”尽可能的少。 更复杂的程序会产生更多 bug! 先从绝对必须存在的状态开始。

如果你很难立即想出最好的办法,那就先从添加足够多的 state 开始,确保所有可能的视图状态都囊括其中 你最初的想法或许不是最好的,但是没关系,重构 state 也是步骤中的一部分!

步骤 4:删除任何不必要的 state 变量

你会想要避免 state 内容中的重复,从而只需要关注那些必要的部分。 花一点时间来重构你的 state 结构,会让你的组件更容易被理解,减少重复并且避免歧义。 你的目的是防止出现在内存中的 state 不代表任何你希望用户看到的有效 UI 的情况。 (比如你绝对不会想要在展示错误信息的同时禁用掉输入框,导致用户无法纠正错误!)

这有一些你可以问自己的, 关于 state 变量的问题:

这个 state 是否会导致矛盾? 相同的信息是否已经在另一个 state 变量中存在? 你是否可以通过另一个 state 变量的相反值得到相同的信息?

通过 reducer 来减少“不可能” state 为了更精确地模块化状态,你可以 将状态提取到一个 reducer 中。 Reducer 可以让您合并多个状态变量到一个对象中并巩固所有相关的逻辑!

步骤 5:连接事件处理函数以设置 state

最后,创建事件处理函数去设置 state 变量

尽管这些代码相对与最初的命令式的例子来说更长,但是却更加健壮。 将所有的交互变为 state 的改变,可以让你避免之后引入新的视图状态后导致现有 state 被破坏。 同时也使你在不必改变交互逻辑的情况下,更改每个状态对应的 UI。

选择状态结构

Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. 良好的状态组织,可以区分开易于修改和调试的组件与频繁出问题的组件。

The most important principle is that state shouldn’t contain redundant or duplicated information. 最重要的原则是,状态不应包含冗余或重复的信息。

If there’s unnecessary state, it’s easy to forget to update it, and introduce bugs! 如果包含一些多余的状态,我们会很容易忘记去更新它,从而导致问题产生!

构建 state 的原则

当你编写一个存有 state 的组件时,你需要选择使用多少个 state 变量以及它们都是怎样的数据格式。 尽管选择次优的 state 结构下也可以编写正确的程序,但有几个原则可以指导您做出更好的决策:

合并关联的 state。

Group related state.

If you always update two or more state variables at the same time, consider merging them into a single state variable. 如果你总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量。

如果你的 state 变量是一个对象时,请记住,你不能只更新其中的一个字段 而不显式复制其他字段。

避免互相矛盾的 state。

Avoid contradictions in state.

When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this. 当 state 结构中存在多个相互矛盾或“不一致”的 state 时,你就可能为此会留下隐患。应尽量避免这种情况。

你仍然可以声明一些常量,以提高可读性:

ts
const isSending = status === 'sending';
const isSent = status === 'sent';

但它们不是 state 变量,所以你不必担心它们彼此失去同步。

避免冗余的 state。

Avoid redundant state.

If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state. 如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。

不要在 state 中镜像(备份) props

Don’t mirror props in state 只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。 按照惯例,prop 名称以 initial 或 default 开头,以阐明该 prop 的新值将被忽略

避免重复的 state。

Avoid duplication in state.

When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can. 当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。

You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render. 你不需要在 state 中保存 选定的元素,因为只有 选定的 ID 是必要的。 其余的可以在渲染期间计算。

避免深度嵌套的 state。

Avoid deeply nested state.

Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way. 深度分层的 state 更新起来不是很方便。如果可能的话,最好以扁平化方式构建 state。

If the state is too nested to update easily, consider making it “flat”. 如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。

Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier. 现在 state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。

You can nest state as much as you like, but making it “flat” can solve numerous problems. 你确实可以随心所欲地嵌套 state,但是将其“扁平化”可以解决许多问题。

It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object. 这使得 state 更容易更新,并且有助于确保在嵌套对象的不同部分中没有重复。

Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. 有时候,你也可以通过将一些嵌套 state 移动到子组件中来减少 state 的嵌套。

This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered. 这对于不需要保存的短暂 UI 状态非常有效,比如一个选项是否被悬停。

总结

The goal behind these principles is to make state easy to update without introducing mistakes. 这些原则背后的目标是 使 state 易于更新而不引入错误。

Removing redundant and duplicate data from state helps ensure that all its pieces stay in sync. 从 state 中删除冗余和重复数据有助于确保所有部分保持同步。

This is similar to how a database engineer might want to “normalize” the database structure to reduce the chance of bugs. 这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。

To paraphrase Albert Einstein, “Make your state as simple as it can be—but no simpler.” 用爱因斯坦的话说,“让你的状态尽可能简单,但不要过于简单。”

在组件间共享状态

Sometimes, you want the state of two components to always change together. 有时候你希望两个组件的状态始终同步更改。

To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props. 要实现这一点,可以将相关状态从这两个组件上移除,并把这些状态移到最近的父级组件,然后通过 props 将状态传递给这两个组件。

This is known as “lifting state up”, and it’s one of the most common things you will do writing React code. 这被称为“状态提升”,这是编写 React 代码时常做的事。

第 1 步: 从子组件中移除状态 第 2 步: 从公共父组件传递硬编码数据 第 3 步: 为公共父组件添加状态

状态提升通常会改变原状态的数据存储类型。

可信单一数据源

For each unique piece of state, you will choose the component that “owns” it. 对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state。

This principle is also known as having a “single source of truth”. 这一原则也被称为拥有 “可信单一数据源”。

It doesn’t mean that all state lives in one place—but that for each piece of state, there is a specific component that holds that piece of information. 它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。

Instead of duplicating shared state between components, lift it up to their common shared parent, and pass it down to the children that need it. 你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。

对 state 进行保留和重置

各个组件的 state 是各自独立的。 根据组件在 UI 树中的位置,React 可以跟踪哪些 state 属于哪个组件。 你可以控制在重新渲染过程中何时对 state 进行保留和重置。

状态与渲染树中的位置相关

React 会为 UI 中的组件结构构建 渲染树。

当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。

但实际上,状态是由 React 保存的。 React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。 只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state。

相同位置的相同组件会使得 state 被保留下来

只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。

记住 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!

相同位置的不同组件会使 state 重置

当你在相同位置渲染不同的组件时,组件的整个子树都会被重置。 一般来说,如果你想在重新渲染时保留 state,几次渲染中的树形结构就应该相互“匹配”。 结构不同就会导致 state 的销毁,因为 React 会在将一个组件从树中移除时销毁它的 state。

永远要将组件定义在最上层并且不要把它们的定义嵌套起来

你在相同位置渲染的是 不同 的组件,所以 React 将其下所有的 state 都重置了。 这样会导致 bug 以及性能问题。 为了避免这个问题, 永远要将组件定义在最上层并且不要把它们的定义嵌套起来。

在相同位置重置 state

默认情况下,React 会在一个组件保持在同一位置时保留它的 state。 通常这就是你想要的,所以把它作为默认特性很合理。 但有时候,你可能想要重置一个组件的 state。

有两个方法可以在它们相互切换时重置 state:
  • 将组件渲染在不同的位置
  • 使用 key 赋予每个组件一个明确的身份 React 允许你覆盖默认行为,可通过向组件传递一个唯一 key 来 强制 重置其状态。即使渲染的是同一个组件 你可能在 渲染列表 时见到过 key。但 key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件

请记住 key 不是全局唯一的。它们只能指定 父组件内部 的顺序。 使用 key 来重置 state 在处理表单时特别有用。

为被移除的组件保留 state

在真正的聊天应用中,你可能会想在用户再次选择前一个收件人时恢复输入 state。 对于一个不可见的组件,有几种方法可以让它的 state “活下去”:

与其只渲染现在这一个聊天,你可以把 所有 聊天都渲染出来,但用 CSS 把其他聊天隐藏起来。 这些聊天就不会从树中被移除了,所以它们的内部 state 会被保留下来。这种解决方法对于简单 UI 非常有效。 但如果要隐藏的树形结构很大且包含了大量的 DOM 节点,那么性能就会变得很差。

你可以进行 状态提升 并在父组件中保存每个收件人的草稿消息。 这样即使子组件被移除了也无所谓,因为保留重要信息的是父组件。 这是最常见的解决方法。

除了 React 的 state,你也可以使用其他数据源。 例如,也许你希望即使用户不小心关闭页面也可以保存一份信息草稿。 要实现这一点,你可以让 Chat 组件通过读取 localStorage 对其 state 进行初始化,并把草稿保存在那里。

使用 Reducer 进行状态管理

Reducer 是处理状态的另一种方式。你可以通过三个步骤将 useState 迁移到 useReducer:

将设置状态的逻辑 修改 成 dispatch 的一个 action; 编写 一个 reducer 函数; 在你的组件中 使用 reducer。

使用 reducers 管理状态与直接设置状态略有不同。 它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)

当像这样分离关注点时,我们可以更容易地理解组件逻辑。 现在,事件处理程序只通过派发 action 来指定 发生了什么,而 reducer 函数通过响应 actions 来决定 状态如何更新。

对比 useState 和 useReducer

Reducers 并非没有缺点!以下是比较它们的几种方法:

代码体积: 通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。

可读性: 当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。

可调试性: 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。

可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。 一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。

个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的! 如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。

当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useState 和 useReducer。

编写一个好的 reducers

编写 reducers 时最好牢记以下两点:

reducers 必须是纯粹的。 这一点和 状态更新函数 是相似的,reducers 在是在渲染时运行的! (actions 会排队直到下一次渲染)。 这就意味着 reducers 必须纯净,即当输入相同时,输出也是相同的。 它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。 它们应该以不可变值的方式去更新 对象 和 数组。

每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由 reducer 管理的表单(包含五个表单项)中点击了 重置按钮,那么 dispatch 一个 reset_form 的 action 比 dispatch 五个单独的 set_field 的 action 更加合理。 如果你在一个 reducer 中打印了所有的 action 日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。 这对代码调试很有帮助!

使用 Context 进行深层数据传递

如果要在组件树中深入传递一些 prop,或者树里的许多组件需要使用相同的 prop,那么传递 prop 可能会变得很麻烦。 Context 允许父组件将一些信息提供给它下层的任何组件,不管该组件多深层也无需通过 props 逐层透传。 传递 props 是将数据通过 UI 树显式传递到使用它的组件的好方法。

但是当你需要在组件树中深层传递参数以及需要在组件间复用相同的参数时,传递 props 就会变得很麻烦。 最近的根节点父组件可能离需要数据的组件很远,状态提升 到太高的层级会导致 “逐层传递 props” 的情况。

Context 让父组件可以为它下面的整个组件树提供数据

Step 1:创建 context,并 将其从一个文件中导出

Step 2:使用 Context

Step 3:提供 context

把它们用 context provider 包裹起来 不同的 React context 不会覆盖彼此 Context 让你可以编写“适应周围环境”的组件,并且根据 在哪 (或者说 在哪个 context 中)来渲染它们不同的样子。

在使用 context 之前,你可以考虑以下几种替代方案:

从 传递 props 开始。 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。

抽象组件并 将 JSX 作为 children 传递 给它们。 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。 举个例子,你可能想传递一些像 posts 的数据 props 到不会直接使用这个参数的组件,类似 <Layout posts={posts} />。 取而代之的是,让 Layout 把 children 当做一个参数,然后渲染 <Layout><Posts posts={posts} /></Layout>。 这样就减少了定义数据的组件和使用数据的组件之间的层级。 如果这两种方法都不适合你,再考虑使用 context。

Context 的使用场景

主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。

当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。 某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。 在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。

路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。 这就是每个链接“知道”它是否处于活动状态的方式。 如果你创建自己的路由库,你可能也会这么做。

状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。 通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。

一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。

使用 Reducer 和 Context 进行状态扩展

Reducer 帮助你合并组件的状态更新逻辑。 Context 帮助你将信息深入传递给其他组件。 你可以将 reducers 和 context 组合在一起使用,以管理复杂应用的状态。

受控组件和非受控组件 Controlled and uncontrolled components

考虑该将组件视为“受控”(由 prop 驱动)或是“不受控”(由 state 驱动)是十分有益的。

It is common to call a component with some local state “uncontrolled”. 通常我们把包含“不受控制”状态的组件称为“非受控组件”

In contrast, you might say a component is “controlled” when the important information in it is driven by props rather than its own local state. This lets the parent component fully specify its behavior. 相反,当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是“受控组件”。 这就允许父组件完全指定其行为。

Uncontrolled components are easier to use within their parents because they require less configuration. But they’re less flexible when you want to coordinate them together. 非受控组件通常很简单,因为它们不需要太多配置。但是当你想把它们组合在一起使用时,就不那么灵活了。

Controlled components are maximally flexible, but they require the parent components to fully configure them with props. 受控组件具有最大的灵活性,但它们需要父组件使用 props 对其进行配置。

In practice, “controlled” and “uncontrolled” aren’t strict technical terms—each component usually has some mix of both local state and props. 在实践中,“受控”和“非受控”并不是严格的技术术语——通常每个组件都同时拥有内部状态和 props。

However, this is a useful way to talk about how components are designed and what capabilities they offer. 然而,这对于组件该如何设计和提供什么样功能的讨论是有帮助的。

When writing a component, consider which information in it should be controlled (via props), and which information should be uncontrolled (via state). 当编写一个组件时,你应该考虑哪些信息应该受控制(通过 props),哪些信息不应该受控制(通过 state)。

But you can always change your mind and refactor later. 当然,你可以随时改变主意并重构代码。

使用 ref 在不重新渲染的情况下“记住”信息

当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref

例如,可以使用 ref 来存储 timeout ID、DOM 元素 和其他不影响组件渲染输出的对象。

你可以用 ref.current 属性访问该 ref 的当前值。 这个值是有意被设置为可变的,意味着你既可以读取它也可以写入它。

像 state 一样,你可以让它指向任何东西:字符串、对象,甚至是函数 与 state 不同的是,ref 是一个普通的 JavaScript 对象,具有可以被读取和修改的 current 属性。

与 state 一样,React 会在每次重新渲染之间保留 ref。 但是,设置 state 会重新渲染组件,更改 ref 不会!

当一条信息用于渲染时,将它保存在 state 中。 当一条信息仅被事件处理器需要,并且更改它不需要重新渲染时,使用 ref 可能会更高效。

React state 的限制不适用于 ref。 例如,state 就像 每次渲染的快照,并且 不会同步更新。但是当你改变 ref 的 current 值时,它会立即改变 这是因为 ref 本身是一个普通的 JavaScript 对象, 所以它的行为就像对象那样。

当你使用 ref 时,也无需担心 避免变更。 只要你改变的对象不用于渲染,React 就不会关心你对 ref 或其内容做了什么。

在渲染期间读取 ref.current 会导致代码不可靠,因为 修改 ref.current 不会导致组件重新渲染。

何时使用 ref

通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。

以下是这些罕见情况中的几个:

存储 timeout ID 存储和操作 DOM 元素,我们将在 下一页 中介绍 存储不需要被用来计算 JSX 的其他对象。

如果你的组件需要存储一些值,但不影响渲染逻辑,请选择 ref。

ref 的最佳实践

遵循这些原则将使你的组件更具可预测性:

将 ref 视为脱围机制。

当你使用外部系统或浏览器 API 时,ref 很有用。 如果你很大一部分应用程序逻辑和数据流都依赖于 ref,你可能需要重新考虑你的方法。

不要在渲染过程中读取或写入 ref.current。

如果渲染过程中需要某些信息,请使用 state 代替。 由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。 (唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)

使用 ref 操作 DOM

由于 React 会自动更新 DOM 以匹配渲染输出,因此组件通常不需要操作 DOM。

但是,有时可能需要访问由 React 管理的 DOM 元素——例如聚焦节点、滚动到此节点,以及测量它的尺寸和位置。

React 没有内置的方法来执行此类操作,所以需要一个指向 DOM 节点的 ref 来实现。

如何使用 ref 回调管理 ref 列表

Hook 只能在组件的顶层被调用。不能在循环语句、条件语句或 map() 函数中调用 useRef

一种可能的解决方案是用一个 ref 引用其父元素,然后用 DOM 操作方法如 querySelectorAll 来寻找它的子节点。 然而,这种方法很脆弱,如果 DOM 结构发生变化,可能会失效或报错。

另一种解决方案是将函数传递给 ref 属性。 这称为 ref 回调。当需要设置 ref 时,React 将传入 DOM 节点来调用你的 ref 回调,并在需要清除它时传入 null 。 这使你可以维护自己的数组或 Map,并通过其索引或某种类型的 ID 访问任何 ref。

tsx
import { useRef } from 'react';

export default function CatFriends() {
  const itemsRef = useRef(null);

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // 首次运行时初始化 Map。
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>
          Tom
        </button>
        <button onClick={() => scrollToId(5)}>
          Maru
        </button>
        <button onClick={() => scrollToId(9)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          {catList.map(cat => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                }
                else {
                  map.delete(cat.id);
                }
              }}
            >
              <img
                src={cat.imageUrl}
                alt={`Cat #${cat.id}`}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: `https://placekitten.com/250/200?image=${i}`
  });
}

在这个例子中,itemsRef 保存的不是单个 DOM 节点,而是保存了包含列表项 ID 和 DOM 节点的 Map。 (Ref 可以保存任何值!) 每个列表项上的 ref 回调负责更新 Map 这使你可以之后从 Map 读取单个 DOM 节点。

使用 ref 转发管理 DOM 节点 forwardRef

默认情况下,React 不允许组件访问其他组件的 DOM 节点。甚至自己的子组件也不行!这是故意的。 Refs 是一种脱围机制,应该谨慎使用。 手动操作 另一个 组件的 DOM 节点会使你的代码更加脆弱。

相反,想要 暴露其 DOM 节点的组件必须选择该行为。一个组件可以指定将它的 ref “转发”给一个子组件

在设计系统中,将低级组件(如按钮、输入框等)的 ref 转发到它们的 DOM 节点是一种常见模式。

另一方面,像表单、列表或页面段落这样的高级组件通常不会暴露它们的 DOM 节点,以避免对 DOM 结构的意外依赖。

使用命令句柄暴露一部分 API

useImperativeHandle 指示 React 将你自己指定的对象作为父组件的 ref 值 在这种情况下,ref “句柄”不是 DOM 节点,而是你在 useImperativeHandle 调用中创建的自定义对象。

React 何时添加 refs

在 React 中,每次更新都分为 两个阶段:

在 渲染 阶段, React 调用你的组件来确定屏幕上应该显示什么。 在 提交 阶段, React 把变更应用于 DOM。

通常,你 不希望 在渲染期间访问 refs。

这也适用于保存 DOM 节点的 refs。 在第一次渲染期间,DOM 节点尚未创建,因此 ref.current 将为 null。 在渲染更新的过程中,DOM 节点还没有更新。 所以读取它们还为时过早。

React 在提交阶段设置 ref.current。

在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。 更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。

通常,你将从事件处理器访问 refs。

如果你想使用 ref 执行某些操作,但没有特定的事件可以执行此操作,你可能需要一个 effect。

用 flushSync 同步更新 state

在 React 中,state 更新是排队进行的。通常,这就是你想要的。 要解决此问题,你可以强制 React 同步更新(“刷新”)DOM。 为此,从 react-dom 导入 flushSync 并将 state 更新包裹 到 flushSync 调用中

请注意,为了强制 React 在滚动前更新 DOM,flushSync 调用是必需的

使用 refs 操作 DOM 的最佳实践

Refs 是一种脱围机制。你应该只在你必须“跳出 React”时使用它们。 这方面的常见示例包括管理焦点、滚动位置或调用 React 未暴露的浏览器 API。

如果你坚持聚焦和滚动等非破坏性操作,应该不会遇到任何问题。 但是,如果你尝试手动修改 DOM,则可能会与 React 所做的更改发生冲突。

避免更改由 React 管理的 DOM 节点 但是,这并不意味着你完全不能这样做。它需要谨慎。 你可以安全地修改 React 没有理由更新的部分 DOM

使用 Effect 实现与 React 之外的系统同步

有些组件需要与外部系统同步。

例如,可能需要根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。

与处理特定事件的事件处理程序不同,Effect 在渲染后运行一些代码。

使用它将组件与 React 之外的系统同步。

许多 Effect 也会自行“清理”。 例如,与聊天服务器建立连接的 Effect 应该返回一个 cleanup 函数,告诉 React 如何断开组件与该服务器的连接

你可能不需要 Effect

Effect 是 React 范式中的一种脱围机制。

它们可以“逃出” React 并使组件和一些外部系统同步。

如果没有涉及到外部系统(例如,需要根据一些 props 或 state 的变化来更新一个组件的 state),不应该使用 Effect。

移除不必要的 Effect 可以让代码更容易理解,运行得更快,并且更少出错。

有两种常见的不必使用 Effect 的情况:

  • 不必为了渲染而使用 Effect 来转换数据。 相反,在渲染时进行尽可能多地计算

  • 不必使用 Effect 来处理用户事件。

响应式 Effect 的生命周期

Effect 的生命周期不同于组件。

组件可以挂载、更新或卸载。 Effect 只能做两件事:开始同步某些东西,然后停止同步它。

如果 Effect 依赖于随时间变化的 props 和 state,这个循环可能会发生多次。

从 Effect 中分离事件

事件处理程序仅在再次执行相同的交互时重新运行。

与事件处理程序不同,如果 Effect 读取的任何值(如 props 或 state)与上次渲染期间不同,则会重新同步。

有时,需要混合两种行为:Effect 重新运行以响应某些值而不是其他值。

Effect 中的所有代码都是 响应式的。如果它读取的某些响应式的值由于重新渲染而发生变化,它将再次运行。

移除 Effect 依赖

当你写 Effect 时,代码检查器会验证是否已经将 Effect 读取的每一个响应式值(如 props 和 state)包含在 Effect 的依赖列表中。

这可以确保 Effect 与组件的 props 和 state 保持同步。

不必要的依赖关系可能会导致 Effect 运行过于频繁,甚至产生无限循环。

删除它们的方式取决于具体情况。

使用自定义 Hook 复用逻辑

你可以创建自定义 Hooks,将它们组合在一起,在它们之间传递数据,并在组件之间重用它们。

随着应用不断变大,你将减少手动编写的 Effect,因为你将能够重用已经编写的自定义 Hooks。

什么是 Effect,它与事件(event)有何不同?

在谈到 Effect 之前,你需要熟悉 React 组件中的两种逻辑类型:

Rendering code (introduced in Describing the UI) lives at the top level of your component. 渲染逻辑代码(在 描述 UI 中有介绍)位于组件的顶层。

This is where you take the props and state, transform them, and return the JSX you want to see on the screen. 你将在这里接收 props 和 state,并对它们进行转换,最终返回你想在屏幕上看到的 JSX。

Rendering code must be pure. Like a math formula, it should only calculate the result, but not do anything else. 渲染的代码必须是纯粹的——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。

Event handlers (introduced in Adding Interactivity) are nested functions inside your components that do things rather than just calculate them. 事件处理程序(在 添加交互性 中介绍)是嵌套在组件内部的函数,而不仅仅是计算函数。

An event handler might update an input field, submit an HTTP POST request to buy a product, or navigate the user to another screen. 事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品,或者将用户导航到另一个屏幕。

Event handlers contain “side effects” (they change the program’s state) caused by a specific user action (for example, a button click or typing). 事件处理程序包含由特定用户操作(例如按钮点击或键入)引起的“副作用”(它们改变了程序的状态)

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Effect 允许你指定由渲染本身,而不是特定事件引起的副作用

Effects run at the end of a commit after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library). Effect 在屏幕更新后的 提交阶段 运行。 这是一个很好的时机,可以将 React 组件与某个外部系统(如网络或第三方库)同步。

Effect 在 React 中是专有定义——由渲染引起的副作用。 为了指代更广泛的编程概念,也可以将其称为“副作用(side effect)”。 与事件不同,Effect 是由渲染本身,而非特定交互引起的

在渲染期间调用 ref.current.focus() 本身是不正确的。因为这会产生“副作用”。 副作用要么应该放在事件处理程序里面,要么在 useEffect 中。 在这种情况下,副作用是组件渲染引起的,而不是任何特定的交互引起的,因此应该将它放在 Effect 中。

你可能不需要 Effect

Don’t rush to add Effects to your components. 不要随意在你的组件中使用 Effect。

Keep in mind that Effects are typically used to “step out” of your React code and synchronize with some external system. 记住,Effect 通常用于暂时“跳出” React 代码并与一些 外部 系统进行同步。

This includes browser APIs, third-party widgets, network, and so on. 这包括浏览器 API、第三方小部件,以及网络等等。

If your Effect only adjusts some state based on other state, you might not need an Effect. 如果你想用 Effect 仅根据其他状态调整某些状态,那么 你可能不需要 Effect。

Effect 是 React 范式中的一种脱围机制。 它们让你可以 “逃出” React 并使组件和一些外部系统同步,比如非 React 组件、网络和浏览器 DOM。 如果没有涉及到外部系统(例如,你想根据 props 或 state 的变化来更新一个组件的 state),你就不应该使用 Effect。

移除不必要的 Effect 可以让你的代码更容易理解,运行得更快,并且更少出错。

如何移除不必要的 Effect

有两种不必使用 Effect 的常见情况:

你不必使用 Effect 来转换渲染所需的数据。

例如,你想在展示一个列表前先做筛选。 你的直觉可能是写一个当列表变化时更新 state 变量的 Effect。然而,这是低效的。 当你更新这个 state 时,React 首先会调用你的组件函数来计算应该显示在屏幕上的内容。 然后 React 会把这些变化“提交”到 DOM 中来更新屏幕。 然后 React 会执行你的 Effect。 如果你的 Effect 也立即更新了这个 state,就会重新执行整个流程。 为了避免不必要的渲染流程,应在你的组件顶层转换数据。 这些代码会在你的 props 或 state 变化时自动重新执行。

你不必使用 Effect 来处理用户事件。

例如,你想在用户购买一个产品时发送一个 /api/buy 的 POST 请求并展示一个提示。 在这个购买按钮的点击事件处理函数中,你确切地知道会发生什么。 但是当一个 Effect 运行时,你却不知道用户做了什么(例如,点击了哪个按钮)。 这就是为什么你通常应该在相应的事件处理函数中处理用户事件。

根据 props 或 state 来更新 state

如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值。

这将使你的代码更快(避免了多余的 “级联” 更新)、更简洁(移除了一些代码)以及更少出错(避免了一些因为不同的 state 变量之间没有正确同步而导致的问题)

如果你可以在渲染期间计算某些内容,则不需要使用 Effect

缓存昂贵的计算

你可以使用 useMemo Hook 缓存(或者说 记忆(memoize))一个昂贵的计算。

你传入 useMemo 的函数会在渲染期间执行,所以它仅适用于 纯函数 场景。

想要缓存昂贵的计算,请使用 useMemo 而不是 useEffect

如何判断计算是昂贵的?

一般来说只有你创建或循环遍历了成千上万个对象时才会很耗费时间。

如果总耗时达到了一定量级(比方说 1ms 或更多),那么把计算结果记忆(memoize)起来可能是有意义的。

useMemo 不会让 第一次 渲染变快。它只是帮助你跳过不必要的更新。

当 props 变化时重置所有 state

通常,当在相同的位置渲染相同的组件时,React 会保留状态

将你的组件拆分为两个组件,并从外部的组件传递一个 key 属性给内部的组件

想要重置整个组件树的 state,请传入不同的 key

当 prop 变化时调整部分 state

有时候,当 prop 变化时,你可能只想重置或调整部分 state ,而不是所有 state。

像这样 存储前序渲染的信息 可能很难理解,但它比在 Effect 中更新这个 state 要好

在渲染期间更新组件时,React 会丢弃已经返回的 JSX 并立即尝试重新渲染。

为了避免非常缓慢的级联重试,React 只允许在渲染期间更新 同一 组件的状态。 如果你在渲染期间更新另一个组件的状态,你会看到一条报错信息。

你可以像这样调整 state,但任何其他副作用(比如变化 DOM 或设置的延时)应该留在事件处理函数或 Effect 中,以 保持组件纯粹。

虽然这种方式比 Effect 更高效,但大多数组件也不需要它。 无论你怎么做,根据 props 或其他 state 来调整 state 都会使数据流更难理解和调试。 总是检查是否可以通过添加 key 来重置所有 state,或者 在渲染期间计算所需内容。

在事件处理函数中共享逻辑

避免:在 Effect 中处理属于事件特定的逻辑 当你不确定某些代码应该放在 Effect 中还是事件处理函数中时,先自问 为什么 要执行这些代码。 Effect 只用来执行那些页面显示出来给用户时,组件 需要执行 的代码

发送 POST 请求

当你决定将某些逻辑放入事件处理函数还是 Effect 中时,你需要回答的主要问题是:从用户的角度来看它是 怎样的逻辑。

如果这个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。

如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。

链式计算

避免:链接多个 Effect 仅仅为了相互触发调整 state

尽可能在渲染期间进行计算,以及在事件处理函数中调整 state

在某些情况下,你 无法 在事件处理函数中直接计算出下一个 state。

例如,试想一个具有多个下拉菜单的表单,如果下一个下拉菜单的选项取决于前一个下拉菜单选择的值。

这时,Effect 链是合适的,因为你需要与网络进行同步

如果你需要更新多个组件的 state,最好在单个事件处理函数中处理。

初始化应用

有些逻辑只需要在应用加载时执行一次。

如果某些逻辑必须在 每次应用加载时执行一次,而不是在 每次组件挂载时执行一次,可以添加一个顶层变量来记录它是否已经执行过了

顶层代码会在组件被导入时执行一次——即使它最终并没有被渲染。 为了避免在导入任意组件时降低性能或产生意外行为,请不要过度使用这种方法。 将应用级别的初始化逻辑保留在像 App.js 这样的根组件模块或你的应用入口中。

组件 显示 时就需要执行的代码应该放在 Effect 中,否则应该放在事件处理函数中

通知父组件有关 state 变化的信息

避免:onChange 处理函数执行的时间太晚了 React 会 批量 处理来自不同组件的更新,所以只会有一个渲染流程。

“状态提升” 允许父组件通过切换自身的 state 来完全控制 Toggle 组件。 这意味着父组件会包含更多的逻辑,但整体上需要关心的状态变少了。 每当你尝试保持两个不同的 state 变量之间的同步时,试试状态提升!

当你尝试在不同组件中同步 state 变量时,请考虑状态提升。

将数据传递给父组件

避免:在 Effect 中传递数据给父组件

在 React 中,数据从父组件流向子组件。 当你在屏幕上看到了一些错误时,你可以通过一路追踪组件树来寻找错误信息是从哪个组件传递下来的,从而找到传递了错误的 prop 或具有错误的 state 的组件。

当子组件在 Effect 中更新其父组件的 state 时,数据流变得非常难以追踪。 既然子组件和父组件都需要相同的数据,那么可以让父组件获取那些数据,并将其 向下传递 给子组件

订阅外部 store

有时候,你的组件可能需要订阅 React state 之外的一些数据。 这些数据可能来自第三方库或内置浏览器 API。 由于这些数据可能在 React 无法感知的情况下发变化,你需要在你的组件中手动订阅它们。 这经常使用 Effect 来实现

尽管通常可以使用 Effect 来实现此功能,但 React 为此针对性地提供了一个 Hook 用于订阅外部 store。 删除 Effect 并将其替换为调用 useSyncExternalStore

获取数据

你可以使用 Effect 获取数据,但你需要实现清除逻辑以避免竞态条件

“竞态条件”:两个不同的请求 “相互竞争”,并以与你预期不符的顺序返回

为了修复这个问题,你需要添加一个 清理函数 来忽略较早的返回结果

虽然仅仅使用自定义 Hook 不如使用框架内置的数据获取机制高效,但将数据获取逻辑移动到自定义 Hook 中将使后续采用高效的数据获取策略更加容易。

一般来说,当你不得不编写 Effect 时,请留意是否可以将某段功能提取到专门的内置 API 或一个更具声明性的自定义 Hook 中 你会发现组件中的原始 useEffect 调用越少,维护应用将变得更加容易。

如何编写 Effect

编写 Effect 需要遵循以下三个规则:

声明 Effect。

默认情况下,Effect 会在每次渲染后都会执行。

每当你的组件渲染时,React 将更新屏幕,然后运行 useEffect 中的代码。 换句话说,useEffect 会把这段代码放到屏幕更新渲染之后执行。

在 React 中,JSX 的渲染必须是纯粹操作,不应该包含任何像修改 DOM 的副作用。

把调用 DOM 方法的操作封装在 Effect 中,你可以让 React 先更新屏幕,确定相关 DOM 创建好了以后然后再运行 Effect。

Effect 通常应该使组件与 外部 系统保持同步。 如果没有外部系统,你只想根据其他状态调整一些状态,那么 你也许不需要 Effect

指定 Effect 依赖。

大多数 Effect 应该按需执行,而不是在每次渲染后都执行。 例如,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。

依赖数组可以包含多个依赖项。 当指定的所有依赖项在上一次渲染期间的值与当前值完全相同时,React 会跳过重新运行该 Effect。 React 使用 Object.is 比较依赖项的值

请注意,不能随意选择依赖项。 如果你指定的依赖项不能与 Effect 代码所期望的相匹配时,lint 将会报错,这将帮助你找到代码中的问题。 如果你不希望某些代码重新运行,那么你应当 重新编辑 Effect 代码本身,使其不需要该依赖项。

为什么依赖数组中可以省略 ref?

这是因为 ref 具有 稳定 的标识:React 保证 每轮渲染中调用 useRef 所产生的引用对象时,获取到的对象引用总是相同的, 即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。 因此,依赖数组中是否包含它并不重要。当然也可以包括它

例如,如果 ref 是从父组件传递的,则必须在依赖项数组中指定它。 这样做是合适的,因为无法确定父组件是否始终是传递相同的 ref,或者可能是有条件地传递几个 ref 之一。 因此,你的 Effect 将取决于传递的是哪个 ref。

useState 返回的 set 函数 也有稳定的标识符,所以也可以把它从依赖数组中忽略掉。 如果在忽略某个依赖项时 linter 不会报错,那么这么做就是安全的。

必要时添加清理(cleanup)函数。

有时 Effect 需要指定如何停止、撤销,或者清除它的效果。 例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”。

如果 Effect 订阅了某些事件,清理函数应该退订这些事件

如果 Effect 对某些内容加入了动画,清理函数应将动画重置

如果 Effect 将会获取数据,清理函数应该要么 中止该数据获取操作,要么忽略其结果

React 总是在执行下一轮渲染的 Effect 之前清理上一轮渲染的 Effect

每一轮渲染都有自己的 Effect,Effect 也是渲染输出的一部分

如果 Effect 因为重新挂载而中断,那么需要实现一个清理函数。

React 将在下次 Effect 运行之前以及卸载期间这两个时候调用清理函数。

其实,每个 Effect 都可以在里面设置一个 ignore 标记变量。 在最开始,ignore 被设置为 false。 然而,当 Effect 执行清理函数后,ignore 就会被设置为 true。 所以此时请求完成的顺序并不重要

不要在 Effect 中执行购买商品一类的操作

“购买”的操作不应由组件的挂载、渲染引起的; 它是由特定的交互作用引起的,它应该只在用户按下按钮时运行。

Transition & Suspense & ErrorBoundary & useDeferredValue

Transition

使用场景:多tab切换,其中某个tab B渲染时比较卡顿。由A 到B然后立即点击C时,若不使用 useTransition,页面会卡顿。

路由切换,A页面访问B页面然后又立刻访问C页面,若B页面比较卡顿,此时推荐使用 useTransition

特点

易错点:

  • 不应将控制输入框的状态变量标记为 transition

这是因为 Transition 是非阻塞的,但是在响应更改事件时更新输入应该是同步的。如果想在输入时运行一个 transition,那么有两种做法:

  1. 声明两个独立的状态变量:一个用于输入状态(它总是同步更新),另一个用于在 Transition 中更新。这样,便可以使用同步状态控制输入,并将用于 Transition 的状态变量(它将“滞后”于输入)传递给其余的渲染逻辑。
  2. 或者使用一个状态变量,并添加useDeferredValue,它将“滞后”于实际值,并自动触发非阻塞的重新渲染以“追赶”新值。
  • 传递给 startTransition 的函数必须是同步的

  • useTransition 是一个 Hook,因此不能在组件外部调用。请使用独立的 startTransition 方法。它们的工作方式相同,但不提供 isPending 标记。

  • 传递给startTransition的函数会立即执行

tsx
// React 运行的简易版本

let isInsideTransition = false;

function startTransition(scope) {
  isInsideTransition = true;
  scope();
  isInsideTransition = false;
}

function setState() {
  if (isInsideTransition) {
    // ……安排 Transition 状态更新……
  }
  else {
    // ……安排紧急状态更新……
  }
}

import { ErrorBoundary } from "react-error-boundary";

tsx
import { useTransition } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // For demonstration purposes to show Error Boundary
  if (comment == null) {
    throw new Error('Example Error: An error thrown to trigger error boundary');
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Intentionally not passing a comment
          // so error gets thrown
          addComment();
        });
      }}
    >
      Add comment
    </button>
  );
}

useDeferredValue

输入 "a",等待结果加载完成,然后将输入框编辑为 "ab"。 注意,现在你看到的不是 suspense 后备方案,而是旧的结果列表,直到新的结果加载完成

tsx
import { Suspense, useDeferredValue, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

你可以将其看成两个步骤:

首先,React 会使用新的 query 值("ab")和旧的 deferredQuery 值(仍为 "a")重新渲染。 传递给结果列表的 deferredQuery 值是延迟的,它“滞后于” query 值。

在后台,React 尝试重新渲染,并将 query 和 deferredQuery 两个值都更新为 "ab"。 如果此次重新渲染完成,React 将在屏幕上显示它。但是,如果它 suspense(即 "ab" 的结果尚未加载),React 将放弃这次渲染,并在数据加载后再次尝试重新渲染。用户将一直看到旧的延迟值,直到数据准备就绪。

被推迟的“后台”渲染是可中断的。例如,如果你再次在输入框中输入,React 将会中断渲染,并从新值开始重新渲染。React 总是使用最新提供的值。

注意,每次按键仍会发起一个网络请求。这里延迟的是显示结果(直到它们准备就绪),而不是网络请求本身。即使用户继续输入,每个按键的响应都会被缓存,所以按下 Backspace 键是瞬时的,不会再次获取数据。

易错点

输入的是原始值,展示时使用的是 useDeferredValue 的返回值

query !== deferredQuery 表明内容已过时

延迟一个值与防抖和节流之间有什么不同?

在上述的情景中,你可能会使用这两种常见的优化技术:

防抖 是指在用户停止输入一段时间(例如一秒钟)之后再更新列表。 节流 是指每隔一段时间(例如最多每秒一次)更新列表。 虽然这些技术在某些情况下是有用的,但 useDeferredValue 更适合优化渲染,因为它与 React 自身深度集成,并且能够适应用户的设备。

与防抖或节流不同,useDeferredValue 不需要选择任何固定延迟时间。如果用户的设备很快(比如性能强劲的笔记本电脑),延迟的重渲染几乎会立即发生并且不会被察觉。如果用户的设备较慢,那么列表会相应地“滞后”于输入,滞后的程度与设备的速度有关。

此外,与防抖或节流不同,useDeferredValue 执行的延迟重新渲染默认是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一次键盘输入,React 会放弃该重新渲染,先处理键盘输入,然后再次开始在后台渲染。相比之下,防抖和节流仍会产生不顺畅的体验,因为它们是阻塞的:它们仅仅是将渲染阻塞键盘输入的时刻推迟了。

如果你要优化的工作不是在渲染期间发生的,那么防抖和节流仍然非常有用。例如,它们可以让你减少网络请求的次数。你也可以同时使用这些技术。

Contributors

作者:Long Mo
字数统计:27.4k 字
阅读时长:96 分钟
Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs