- 组件名必须大写开头
- 组件应该在顶层定义。不要在组件函数里再定义其他组件。
- 标签必须闭合,例如使用
<br/>
而非<br>
- 组件返回只能返回一个标签,如需要多个可以包裹到一个空的父级里,如
<>...</>
(但只有其完整形式<Fragment/>
可以接收key
属性):
import { Fragment } from 'react';
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
组件渲染流程
- 组件初次渲染,或组件或其祖先的状态发生了改变,触发组件渲染
- 组件渲染:执行组件函数(不包括Hook),并在虚拟DOM树中进行diff运算
注意,组件函数必须都是没有副作用的纯函数。 - 浏览器绘制(DOM渲染):此时DOM才真正更新
-
useEffect
回调触发
组件渲染的逻辑
每次组件渲染会重新执行组件函数,但不包括其中的 Hook 内容
纯函数
组件应当都是纯函数,只负责自己的任务,不改变外部变量,且相同的输入总得到相同的输入。以此防止组件每次渲染时造成副作用。
Hook 内容不会在组件再次渲染时重复执行
父组件传入的 props 如通过useState
镜像,则父组件稍后传递不同的props时,该state不会更新:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
}
props
- 父组件可以通过 prop 传递数据、方法给子组件。
- 子组件通过解构赋值来接收父组件传递进来的数据和方法。
- 组件定义时本身嵌套的JSX内容会作为该组件的
children
prop(作用类似Vue的slot):
function Card({children}) {
return <div>hello {children}</div>
}
export default function Profile() {
return <Card><b>world</b></Card>
}
//最后会渲染成<div>hello <b>world</b></div>
- 将本该子组件使用的state提升到父组件,再由父组件将state、setState通过 props 传入子组件供调用,这种操作称为
状态提升
。
function MyApp() {
function speak(name) {
console.log("name", name);
}
return (
<>
<Child name="VV" speak={(name) => speak(name)} />
</>
);
}
function Child({ name, speak }) {
return (
<>
<div>我的名字是{name}</div>
<button onClick={() => speak(name)}>说话</button>
</>
);
}
- 组件也可以将自己从父组件获得的props再传递给自己的子组件
可以通过扩展运算符全部传递:
export default function Father(props) {
return (
<Child
{...props}
/>
);
}
function Child({ savedContact, onSave }) {
...
}
事件
- 原生事件名在JSX中需要改为驼峰命名,如
onClick
。需要在捕获阶段触发的事件在最后额外加上Capture
,如onClickCapture
。 - 事件处理程序通常命名为 handle 接事件名,例如:
onClick={handleClick}
- 当需要指定函数入参时,可以使用箭头函数:
onClick={() => handleClick(100)}
- 父组件可以把事件通过 props 传递给子组件,通常在 props 中的命名为
on
开头的大驼峰 - 通过原生的
e.stopPropagation()
阻止冒泡,e.preventDefault()
阻止默认行为。
Hook 与状态管理
以 use 开头的函数被称为 Hook,用于管理状态。
- Hook 只能在组件或其他 Hook(包括自定义Hook)的顶层调用,且不能出现在条件语句或循环中。
Hook 不能被包裹在非组件且非 Hook 的普通自定义函数内。 - 可以将一些组件的公用逻辑抽出来,作为自定义 Hook。自定义 Hook 共享的是状态逻辑,而不是状态本身(即被不同组件引用时,内容不互通)。
useState
const [state, setState] = useState(初始值)
- state 状态变量,独立于组件保存,组件多次渲染不会重定义该值。
- setState 函数,用于更新变量并触发再次渲染
函数入参可以是一个更新函数(必须为纯函数,只用于计算下一个状态。多个更新函数会在渲染期间依次调用),或一个固定值(本质上是更新函数返回固定值的简写)
注意,state在每次渲染中是固定的(快照),setState不会改变当前渲染中的state,只会改变下一次渲染的值!
如想要在当前渲染中就改变,可使用普通变量或useRef
。
一个事件处理函数会全部执行完再进行渲染,称为批处理。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(10);
setNumber(number + 1);
setNumber(number + 1);
setNumber(n=>n + 1);
setNumber(n=>n + 1);
console.log(number);//0
}}>点击后变成3</button>
</>
)
}
使用flushSync
函数同步更新DOM
传入flushSync
执行的setState
操作会提前触发DOM渲染(但快照中的state保持不变),而不会等待批处理完成:
const [number, setNumber] = useState(0);
return (
<>
<h1 id="h1">{number}</h1>
<button onClick={() => {
flushSync(() => {
setNumber(1);
console.log(number);//0
console.log(document.getElementById("h1").innerHTML);//0
});
console.log(number);//0
console.log(document.getElementById("h1").innerHTML);//1
}}>点我</button>
</>
);
state 不保存在 JSX 标签里
state 与树中放置该 JSX 的位置相关联。因此在渲染树中相同位置的相同组件,其内部状态会得到保留:
//以下<Counter/>组件内部的state,在 isFancy 切换时会得到保留
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
//以下<Counter/>组件内部的state,在 isPlayerA 切换时会重置(因位置不同)
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
重置其内部状态的几个方式:
- 渲染树中不同位置
相同渲染树需要:上级组件相同,且前置兄弟组件数量相同(<></>
是一个组件,DOM元素也视为一种组件,一段JSX{}
也算一种组件) - 不同组件
- 销毁该组件后不立刻重新渲染该组件
- 使用不同 key
突变 mutation
通过 setState 以外的方式造成的 state 数据变化称为突变(例如通过原生JS直接修改对象、数组的某个成员)。突变的结果会在下次渲染中显示,但突变本身不会触发渲染。
应当避免突变的发生。
useReducer
当需要在多处分别对同一state
做出不同修改时,可以通过useReducer
代替useState
进行统一管理。
-
useReducer
可传入两个入参,第一个是reducer
函数,第二个是state
初始值。 -
reducer
函数必须为纯函数,只用于计算下一个状态。(严格模式下会调用两次)。该函数的返回值用于更新状态。 - 返回的
dispatch
函数用来 “派发” 用户操作给reducer
函数,调用时传入一个自定义参数(通常约定会有 type 属性),传给reducer
函数的第二个参数:
import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function tasksReducer(tasks, action) {
switch (action.type) {
case 1:
return{
...tasks,
age: action.age
}
default: {
throw Error('未知 action: ' + action.type);
}
}
}
useRef(脱围)
类似useState
,返回一个ref
实例,用于在组件渲染间保留信息。
不要在渲染期间(即组件主体)读取或写入ref
,应该移到事件处理程序或者 Effect 中。
-
ref
实例是一个具有current
属性的对象,其更新不会触发组件渲染,也不是个快照,可以直接对current
属性赋值以修改信息。 -
ref
实例通常用于储存 timeout ID、DOM元素、或其他不需要在JSX中渲染计算的内容:
import { useRef } from 'react';
const ref = useRef(0);
ref.current = ref.current + 1;//1
- JSX中DOM节点的
ref
属性,可直接赋值为ref
实例以绑定DOM元素,也可以传入一个方法,在方法内手动绑定DOM元素。
当传入方法时,每当组件重新渲染,先前的函数将被调用并传递 null 作为参数,并且下一个函数将被调用并传递对应 DOM 节点作为参数:
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
const divRefs = useRef(new Map());
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>
聚焦输入框
</button>
{
[0, 1, 2, 3, 4, 5].map(i => (
<div key={i} ref={node => {
if(node){
divRefs.current.set(i, node)
}else{
divRefs.current.delete(i)
}
}}
onClick={() => {
divRefs.current.get(i).style.color = "red";
}}>
我是第{i}个,点我会变红
</div>
))
}
</>
);
}
上述方法默认只能用于浏览器原生元素,当用于JSX中的自定义组件时,该组件需要由forwardRef
方法创建(在此方法中还可以通过useImperativeHandle
限制暴露的内容):
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
return (
<>
<MyInput ref={inputRef} />
</>
);
}
useEffect(脱围,由渲染引起的副作用,与React之外的系统同步)
- 处理 Effect 的函数(必传)
- 该函数在严格模式+开发环境下会调用两次。
- 该函数可以返回一个清理函数,会在组件卸载,或
Effect
再次执行前调用。
注意清理函数在Effect函数内是个闭包,因此调用时起内容只影响其声明时的Effect内变量:
//此处返回值内的ignore可以让同一次调用中的fetch回调失效,但不影响下一次的Effect
//可用于防止竞态条件,保证是最后一次触发的生效
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
}
}, [person]);
- 依赖数组(非必传)
- 若不传,则会在组件每次渲染并调用触发DOM渲染后调用函数。
- 若传入依赖数组(可传入
state
、props
、组件体内声明的利用前两者计算得出的其他响应式变量),且依赖内容均和上次渲染时相同,则跳过本次Effect函数执行。
传入依赖数组后,函数只能使用依赖数组内包含的响应式变量。- 类似
location.pathname
这样的外部可变值不是响应式变量,应改用useSyncExternalStore
来读取和订阅。 - 当前组件的
ref
不是响应式变量,因为其是有意可变的、稳定的。其变化不会触发重新渲染 -
useState
创建的setState
方法不是响应式变量
- 类似
import { useEffect } from 'react';
useEffect(() => {
// 这里的代码会在每次渲染后执行
});
useEffect(() => {
// 这里的代码只会在组件挂载后执行
}, []);
useEffect(() => {
//这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);
注意:组件内声明的对象、数组、函数等(包括父组件传递下来的props中的以上内容),因为每次渲染都不全等,如果传递给useEffect
作为依赖则会导致每次渲染都触发useEffect
。此时需要使用useMemo
或 useCallback
,或将该声明移动到组件外或Effect
内。
而当某个响应式变量既需要参与useEffect
内容,又不想因其变化而导致触发Effect
,则可以使用useEffectEvent
。
useMemo 和 useCallback
useMemo 返回值,useCallback 返回函数。两者均用于避免组件每次渲染时的重复计算。
- 需传入依赖数组,若依赖内容均和上次渲染时相同,则返回和上一次全等的结果。
- 会在JSX渲染环节(即
DOM
更新前)生效,因此都必须是纯函数。
import { useMemo, useState, useCallback } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ 除非 todos 或 filter 发生变化,否则不会重新执行 getFilteredTodos()
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
useEffectEvent
用于提取Effect中部分逻辑,以实现如下功能:存在某个响应式变量,既要在Effect中使用,又不想充当Effect的依赖项。
相比于useCallback
的区别:
- 不用显式声明依赖
- 即使依赖变了,fn的引用也不变(与原来全等)
- 只能在
useEffect
内部使用,且不需要加入useEffect
依赖
//在这里,onVisit 内的 url 对应 最新的 url(可能已经变化了),但是 visitedUrl 对应的是最开始引起这个 Effect(并且是本次 onVisit 调用)运行的 url 。
import { experimental_useEffectEvent as useEffectEvent } from 'react';
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // 延迟记录访问
}, [url]);
使用 Immer 库简化 useState 和 useReducer
Immer 库可代替useState
和 useReducer
,以避免突变,并简化对象、数组的更新操作。
npm install use-immer
useImmer
提供的修改方法,其参数同样可以是一个更新函数或一个固定值。
- 为固定值时效果同
useState
- 为更新函数时(通常以
update
开头命名),其参数(通常命名为draft
)是一个Proxy而不是原始的state(类似 Vue3 实现了数据劫持),因此可以直接修改其中属性:
import { useImmer } from 'use-immer';
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
});
updatePerson(draft => {
draft.name = e.target.value;
});
useImmerReducer
可以返回,也可以直接修改 draft
对象属性。
注意switch
中如果不使用return
,要使用break
分隔case
:
import { useImmerReducer } from 'use-immer';
const [tasks, dispatch] = useImmerReducer(tasksReducer, [{id:1},{id:2},{id:3}]);
function tasksReducer(draft, action) {
switch (action.type) {
case 'changed': {
const index = draft.findIndex((t) => t.id === action.task.id);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action:' + action.type);
}
}
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
使用 Context 深层传递参数
用于祖先元素向其深层后代传递信息,以代替 props 逐级下传
// 单独的文件 MyContext.js
import { createContext } from 'react';
export const MyContext = createContext("默认值")
// 父组件提供Context,Context.Provider的后代元素都可以获得距离最近的值
import { MyContext } from './Context.js';
export default function App() {
return (
<>
<MyContext.Provider value={100}>
<List/>
</MyContext.Provider>
</>
)
}
// 子组件获取Context
import { useContext } from 'react';
import { MyContext } from './MyContext.js';
function List() {
const data = useContext(MyContext)
return <div>{data}</div>
}