参考React19文档
React组件构建思路
- 将 UI 拆解为组件层级结构
- 使用 React 构建一个静态版本
- 找出 UI 精简且完整的 state 表示
- 验证 state 应该被放置在哪里
- 添加反向数据流
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
| 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是隔离且私有的
index和setIndex这种起名方式是约定俗成的
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渲染为:
- 触发渲染
- React渲染组件
- React更新DOM
state在一轮渲染内只会保存一种状态,相当于一轮渲染为一个快照,所以在一次事件中多次调用setIndex函数,只会有一个相同的输入。
想要多次修改state的值,可以给setState函数传入一个更新函数,每次调用setState会将更新函数加入队列。在下一次渲染时,会遍历执行队列中的函数,并更新最终的state。
例如
1 2 3 4 5 6 7 8 9 10 11
| const [number, setNumber] = useState(0);
setNumber(number + 1); setNumber(number + 1);
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],filter,map等不修改数组本身方法新建一个数组,然后再调用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、触发状态更新的函数
使用:
- 定义
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; } }
|
- 初始化状态:确定组件的初始状态。
1
| const initialState = { count: 0 };
|
- 在组件中调用
useReducer:传入定义好的 reducer 和初始状态
1
| const [state, dispatch] = useReducer(reducer, initialState);
|
- 派发
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]);
|
使用场景:
- 数据获取
- 事件监听
- 定时器
- 手动操作 DOM
4.1.3 useContext
作用:解决组件树中跨层级传递数据
使用:
- 创建上下文
1 2 3 4 5 6 7 8 9
|
import React from 'react';
const MyContext = React.createContext({ name: 'Guest', age: 0 });
export default MyContext;
|
- 提供数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
import React, { useState } from 'react'; import MyContext from './MyContext'; import ChildComponent from './ChildComponent';
function App() { const [user, setUser] = useState({ name: 'John', age: 30 });
return ( <MyContext.Provider value={{ user, setUser }}> <div> <ChildComponent /> </div> </MyContext.Provider> ); }
|
- 获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
import React, { useContext } from 'react'; import MyContext from './MyContext';
function ChildComponent() { 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);
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('');
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.current 是 null。当 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) { 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); setError(null); try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } };
fetchData(); }, [url]);
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';
function UserList() { const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');
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;
|