React全家桶笔记(九):React扩展与Router 6
本篇是系列最后一篇,涵盖 React 高级扩展知识(Hooks、性能优化、错误边界等)以及 React Router 6 的全新 API。 📺 对应张天禹react全家桶视频:P115 - P141
一、setState 的两种写法(P116)
1.1 对象式写法(常用)
this.setState({ count: this.state.count + 1 })
1.2 函数式写法
this.setState((state, props) => {
return { count: state.count + 1 }
})
1.3 setState 的回调函数
setState 是异步更新的,如果需要在状态更新后执行操作,使用第二个参数(回调函数):
this.setState({ count: this.state.count + 1 }, () => {
// 这里能拿到更新后的 state
console.log('更新后的count:', this.state.count)
})
总结:
- 对象式是函数式的语法糖
- 如果新状态依赖于原状态 → 推荐函数式
- 如果需要在 setState 后获取最新状态 → 使用回调函数
🎯 面试高频:setState 是同步还是异步? 在 React 控制的事件处理函数和生命周期中,setState 表现为"异步"(批量更新)。在 setTimeout、原生 DOM 事件中,setState 表现为"同步"。React 18 之后,所有场景默认都是批量更新(自动批处理)。
二、路由懒加载 LazyLoad(P117)
import React, { Component, lazy, Suspense } from 'react'
import { Route, Switch, Redirect } from 'react-router-dom'
// 懒加载路由组件(不再使用 import Xxx from './pages/Xxx')
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
export default class App extends Component {
render() {
return (
<div>
<div className="nav">
<NavLink to="/home">Home</NavLink>
<NavLink to="/about">About</NavLink>
</div>
{/* Suspense 指定加载中的 fallback UI */}
<Suspense fallback={<h1>Loading...</h1>}>
<Switch>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
<Redirect to="/home" />
</Switch>
</Suspense>
</div>
)
}
}
要点:
lazy()接收一个函数,函数中动态import()组件- 必须配合
<Suspense>使用,指定加载中的 fallback UI - 懒加载的组件会被单独打包成一个 chunk,按需加载
🔗 概念扩展:代码分割(Code Splitting) 默认情况下,打包工具会把所有代码打成一个大文件。懒加载利用动态 import 实现代码分割,将不同路由的代码拆分成独立的文件,用户访问某个路由时才加载对应的代码,减少首屏加载时间。
三、React Hooks(P118-P120)
Hooks 是 React 16.8 引入的新特性,让函数式组件也能使用 state 和其他 React 特性。
3.1 State Hook — useState(P118)
import React, { useState } from 'react'
function Demo() {
// useState 返回一个数组:[当前状态值, 更新状态的函数]
// 参数是状态的初始值
const [count, setCount] = useState(0)
const [name, setName] = useState('Tom')
function add() {
// 写法1:直接传新值
setCount(count + 1)
// 写法2:传函数(推荐,当新值依赖旧值时)
setCount(count => count + 1)
}
return (
<div>
<h2>当前求和为:{count}</h2>
<h2>我的名字是:{name}</h2>
<button onClick={add}>点我+1</button>
</div>
)
}
useState 要点:
- 参数:初始状态值(只在第一次渲染时生效)
- 返回值:
[状态值, 更新函数](数组解构) - setXxx 的两种写法:直接传值 / 传函数
- 每次调用 setXxx 都会触发组件重新渲染
3.2 Effect Hook — useEffect(P119)
import React, { useState, useEffect } from 'react'
function Demo() {
const [count, setCount] = useState(0)
// useEffect 相当于 componentDidMount + componentDidUpdate + componentWillUnmount
useEffect(() => {
// 这个回调在组件挂载和更新时都会执行
console.log('组件挂载/更新了')
// 返回的函数相当于 componentWillUnmount
return () => {
console.log('组件将要卸载 / 下次 effect 执行前的清理')
}
}, [count])
// 第二个参数是依赖数组:
// 不传 → 每次渲染都执行(componentDidMount + 每次 componentDidUpdate)
// 传 [] → 只在挂载时执行一次(componentDidMount)
// 传 [count] → 挂载时 + count 变化时执行
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={() => setCount(count + 1)}>点我+1</button>
</div>
)
}
useEffect 依赖数组对照表:
useEffect(() => {...}) → 挂载 + 每次更新都执行
useEffect(() => {...}, []) → 只在挂载时执行一次(≈ componentDidMount)
useEffect(() => {...}, [a, b]) → 挂载 + a 或 b 变化时执行
useEffect(() => { return () => {...} }, []) → 卸载时执行清理(≈ componentWillUnmount)
🎯 面试高频:useEffect 的依赖数组有什么作用? 依赖数组决定了 effect 何时重新执行。空数组表示只执行一次(挂载时),有值则在对应值变化时重新执行。不传则每次渲染都执行。返回的清理函数在组件卸载或下次 effect 执行前调用。
3.3 Ref Hook — useRef(P120)
import React, { useRef } from 'react'
function Demo() {
const myRef = useRef()
function show() {
alert(myRef.current.value)
}
return (
<div>
<input type="text" ref={myRef} />
<button onClick={show}>点击提示数据</button>
</div>
)
}
useRef 的功能与 React.createRef() 一样,返回一个可变的 ref 对象,通过 .current 访问 DOM 节点。
3.4 Hooks 使用规则
Hooks 规则:
├── 只能在函数组件的最顶层调用(不能在循环、条件、嵌套函数中调用)
├── 只能在 React 函数组件或自定义 Hook 中调用
└── 自定义 Hook 必须以 "use" 开头命名
四、Fragment(P121)
import React, { Fragment } from 'react'
// 问题:组件必须有一个根标签,但有时不想多一层无意义的 div
// 方案1:Fragment — 编译后会被丢弃,不会渲染到 DOM 中
function Demo() {
return (
<Fragment>
<input type="text" />
<input type="text" />
</Fragment>
)
}
// 方案2:空标签(更简洁,但不能传 key 属性)
function Demo() {
return (
<>
<input type="text" />
<input type="text" />
</>
)
}
Fragment vs 空标签:
<Fragment>可以传key属性(遍历时使用)<></>不能传任何属性,但写法更简洁
五、Context(P122)
Context 用于跨层级组件通信(祖孙组件),不需要逐层传递 props。
import React, { Component, createContext } from 'react'
// 1. 创建 Context 对象
const MyContext = createContext()
const { Provider, Consumer } = MyContext
// 祖组件
export default class A extends Component {
state = { username: 'Tom', age: 18 }
render() {
const { username, age } = this.state
return (
<div>
<h3>我是A组件,用户名:{username}</h3>
{/* 2. 用 Provider 包裹子组件,通过 value 传递数据 */}
<Provider value={{ username, age }}>
<B />
</Provider>
</div>
)
}
}
// 子组件(中间层,不需要使用数据)
class B extends Component {
render() {
return (
<div>
<h3>我是B组件</h3>
<C />
</div>
)
}
}
// 孙组件 — 类式组件接收方式
class C extends Component {
static contextType = MyContext // 3. 声明接收
render() {
const { username, age } = this.context // 4. 通过 this.context 获取
return <h3>我是C组件,从A接收到:{username},年龄{age}</h3>
}
}
// 孙组件 — 函数式组件接收方式
function C() {
return (
<div>
<Consumer>
{value => <h3>从A接收到:{value.username},年龄{value.age}</h3>}
</Consumer>
</div>
)
}
💡 Context 在实际开发中用得不多,一般用 Redux 代替。但理解 Context 很重要,因为 react-redux 的 Provider 底层就是用 Context 实现的。
六、PureComponent 组件优化(P123)
6.1 问题
React 中 shouldComponentUpdate 默认返回 true,即使 state/props 没有变化,父组件重新 render 时子组件也会跟着重新 render,造成不必要的渲染。
6.2 解决方案:PureComponent
import React, { PureComponent } from 'react'
// PureComponent 会自动进行 state 和 props 的浅比较
// 如果没有变化,就不会重新 render
export default class Demo extends PureComponent {
state = { carName: '奔驰' }
changeCar = () => {
// ❌ 错误!对象引用没变,PureComponent 检测不到变化
// const obj = this.state
// obj.carName = '宝马'
// this.setState(obj)
// ✅ 正确!产生新对象
this.setState({ carName: '宝马' })
}
render() {
return <h2>{this.state.carName}</h2>
}
}
PureComponent 要点:
- 自动实现了
shouldComponentUpdate,进行 state 和 props 的浅比较 - 浅比较:只比较第一层,不会深层递归比较
- 所以必须产生新的对象/数组,不能直接修改原数据
🎯 面试高频:Component 和 PureComponent 的区别? PureComponent 自动实现了 shouldComponentUpdate,通过浅比较 props 和 state 来决定是否重新渲染。Component 默认每次都重新渲染。使用 PureComponent 时要注意不可变数据的原则。
七、Render Props(P124)
类似 Vue 的插槽(slot),让组件的内容更加灵活。
// A 组件通过 render prop 接收一个函数,决定渲染什么
export default class Parent extends Component {
render() {
return (
<div>
<h3>我是Parent组件</h3>
<A render={(name) => <B name={name} />} />
</div>
)
}
}
class A extends Component {
state = { name: 'Tom' }
render() {
const { name } = this.state
return (
<div>
<h3>我是A组件</h3>
{/* 调用 render prop,把自己的状态传出去 */}
{this.props.render(name)}
</div>
)
}
}
class B extends Component {
render() {
return <h3>我是B组件,接收到:{this.props.name}</h3>
}
}
Render Props vs children props:
// children props 方式(类似 Vue 默认插槽)
<A>
<B />
</A>
// A 组件中:{this.props.children}
// 问题:B 组件无法获取 A 组件的状态
// render props 方式(类似 Vue 作用域插槽)
<A render={(data) => <B data={data} />} />
// A 组件中:{this.props.render(this.state.data)}
// 优势:B 组件可以获取 A 组件的状态
八、Error Boundary 错误边界(P125)
错误边界用于捕获后代组件的错误,渲染出备用 UI,而不是让整个应用崩溃。
export default class Parent extends Component {
state = { hasError: '' }
// 当子组件出现错误时,会触发这个静态方法
// 返回值会作为新的 state
static getDerivedStateFromError(error) {
console.log('子组件出错了:', error)
return { hasError: error }
}
// 组件出错后的生命周期,用于记录错误日志
componentDidCatch(error, info) {
console.log('错误信息:', error)
console.log('错误组件栈:', info)
// 可以在这里上报错误到日志服务
}
render() {
return (
<div>
<h2>我是Parent组件</h2>
{this.state.hasError ? <h2>当前网络不稳定,稍后再试</h2> : <Child />}
</div>
)
}
}
Error Boundary 要点:
- 只能捕获后代组件在渲染过程中的错误
- 不能捕获自身的错误
- 不能捕获事件处理函数中的错误(用 try-catch)
- 不能捕获异步代码中的错误(如 setTimeout)
- 只能用类组件实现(需要 getDerivedStateFromError 或 componentDidCatch)
九、组件间通信方式总结(P126)
React 组件通信方式大全:
┌──────────────────────┬──────────────────────────────────┐
│ 方式 │ 适用场景 │
├──────────────────────┼──────────────────────────────────┤
│ props │ 父 → 子(最基本) │
│ 回调函数 props │ 子 → 父 │
│ 消息订阅发布 PubSub │ 任意组件(兄弟、跨层级) │
│ 集中式管理 Redux │ 任意组件(大型应用推荐) │
│ Context │ 祖孙组件(跨层级,生产者-消费者) │
│ Render Props │ 组件内容动态化(类似 Vue 插槽) │
└──────────────────────┴──────────────────────────────────┘
选择建议:
├── 父子通信 → props
├── 兄弟通信 → PubSub 或状态提升
├── 跨层级通信 → Context 或 Redux
└── 大型应用全局状态 → Redux(或 Zustand/MobX 等)
十、React Router 6(P127-P141)
10.1 Router 6 概述(P127)
React Router 6 是 2021 年 11 月发布的大版本更新,相比 v5 有较大变化:
- 内置了对 Hooks 的良好支持
- 包体积减小了约 60%
<Switch>改为<Routes>component={Xxx}改为element={<Xxx/>}- 新增了很多实用的 Hooks
10.2 一级路由(P128)
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<div>
<Link to="/about">About</Link>
<Link to="/home">Home</Link>
{/* Routes 替代了 Switch,且 Route 必须被 Routes 包裹 */}
<Routes>
{/* component 改为 element,值是 JSX 元素 */}
<Route path="/about" element={<About />} />
<Route path="/home" element={<Home />} />
</Routes>
</div>
</BrowserRouter>
)
}
v5 → v6 变化:
<Switch>→<Routes>(必须用 Routes 包裹 Route)component={Home}→element={<Home/>}- Route 默认就是严格匹配(不再需要 exact)
10.3 重定向 Navigate(P129)
import { Routes, Route, Navigate } from 'react-router-dom'
<Routes>
<Route path="/about" element={<About />} />
<Route path="/home" element={<Home />} />
{/* Redirect 改为 Navigate */}
<Route path="/" element={<Navigate to="/about" />} />
</Routes>
<Navigate> 只要被渲染就会触发跳转,可以用 replace 属性控制跳转模式。
10.4 NavLink 高亮(P130)
// v5 写法
<NavLink activeClassName="active" to="/about">About</NavLink>
// v6 写法 — activeClassName 被移除,改用函数式 className
<NavLink
className={({ isActive }) => isActive ? 'list-group-item active' : 'list-group-item'}
to="/about"
>
About
</NavLink>
10.5 useRoutes 路由表(P131)
import { useRoutes, Navigate } from 'react-router-dom'
// 路由表配置(类似 Vue Router 的路由配置)
const routes = [
{ path: '/about', element: <About /> },
{
path: '/home',
element: <Home />,
children: [
{ path: 'news', element: <News /> },
{ path: 'message', element: <Message /> },
]
},
{ path: '/', element: <Navigate to="/about" /> },
]
function App() {
// useRoutes 根据路由表生成路由组件
const element = useRoutes(routes)
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
{element}
</div>
)
}
🔗 概念扩展:路由表的好处 将路由配置集中管理,类似 Vue Router 的写法。便于维护、权限控制、动态路由生成等。
10.6 嵌套路由与 Outlet(P132)
import { Outlet } from 'react-router-dom'
function Home() {
return (
<div>
<h2>Home 内容</h2>
<NavLink to="news">News</NavLink>
<NavLink to="message">Message</NavLink>
{/* Outlet 指定子路由组件的渲染位置(类似 Vue 的 router-view) */}
<Outlet />
</div>
)
}
注意:子路由的 to 不需要写完整路径,直接写相对路径即可(如 news 而非 /home/news)。
10.7 路由参数(P133-P135)
params 参数(P133):
// 路由表
{ path: 'detail/:id/:title', element: <Detail /> }
// 链接
<Link to={`detail/${m.id}/${m.title}`}>{m.title}</Link>
// 接收 — 使用 useParams Hook
import { useParams } from 'react-router-dom'
function Detail() {
const { id, title } = useParams()
return <div>{id} - {title}</div>
}
search 参数(P134):
// 链接
<Link to={`detail?id=${m.id}&title=${m.title}`}>{m.title}</Link>
// 接收 — 使用 useSearchParams Hook
import { useSearchParams } from 'react-router-dom'
function Detail() {
const [search, setSearch] = useSearchParams()
const id = search.get('id')
const title = search.get('title')
return <div>{id} - {title}</div>
}
state 参数(P135):
// 链接
<Link to="detail" state={{ id: m.id, title: m.title }}>{m.title}</Link>
// 接收 — 使用 useLocation Hook
import { useLocation } from 'react-router-dom'
function Detail() {
const { state: { id, title } } = useLocation()
return <div>{id} - {title}</div>
}
10.8 编程式路由导航(P136)
import { useNavigate } from 'react-router-dom'
function Message() {
const navigate = useNavigate()
function showDetail(m) {
// push 模式(默认)
navigate('detail', {
replace: false,
state: { id: m.id, title: m.title }
})
// replace 模式
navigate('detail', { replace: true, state: { id: m.id, title: m.title } })
}
// 前进后退
function back() { navigate(-1) }
function forward() { navigate(1) }
return (
<div>
<button onClick={() => showDetail(msg)}>查看详情</button>
<button onClick={back}>后退</button>
<button onClick={forward}>前进</button>
</div>
)
}
10.9 其他 Hooks(P137-P140)
// useInRouterContext — 判断组件是否在 Router 上下文中
import { useInRouterContext } from 'react-router-dom'
console.log(useInRouterContext()) // true or false
// useNavigationType — 判断用户是如何来到当前页面的
import { useNavigationType } from 'react-router-dom'
const type = useNavigationType() // 'POP' | 'PUSH' | 'REPLACE'
// useOutlet — 获取当前嵌套路由的子路由元素
import { useOutlet } from 'react-router-dom'
const outlet = useOutlet()
// 如果子路由已挂载,返回子路由对象;否则返回 null
// useResolvedPath — 解析路径
import { useResolvedPath } from 'react-router-dom'
const resolved = useResolvedPath('/user?id=1&name=tom#abc')
// { pathname: '/user', search: '?id=1&name=tom', hash: '#abc' }
10.10 Router 6 总结(P141)
React Router 5 → 6 变化速查:
┌─────────────────────┬──────────────────────────────┐
│ v5 │ v6 │
├─────────────────────┼──────────────────────────────┤
│ <Switch> │ <Routes>(必须包裹 Route) │
│ component={Xxx} │ element={<Xxx/>} │
│ <Redirect to="/"> │ <Navigate to="/"/> │
│ activeClassName │ className={({isActive})=>..} │
│ 手动写嵌套 Route │ useRoutes 路由表 + Outlet │
│ this.props.history │ useNavigate() │
│ this.props.match │ useParams() │
│ this.props.location │ useLocation() │
│ withRouter(Comp) │ 不需要了(直接用 Hooks) │
│ 需要 exact │ 默认精确匹配 │
└─────────────────────┴──────────────────────────────┘
本章知识图谱
React 扩展与 Router 6
├── setState 深入
│ ├── 对象式 / 函数式
│ └── 异步特性 + 回调函数
├── 路由懒加载
│ ├── lazy() + import()
│ └── Suspense fallback
├── Hooks 三剑客
│ ├── useState → 函数组件的 state
│ ├── useEffect → 副作用(≈ 生命周期)
│ └── useRef → DOM 引用
├── 组件优化
│ ├── Fragment → 避免多余 DOM 节点
│ ├── PureComponent → 浅比较优化渲染
│ └── Error Boundary → 错误兜底 UI
├── 高级模式
│ ├── Context → 跨层级通信
│ └── Render Props → 动态内容(类似插槽)
└── React Router 6
├── Routes + Route element
├── Navigate 重定向
├── useRoutes 路由表
├── Outlet 子路由出口
├── useNavigate 编程式导航
├── useParams / useSearchParams / useLocation
└── 默认精确匹配,不再需要 exact
全系列完结 🎉
恭喜你完成了 React 全家桶的系统学习!回顾一下整个系列:
- React入门 — 虚拟DOM与JSX
- React组件核心 — State、Props、Refs
- React进阶 — 事件处理、表单与生命周期
- React脚手架与TodoList实战
- React网络请求 — 代理、Axios与Fetch
- React Router 5 全解
- React UI组件库 — Ant Design实践
- Redux与React-Redux状态管理
- React扩展与Router 6(本篇)
💡 学习建议:看完笔记后,建议动手实现一个完整的小项目(如博客系统、电商后台),把所有知识点串联起来。纸上得来终觉浅,绝知此事要躬行。