描述UI

组件

React 组件是常规的 JavaScript 函数,但 组件的名称必须以大写字母开头,否则它们将无法运行!

定义一个react组件。

1
2
3
function Hello(){
return <i>Hello</i>
}

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

组件的导入和导出

组件的导出主要分为具名导出和默认导出,默认导出在一个文件中只能有一个。

语法导出语句导入语句
默认export default function Button() {}import Button from ‘./Button.js’;
具名export function Button() {}import { Button } from ‘./Button.js’;

JSX书写标签语言

JSX and React 是相互独立的 东西。但它们经常一起使用,但你 可以 单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。

  • 只能返回一个根元素。

如果想要在一个组件中包含多个元素,需要用一个父标签把它们包裹起来

如果你不想在标签中增加一个额外的<div>,可以用<></>元素来代替。

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

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

  • 标签必须闭合。

JSX 要求标签必须正确闭合。像<img>这样的自闭合标签必须书写成<img />,而像<li>oranges这样只有开始标签的元素必须带有闭合标签,需要改为<li>oranges</li>

  • 使用驼峰式命名法给 所有 大部分属性命名

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

在 JSX 中通过大括号使用 JavaScript

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

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

传递Props

React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。

父组件传递props:

1
2
3
4
5
6
7
8
export default function Profile() {
return (
<Avatar
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
);
}

子组件读取props:这里使用了解构语法。

1
2
3
function Avatar({ person, size }) {
// 在这里 person 和 size 是可访问的
}

如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 = 和默认值来进行解构:

1
2
3
function Avatar({ person, size = 90 }) {
// ...
}

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

子组件传递。将父组件包裹的内容渲染出来。

父组件将在名为children的 prop 中接收到该内容。例如,下面的Card组件将接收一个被设为 <Avatar />childrenprop 并将其包裹在 div 中渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}

export default function Profile() {
return (
<Card>
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageId: 'YfeOqp2'
}}
/>
</Card>
);
}

条件渲染

在 React 中,你可以通过使用 JavaScript 的<font style="color:rgb(35, 39, 47);"> </font>if<font style="color:rgb(35, 39, 47);"> </font>语句、&&<font style="color:rgb(35, 39, 47);"> </font>? :<font style="color:rgb(35, 39, 47);"> </font>运算符来选择性地渲染 JSX。

<font style="color:rgb(35, 39, 47);">if</font>

1
2
3
4
5
6
function Item({ name, isPacked }) {
if (isPacked) {
return <li className="item">{name} ✔</li>;
}
return <li className="item">{name}</li>;
}

? :

1
2
3
4
5
return (
<li className="item">
{isPacked ? name + ' ✔' : name}
</li>
);

&&

1
2
3
4
5
6
7
function Item({ name, isPacked }) {
return (
<li className="item">
{name} {isPacked && '✔'}
</li>
);
}

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

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

渲染列表

使用map渲染列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const people = [
{
id: 0, // 在 JSX 中作为 key 使用
name: '凯瑟琳·约翰逊',
profession: '数学家',
accomplishment: '太空飞行相关数值的核算',
imageId: 'MK3eW3A',
},
...
];

export default function List() {
const listItems = people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
</li>
);
return <ul>{listItems}</ul>;
}

如果你想让每个列表项都输出多个 DOM 节点而非一个的话,该怎么做呢?
Fragment 语法的简写形式 <> </> 无法接受 key 值,所以你只能要么把生成的节点用一个

标签包裹起来,要么使用长一点但更明确的写法:

1
2
3
4
5
6
7
8
import { Fragment } from 'react';
// ...
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);

这里的 Fragment 标签本身并不会出现在 DOM 上,这串代码最终会转换成 <h1><p><h1><p>…… 的列表。
key值具有唯一标识一个信息的能力,但不能是随机值。

添加交互

添加响应事件

1
2
3
4
5
6
7
8
9
10
11
export default function Button() {
function handleClick() {
alert('你点击了我!');
}

return (
<button onClick={handleClick}>
点我
</button>
);
}

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

传递一个函数(正确)调用一个函数(错误)

区别很微妙。在第一个示例中,handleClick 函数作为 onClick 事件处理函数传递。这会让 React 记住它,并且只在用户点击按钮时调用你的函数。

在第二个示例中,handleClick() 中最后的 () 会在 渲染 过程中 立即 触发函数,即使没有任何点击。这是因为在 JSX { 和 } 之间的 JavaScript 会立即执行。

当你编写内联代码时,同样的陷阱可能会以不同的方式出现:

传递一个函数(正确)调用一个函数(错误)
<button onClick={() => alert(‘…’)}><button onClick={alert(‘…’)}>

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

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

1
2
3
4
5
6
7
8
9
10
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}

某些浏览器事件具有与事件相关联的默认行为。例如,点击

表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面。

你可以调用事件对象中的 e.preventDefault() 来阻止这种情况发生:

1
2
3
4
5
6
7
8
9
10
11
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('提交表单!');
}}>
<input />
<button>发送</button>
</form>
);
}

State:组件的记忆

为什么需要state?

在组件内部某些值直接修改后,UI上对应内容并没有发生改变,react并不知道你修改了内容。

state具有保存数据和触发UI渲染的能力。

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
const [index, setIndex] = useState(0);

function handleClick() {
setIndex(index + 1);
}
return (
<>
<p>{index}</p>
<button onClick={handleClick}>
Next
</button>
</>
);
}

深入了解state

你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。

在同一组件的每次渲染中,Hooks 都依托于一个稳定的调用顺序

推荐阅读:https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

  • 在一个 React 应用中一次屏幕更新都会发生以下三个步骤:
    1. 触发
    2. 渲染
    3. 提交
  • 你可以使用严格模式去找到组件中的错误
  • 如果渲染结果与上次一样,那么 React 将不会修改 DOM

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

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

<font style="color:rgb(35, 39, 47);">setState</font>被调用后,只会为下一次渲染变更 state 的值。例如:

1
2
3
4
5
6
number = 0
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>

即使这里连续调用三次,但number此失都是0,所以最终的结果只会让number + 1

把一系列 state 更新加入队列

上面的代码可以进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from 'react';

export default function Counter() {
const [number, setNumber] = useState(0);

return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}

在这里,n => n + 1 被称为 更新函数。当你将它传递给一个 state 设置函数时:

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

更新state中的对象

把所有存放在 state 中的 JavaScript 对象都视为只读的

在更新数据时,要创建一个新对象并把它传递给 state 的设置函数。

使用展开语法复制对象:

1
2
3
4
setPerson({
...person, // 复制上一个 person 中的所有字段
firstName: e.target.value // 但是覆盖 firstName 字段
});

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

使用 Immer 编写简洁的更新逻辑。https://github.com/immerjs/use-immer

为什么在 React 中不推荐直接修改 state?

有以下几个原因:

  • 调试:如果你使用 console.log 并且不直接修改 state,你之前日志中的 state 的值就不会被新的 state 变化所影响。这样你就可以清楚地看到两次渲染之间 state 的值发生了什么变化
  • 优化:React 常见的 优化策略 依赖于如果之前的 props 或者 state 的值和下一次相同就跳过渲染。如果你从未直接修改 state ,那么你就可以很快看到 state 是否发生了变化。如果 prevObj === obj,那么你就可以肯定这个对象内部并没有发生改变。
  • 新功能:我们正在构建的 React 的新功能依赖于 state 被 像快照一样看待 的理念。如果你直接修改 state 的历史版本,可能会影响你使用这些新功能。
  • 需求变更:有些应用功能在不出现任何修改的情况下会更容易实现,比如实现撤销/恢复、展示修改历史,或是允许用户把表单重置成某个之前的值。这是因为你可以把 state 之前的拷贝保存到内存中,并适时对其进行再次使用。如果一开始就用了直接修改 state 的方式,那么后面要实现这样的功能就会变得非常困难。
  • 更简单的实现:React 并不依赖于 mutation ,所以你不需要对对象进行任何特殊操作。它不需要像很多“响应式”的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来,或者在初始化时做其他工作。这也是为什么 React 允许你把任何对象存放在 state 中——不管对象有多大——而不会造成有任何额外的性能或正确性问题的原因。

在实践中,你经常可以“侥幸”直接修改 state 而不出现什么问题,但是我们强烈建议你不要这样做,这样你就可以使用我们秉承着这种理念开发的 React 新功能。未来的贡献者甚至是你未来的自己都会感谢你的!

更新state中的数组

同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。

下面是常见数组操作的参考表。当你操作 React state 中的数组时,你需要避免使用左列的方法,而首选右列的方法:

避免使用 (会改变原始数组)推荐使用 (会返回一个新数组)
添加元素pushunshiftconcat[…arr] 展开语法(例子
删除元素popshiftsplicefilterslice例子
替换元素splicearr[i] = … 赋值map例子
排序reversesort先将数组复制一份(例子

或者,你可以使用 Immer ,这样你便可以使用表格中的所有方法了。

状态管理

state相关

构建state的原则:

  1. 合并关联的 state。如果你总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量。
  2. 避免互相矛盾的 state。当 state 结构中存在多个相互矛盾或“不一致”的 state 时,你就可能为此会留下隐患。应尽量避免这种情况。
  3. 避免冗余的 state。如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。
  4. 避免重复的 state。当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。
  5. 避免深度嵌套的 state。深度分层的 state 更新起来不是很方便。如果可能的话,最好以扁平化方式构建 state。

不要在 state 中镜像 props。

组件之间的状态共享:

有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。

对state进行保留和重置:

当向一个组件添加状态时,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。

只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state。

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

假设通过if语言判断,根据条件返回了带有不同参数的同一个组件<Counter />,两个<Counter />标签被渲染在了相同的位置。 React 不知道你的函数里是如何进行条件判断的,它只会“看到”你返回的树。在这两种情况下App组件都会返回一个包裹着<Counter />作为第一个子组件的 div。这就是 React 认为它们是同一个<Counter />的原因。

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

为什么应该在组件函数中嵌套定义组件函数?

如果在组件函数内嵌套定义并使用了组件函数,当外层组件重新渲染时,内部嵌套定义的组件会被重新创建,导致其丢失掉原本的state。

如何在相同位置充值state?

  1. 将组件渲染在不同位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useState } from 'react';

export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</button>
</div>
);
}
  1. 使用key重置state。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from 'react';

export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</button>
</div>
);
}

迁移逻辑状态到reducer中

假设在一个组件中,我们对一个state数组有添加、编辑、删除等操作,过于分散的事件处理程序将使得很难一眼看清所有的组件状态更新逻辑。这时就可以用到reducer。

  1. 编写一个reducer。接收两个参数:当前state和action对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//   reducer(state,action)=>newstate
// reducer 是一个函数,接受当前的state(状态)和action(要进行的操作)
// 计算并返回最新的state
const reducer = (state, action) => {
switch (action.type) {
case "add":
return [
...state,
action.data,
];
//...
default:
return state;
}
};
  1. 在组件中使用reducer。useReducer接受两个参数:一个reducer函数;一个初始的state。
1
2
3
4
5
6
7
8
9
10
11
import { useReducer } from 'react';

function App(){
const [state, dispatch] = useReducer(reducer, []);
function addData(){
dispatch({type:'add',data:1})
}
return (
<button onClick={addData}>点我添加一个数字1</button>
)
}

使用Context深层传递参数

作用类似于vue中的provider,可以使得组件的参数进行深层次传递,例如:祖父组件传递给子孙组件。

  1. 创建Context。
1
2
3
import { createContext } from 'react';

export const AppContext = createContext(1); // 只需默认值这么一个参数
  1. 使用Context。
1
2
3
4
5
6
7
import { useContext } from 'react';
import { AppContext } from './AppContext.js';

export default function Heading({ children }) {
const level = useContext(AppContext); // level会是默认值
// ...
}
  1. 提供Context。这里可以将一个state通过Context传递下去。
1
2
3
4
5
6
7
8
9
10
11
import { AppContext } from './LevelContext.js';

export default function Section({ level, children }) {
return (
<section className="section">
<AppContext.Provider value={level}>
{children}
</AppContext.Provider>
</section>
);
}

如果Section组件嵌套使用的话,每个Section都可以读取到上层Sectionlevel

Context使用场景:

  • 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
  • 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
  • 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
  • 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。

useContext和useReducer配合使用

useContext和useReducer一起使用可以达到redux的效果。

  1. 创建context。
1
2
3
4
import { createContext } from 'react';

export const TasksContext = createContext(null); // 传递state
export const TasksDispatchContext = createContext(null); // 传递dispatch
  1. 创建reducer。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useReducer } from 'react';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
// ....
}
}

let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
  1. 将state和dispatch函数放入context。
1
2
3
4
5
6
7
8
9
10
11
12
13
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
  1. 使用context。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...

脱围机制

使用ref引用值

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

使用举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function Counter() {
let ref = useRef(0);

function handleClick() {
ref.current = ref.current + 1;
alert('你点击了 ' + ref.current + ' 次!');
}

return (
<button onClick={handleClick}>
点击我!
</button>
);
}

useRef返回值:

{

current:0// 你向 useRef 传入的值

}

ref 和 state 的不同之处

也许你觉得 ref 似乎没有 state 那样“严格” —— 例如,你可以改变它们而非总是必须使用 state 设置函数。但在大多数情况下,我们建议你使用 state。ref 是一种“脱围机制”,你并不会经常用到它。 以下是 state 和 ref 的对比:

refstate
useRef(initialValue)返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
更改时不会触发重新渲染更改时触发重新渲染。
可变 —— 你可以在渲染过程之外修改和更新 current 的值。“不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。
你不应在渲染期间读取(或写入) current 值。你可以随时读取 state。但是,每次渲染都有自己不变的 state 快照

什么时候使用?

  • 存储 timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要被用来计算 JSX 的其他对象。

最佳实践:

  • 将 ref 视为脱围机制。当你使用外部系统或浏览器 API 时,ref 很有用。如果你很大一部分应用程序逻辑和数据流都依赖于 ref,你可能需要重新考虑你的方法。
  • 不要在渲染过程中读取或写入 **ref.current** 如果渲染过程中需要某些信息,请使用 state 代替。由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)

使用ref操作DOM

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useRef } from 'react';

export default function Form() {
const inputRef = useRef(null);

function handleClick() {
inputRef.current.focus();
}

return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}

HOOK只能在顶层调用,如果我有一个列表,想为每一项都绑定一个ref,在map()中调用Hook是不可行的!

方案:

  1. ref引用其父组件,这种方法很脆弱,如果 DOM 结构发生变化,可能会失效或报错。
  2. 将函数传递给 **ref** 属性
1
2
3
4
5
6
7
<div ref={(node) => {
console.log(node);

return () => {
console.log('Clean up', node)
}
}}>

将ref放在组件上:

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

相反,想要 暴露其 DOM 节点的组件必须选择该行为。一个组件可以指定将它的 ref “转发”给一个子组件。下面是 MyInput 如何使用 forwardRef API:

1
2
3
4
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

这样就可以将ref放在<MyInput />组件上了。

子组件限制父组件能够给访问的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
import {
forwardRef, useImperativeHandle} from 'react';

const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});

在这种情况下,ref “句柄”不是 DOM 节点,而是你在useImperativeHandle调用中创建的自定义对象。

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

如何实现调用<font style="color:rgb(35, 39, 47);">setTodos</font>后更新DOM,然后执行第二行代码?

1
2
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

要解决此问题,你可以强制 React 同步更新(“刷新”)DOM。 为此,从 react-dom 导入 flushSync将 state 更新包裹flushSync 调用中:

1
2
3
4
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

使用Effect同步

Effect 允许你指定由渲染本身,而不是特定事件引起的副作用。Effect运行的时机是在组件提交之后进行的。

使用:

1
2
3
4
5
6
7
8
import { useEffect } from 'react';

function MyComponent() {
useEffect(() => {
// 每次渲染后都会执行此处的代码
});
return <div />;
}

Effect 会在 每次 渲染后执行,如果在useEffect调用setState函数,程序将会陷入死循环。

useEffect第二个参数接受一个数组,如果数组中的值和上一次渲染的时候相同,则跳过本次执行。

1
2
3
4
5
6
7
8
9
10
11
useEffect(() => {
// 这里的代码会在每次渲染后执行
});

useEffect(() => {
// 这里的代码只会在组件挂载后执行,即首次出现在屏幕上这一阶段。
}, []);

useEffect(() => {
//这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);

在开发环境中,React 会在初始挂载组件后,立即再挂载一次。

useEffect可以返回一个**清理函数**,每次重新执行 Effect 之前,React 都会调用清理函数;组件被卸载时,也会调用清理函数。

1
2
3
4
5
6
7
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);

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

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

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

How to fetch data with React Hooks

不是什么时候都需要使用useEffect。

  • 根据 props 或 state 来更新 state。
1
2
3
4
5
6
7
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ 非常好:在渲染期间进行计算
const fullName = firstName + ' ' + lastName;
// ...
}
  • 缓存耗时计算。
1
2
3
4
5
6
7
8
9
10
import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ 除非 todos 或 filter 发生变化,否则不会重新执行
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}

将事件从Effect中分开

  • Effect内部的逻辑是响应式的。
  • 事件处理函数内部的逻辑是非响应式的。

要从Effect中提取出非响应式的逻辑可以通过Effect Event(非正式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 声明所有依赖项
// ...

自定义HOOK

  1. React 组件名称必须以大写字母开头,比如 StatusBar SaveButton。React 组件还需要返回一些 React 能够显示的内容,比如一段 JSX。
  2. Hook 的名称必须以 **use** 开头,然后紧跟一个大写字母。Hook 可以返回任意值。

一些Hook

memo

memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。

React 通常在其父组件重新渲染时重新渲染一个组件。你可以使用 memo 创建一个组件,当它的父组件重新渲染时,只要它的新 props 与旧 props 相同时,React 就不会重新渲染它。这样的组件被称为 记忆化的(memoized)组件。

1
2
3
4
5
6
7
import { memo } from 'react';

const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

lazy

在组件外部调用 lazy,以声明一个懒加载的 React 组件:

1
2
3
4
5
6
7
8
import { lazy } from 'react';

const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));

<Suspense fallback={<Loading />}>
<h2>Preview</h2>
<MarkdownPreview />
</Suspense>

useMemo

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function TodoList({ todos, tab, theme }) {
// 告诉 React 在重新渲染之间缓存你的计算结果...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...所以只要这些依赖项不变...
);
return (
<div className={theme}>
{/* ... List 也就会接受到相同的 props 并且会跳过重新渲染 */}
<List items={visibleTodos} />
</div>
);
}

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function ProductPage({ productId, referrer, theme }) {
// 在多次渲染中缓存函数
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // 只要这些依赖没有改变

return (
<div className={theme}>
{/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}