React全家桶笔记(九):React扩展与Router 6

3 阅读11分钟

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])  → 挂载 + ab 变化时执行
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 56 变化速查:
┌─────────────────────┬──────────────────────────────┐
│ v5                   │ v6                           │
├─────────────────────┼──────────────────────────────┤
│ <Switch>             │ <Routes>(必须包裹 Route)    │
│ component={Xxx}      │ element={<Xxx/>}             │
│ <Redirect to="/">    │ <Navigate to="/"/>           │
│ activeClassName      │ className={({isActive})=>..} │
│ 手动写嵌套 Route      │ useRoutes 路由表 + Outlet    │
│ this.props.historyuseNavigate()                │
│ this.props.matchuseParams()                  │
│ this.props.locationuseLocation()                │
│ 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 全家桶的系统学习!回顾一下整个系列:

  1. React入门 — 虚拟DOM与JSX
  2. React组件核心 — State、Props、Refs
  3. React进阶 — 事件处理、表单与生命周期
  4. React脚手架与TodoList实战
  5. React网络请求 — 代理、Axios与Fetch
  6. React Router 5 全解
  7. React UI组件库 — Ant Design实践
  8. Redux与React-Redux状态管理
  9. React扩展与Router 6(本篇)

💡 学习建议:看完笔记后,建议动手实现一个完整的小项目(如博客系统、电商后台),把所有知识点串联起来。纸上得来终觉浅,绝知此事要躬行。