参考React19文档

React组件构建思路

  1. 将 UI 拆解为组件层级结构
  2. 使用 React 构建一个静态版本
  3. 找出 UI 精简且完整的 state 表示
  4. 验证 state 应该被放置在哪里
  5. 添加反向数据流

1.组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//标准组件格式
export function Profile() {
return (
<img
src="https://i.imgur.com/QIrZWGIs.jpg"
alt="Alan L. Hart"
/>
);
}

export default function Gallery() {
return (
<section>
<h1>了不起的科学家们</h1>
<Profile />
<Profile />
</section>
);
}
//导入组件
import Gallery from './Gallery.js';
import { Profile } from './Gallery.js';

1.1组件props传值

1
2
3
4
5
6
7
8
9
10
11
12
13
//标准解构props传值
function Avatar({ size }) {
return (
<img width={size} height={size}/>
);
}
export default function Profile() {
return (
<div>
<Avatar size={100}/>
</div>
);
}

可以用...props展开语法传递所有props

1
2
3
4
5
6
7
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}

1.2组件children传递

与插槽类似,写在组件标签内的内容,会通过props.children传递

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

export default function Profile() {
return (
<Card>
<Avatarsize={100}/>
</Card>
);
}

条件渲染与列表渲染与前文旧版react变化不大,此处省略

1.3 内置组件

1.3.1 React.memo

高阶组件,通过浅比较props是否变化,来决定是否跳过组件重新渲染,适用于重渲染父组件而跳过子组件的情况

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

const ExpensiveChild = memo(({ data }) => {
console.log('ExpensiveChild 渲染了');
return <div>{data}</div>;
});

function Parent() {
const [count, setCount] = useState(0);
const stableData = "这是一个稳定的值";

return (
<div>
<button onClick={() => setCount(c => c + 1)}>点击我 {count}</button>
{/* 即使父组件因 count 变化而重新渲染,ExpensiveChild 不会重新渲染 */}
<ExpensiveChild data={stableData} />
</div>
);
}

2.处理事件

事件可以通过props传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Button({ onSmash, children }) {
return (
<button onClick={onSmash}>
{children}
</button>
);
}

export default function App() {
return (
<div>
<Button onSmash={() => alert('正在播放!')}>
播放电影
</Button>
</div>
);
}

必要时需要使用

  • e.stopPropagation()阻止事件冒泡传播;
  • e.preventDefault()阻止事件默认行为;

3.状态

使用useStateHook函数声明状态,state是隔离且私有的

indexsetIndex这种起名方式是约定俗成的

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

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

function handleClick() {
setIndex(index + 1);
}

return (
<>
<button onClick={handleClick}>
Next
</button>
<div>
{index}
</div>
</>
);
}

首次加载和调用setIndex函数会触发一轮React渲染;

一轮React渲染为:

  1. 触发渲染
  2. React渲染组件
  3. React更新DOM

state在一轮渲染内只会保存一种状态,相当于一轮渲染为一个快照,所以在一次事件中多次调用setIndex函数,只会有一个相同的输入。

想要多次修改state的值,可以给setState函数传入一个更新函数,每次调用setState会将更新函数加入队列。在下一次渲染时,会遍历执行队列中的函数,并更新最终的state。

例如

1
2
3
4
5
6
7
8
9
10
11
const [number, setNumber] = useState(0);

//下边两次调用,number只会加1,因为传给setNumber函数的是两次 0 + 1
//实际上setNumber(42)这种类型也会加入任务队列,只不过该项任务的逻辑是将number置成42
setNumber(number + 1);
setNumber(number + 1);

//下边两次调用,number会加2,因为队列中有两个更新函数,依次处理后n变成了2
setNumber(n => n + 1);
setNumber(n => n + 1);

3.1修改state中对象/数组

始终使用setState函数来更新对象

1
2
3
4
5
6
7
8
9
10
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]'
});

setPerson({
...person,
firstName: e.target.value
});

修改数组时使用展开语法[...arr]filtermap等不修改数组本身方法新建一个数组,然后再调用setState函数来更新。

4.状态管理

4.1常用内置Hook

先介绍一个常用的内置Hook函数

4.1.1 useReducer

作用:管理复杂的组件状态。例如状态的修改要区分不同的情况。

基本语法:const [state, dispatch] = useReducer(reducer, initialState, initFunction);

  • ​reducer​​: 一个纯函数,接收当前状态 state和描述操作的 action对象,根据 action.type决定如何计算并返回​​新状态​
  • ​initialState​​: 状态的初始值
  • initFunction​​ (可选): 用于​​延迟计算初始状态​​的函数。如果提供,初始状态将是 initFunction(initialState)的结果
  • state: 当前的状态
  • dispatch: 用于派发 action、触发状态更新的函数

使用:

  1. 定义reducer函数:纯函数,接收当前状态和 action,根据 action 的类型返回新的状态
1
2
3
4
5
6
7
8
9
10
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state; // 务必处理未知 action 类型
}
}
  1. 初始化状态​:确定组件的初始状态。
1
const initialState = { count: 0 };
  1. 在组件中调用useReducer:传入定义好的 reducer 和初始状态
1
const [state, dispatch] = useReducer(reducer, initialState);
  1. 派发action更新状态​:通过调用 dispatch函数并传入一个描述“发生了什么”的 action 对象来请求状态更新。action 通常有一个 type字段(表示操作类型)和可选的 payload字段(携带数据)
1
const handleIncrement = () => { dispatch({ type: 'increment' }); };

4.1.2 useEffect

作用:用来处理​ 那些​​不直接参与UI渲染​​,但又必须进行的​​额外操作

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//副作用函数​:包含需要执行的副作用逻辑。此函数可以返回一个​清理函数​,用于在组件卸载或下一次副作用执行前进行清理(如取消订阅、清除定时器)

/*
依赖数组(可选)​:指定副作用函数依赖的状态或属性值。根据依赖项决定是否重新执行副作用函数
依赖数组不传:每次组件渲染后都执行
依赖数组传空数组:仅在组件首次挂载时执行一次
依赖数组正常传值:组件首次挂载时执行,且依赖项变化时重新执行
*/
import { useEffect } from 'react';
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)在组件卸载或dep变化引起的下一次副作用执行前触发
};
}, [dep]); // 依赖数组

使用场景:

  1. 数据获取
  2. 事件监听
  3. 定时器
  4. 手动操作 DOM

4.1.3 useContext

作用:解决组件树中​跨层级传递数据

使用:

  1. ​创建上下文
1
2
3
4
5
6
7
8
9
//使用 `React.createContext`创建一个上下文对象.

// MyContext.js
import React from 'react';

// 创建一个上下文对象,并提供一个默认值(当组件不在 Provider 包裹下时使用)
const MyContext = React.createContext({ name: 'Guest', age: 0 });

export default MyContext;
  1. 提供数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//在组件树中合适的位置(通常是顶层或父组件),使用 `<MyContext.Provider>`组件包裹需要接收数据的子组件,并通过其 `value`属性传递数据

// App.js
import React, { useState } from 'react';
import MyContext from './MyContext';
import ChildComponent from './ChildComponent';

function App() {
const [user, setUser] = useState({ name: 'John', age: 30 });

return (
// 使用 Provider 提供数据。value 可以是任何类型:值、对象、函数等。
<MyContext.Provider value={{ user, setUser }}>
<div>
<ChildComponent />
</div>
</MyContext.Provider>
);
}
  1. 获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//在需要访问上下文数据的子组件(无论层级多深)中,使用 useContext 来获取上下文的值

// ChildComponent.js
import React, { useContext } from 'react';
import MyContext from './MyContext';

function ChildComponent() {
// 使用 useContext 获取上下文的值
const { user, setUser } = useContext(MyContext);

return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={() => setUser({ ...user, age: user.age + 1 })}>
Increase Age
</button>
</div>
);
}

4.1.4 useCallback(函数记忆)

  • 用于缓存函数本身的引用
  • 接收一个内联回调函数和依赖项数组,只有依赖项变化时,才会返回一个新的函数实例,
  • 适用于将回调函数传递被React.memo优化过的子组件时(或作为其他Hook如useEffect的依赖项时),避免因函数引用变化导致的不必要重渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState, useCallback, memo } from 'react';

const ChildButton = memo(({ onClick }) => {
console.log('ChildButton 渲染了');
return <button onClick={onClick}>点击我</button>;
});

function Parent() {
const [count, setCount] = useState(0);

// 使用 useCallback 缓存函数,空依赖数组表示该函数永不改变
const handleClick = useCallback(() => {
console.log('按钮被点击');
}, []);

return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
{/* 父组件重新渲染时,handleClick 的引用不变,ChildButton 不会重新渲染 */}
<ChildButton onClick={handleClick} />
</div>
);
}

4.1.5 useMemo(值记忆)

  • 用于缓存计算成本较高的函数结果
  • 接收函数和依赖项数组,只有当依赖项发生变化时,才会重新调用函数来计算结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useMemo, useState } from 'react';

function MyComponent({ list }) {
const [filter, setFilter] = useState('');

// 仅当 `list` 或 `filter` 变化时,才会重新执行昂贵的过滤计算
const filteredList = useMemo(() => {
console.log('正在进行昂贵的计算...');
return list.filter(item => item.name.includes(filter));
}, [list, filter]); // 依赖项

return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<ul>
{filteredList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}

4.2状态提升/在组件间共享内容

这部分与前文8.1 状态提升相似,不在赘述

5.ref

useRef Hook 返回一个对象,该对象有一个名为 current 的属性。最初,myRef.currentnull。当 React 为这个 <div> 创建一个 DOM 节点时,React 会把对该节点的引用放入 myRef.current

使用方式:

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

const myRef = useRef(null);

<input ref={inputRef} />

inputRef.current.focus();

ref也可以通过props传递

1
2
3
4
5
6
7
8
9
10
import { useRef } from 'react';

function MyInput({ ref }) {
  return <input ref={ref} />;
}

function MyForm() {
  const inputRef = useRef(null);
  return <MyInput ref={inputRef} />
}

动态列表绑定ref:使用ref回调

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
27
const itemsRef = useRef(null);
<ul>
  {catList.map((cat) => (
    <li
      key={cat}
      ref={(node) => {
        const map = getMap();
        map.set(cat, node);
       
        //此处返回清理函数是React19新特性,React18需要在useEffect中清理
        return () => {
          map.delete(cat);
        };
      }}
    >
      <img src={cat} />
    </li>
  ))}
</ul>

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

5.1React强制更新DOM

1
2
3
4
5
6
7
import { flushSync } from 'react-dom';

flushSync(() => {
setTodos([ ...todos, newTodo]);
});

listRef.current.lastChild.scrollIntoView();

6.自定义Hook

  • Hook名字以use开头
  • 自定义Hook共享的是状态逻辑(数据处理过程),而不是状态(数据)本身。
  • 只能在函数组件或其他自定义Hook的顶层调用Hook,不能在普通函数或逻辑语句中调用。
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
27
28
29
30
31
32
33
import { useState, useEffect } from 'react';

function useFetch(url) {
// 定义状态来存储数据、加载状态和错误信息
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
// 定义一个异步函数来获取数据
const fetchData = async () => {
setLoading(true); // 开始请求时设置 loading 为 true
setError(null); // 重置错误状态
try {
const response = await fetch(url);
if (!response.ok) { // 如果 HTTP 状态码不在 200-299 范围内,抛出错误
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result); // 请求成功,设置数据
} catch (err) {
setError(err.message); // 请求失败,设置错误信息
} finally {
setLoading(false); // 无论成功与否,请求结束,设置 loading 为 false
}
};

fetchData(); // 执行异步函数
}, [url]); // 依赖数组:当 url 变化时,effect 会重新执行

// 返回状态和函数,供组件使用
return { data, loading, error };
}

在接口中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import useFetch from './useFetch'; // 导入自定义 Hook

function UserList() {
// 使用 useFetch Hook,传入 API URL
const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');

// 根据状态渲染不同的 UI
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
if (!users || users.length === 0) return <div>No users found.</div>;

return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}

export default UserList;