阅读视图

发现新文章,点击刷新页面。

【React-9/Lesson93(2025-12-30)】React Hooks 深度解析:从基础到实战🎯

📚 一、Hooks 概述

🌟 什么是 Hooks

Hooks 是 React 16.8 引入的新特性,它是一种函数编程思想的体现。Hooks 以 use 开头,用于封装 Vue/React 组件的状态和生命周期,让开发者可以"呼之即来",使用起来非常方便。

Hooks 的核心理念是将组件的状态逻辑抽离出来,使组件更加简洁、可维护。通过 Hooks,我们可以在不编写 class 组件的情况下使用 state 以及其他的 React 特性。

🔧 Hooks 的分类

Hooks 可以分为两大类:

  1. React 内置 Hooks:React 官方提供的一系列常用 Hooks
  2. 自定义 Hooks:开发者根据业务需求自己封装的 Hooks

🎨 二、React 内置 Hooks 详解

1️⃣ useState Hook

useState 是最基础的 Hook,用于在函数组件中添加状态管理能力。

基本语法

const [state, setState] = useState(initialValue)

参数说明

  • initialValue:状态的初始值,可以是任意类型(数字、字符串、对象、数组等)
  • 也可以是一个函数,用于惰性初始化状态

返回值

返回一个数组,包含两个元素:

  • 第一个元素:当前状态的值
  • 第二个元素:更新状态的函数

使用示例

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setCount(count - 1)}>减少</button>
    </div>
  )
}

惰性初始化

当初始状态需要通过复杂计算得出时,可以使用函数作为初始值:

const [todos, setTodos] = useState(() => {
  const storedTodos = localStorage.getItem('todos')
  return storedTodos ? JSON.parse(storedTodos) : []
})

这种方式可以避免每次渲染都执行复杂的初始化逻辑。

状态更新注意事项

  1. 直接替换:状态更新是直接替换,而不是合并
const [user, setUser] = useState({ name: '张三', age: 18 })

// 错误方式
setUser({ age: 19 }) // 会丢失 name 属性

// 正确方式
setUser({ ...user, age: 19 })
  1. 异步更新:状态更新是异步的,不能立即获取到最新值

  2. 函数式更新:当新状态依赖于旧状态时,使用函数式更新

setCount(prevCount => prevCount + 1)

2️⃣ useEffect Hook

useEffect 用于处理副作用操作,如数据获取、订阅、手动修改 DOM 等。

基本语法

useEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数(可选)
  }
}, [dependencies])

参数说明

  1. 第一个参数:副作用函数,在组件渲染后执行
  2. 第二个参数:依赖数组,控制副作用何时执行

执行时机

依赖数组 执行时机
不提供 每次渲染后都执行
[] 只在组件挂载时执行一次
[a, b] 当 a 或 b 变化时执行

清理函数

清理函数在组件卸载或下一次副作用执行前执行,主要用于:

  • 清除事件监听器
  • 清除定时器
  • 取消网络请求
  • 清除订阅

使用示例:事件监听

import { useState, useEffect } from 'react'

export default function useMouse() {
  const [x, setX] = useState(0)
  const [y, setY] = useState(0)

  useEffect(() => {
    const update = (event) => {
      console.log('鼠标移动')
      setX(event.pageX)
      setY(event.pageY)
    }
    
    // 组件挂载时,监听 mousemove 事件
    window.addEventListener('mousemove', update)
    console.log('事件监听已添加')
    
    return () => {
      // 组件卸载时,移除 mousemove 事件
      // 防止内存泄漏
      console.log('清除事件监听')
      window.removeEventListener('mousemove', update)
    }
  }, []) // 空依赖数组,只在挂载时执行一次

  return (
    <div>
      鼠标位置:{x} {y}
    </div>
  )
}

使用示例:数据持久化

const STORAGE_KEY = 'todos'

function loadFromStorage() {
  const storedTodos = localStorage.getItem(STORAGE_KEY)
  return storedTodos ? JSON.parse(storedTodos) : []
}

function saveToStorage(todos) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}

export default function useTodos() {
  const [todos, setTodos] = useState(() => loadFromStorage())

  // 监听 todos 变化,保存到 localStorage
  useEffect(() => {
    saveToStorage(todos)
  }, [todos])

  return { todos, setTodos }
}

3️⃣ useContext Hook

useContext 用于在组件树中跨层级传递数据,避免通过 props 一层层传递。

基本语法

const value = useContext(MyContext)

使用步骤

  1. 创建 Context
const MyContext = React.createContext(defaultValue)
  1. 提供 Context
<MyContext.Provider value={/* 某个值 */}>
  <子组件 />
</MyContext.Provider>
  1. 消费 Context
import { useContext } from 'react'

function ChildComponent() {
  const value = useContext(MyContext)
  return <div>{value}</div>
}

使用示例

import { createContext, useContext, useState } from 'react'

// 创建 Context
const ThemeContext = createContext()

// 父组件提供 Context
function App() {
  const [theme, setTheme] = useState('light')
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Content />
    </ThemeContext.Provider>
  )
}

// 子组件消费 Context
function Header() {
  const { theme, setTheme } = useContext(ThemeContext)
  
  return (
    <header className={theme}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </header>
  )
}

4️⃣ useRef Hook

useRef 用于创建一个可变的 ref 对象,其 .current 属性可以被赋值和读取。

基本语法

const refContainer = useRef(initialValue)

主要用途

  1. 访问 DOM 元素
import { useRef, useEffect } from 'react'

function TextInput() {
  const inputRef = useRef(null)
  
  useEffect(() => {
    inputRef.current.focus()
  }, [])
  
  return <input ref={inputRef} type="text" />
}
  1. 保存可变值(不触发重新渲染)
function Timer() {
  const timerRef = useRef(null)
  
  const start = () => {
    timerRef.current = setInterval(() => {
      console.log('定时器运行中')
    }, 1000)
  }
  
  const stop = () => {
    clearInterval(timerRef.current)
  }
  
  return (
    <div>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  )
}
  1. 保存上一次的值
function usePrevious(value) {
  const ref = useRef()
  
  useEffect(() => {
    ref.current = value
  }, [value])
  
  return ref.current
}

📝 三、JavaScript 函数回顾

1️⃣ 普通函数

使用 function 关键字声明的函数。

function greet(name) {
  return `你好,${name}!`
}

console.log(greet('张三')) // 你好,张三!

特点:

  • 有函数提升
  • 可以作为构造函数使用
  • this 指向调用时的对象

2️⃣ 箭头函数

ES6 引入的函数语法,更简洁。

const greet = (name) => {
  return `你好,${name}!`
}

// 简写形式
const greet = name => `你好,${name}!`

console.log(greet('李四')) // 你好,李四!

特点:

  • 没有 this 绑定,this 继承自外层作用域
  • 没有 arguments 对象
  • 不能作为构造函数使用
  • 没有 prototype 属性
  • 更简洁的语法

3️⃣ 匿名函数

没有函数名的函数,通常作为回调函数使用。

setTimeout(function() {
  console.log('1秒后执行')
}, 1000)

// 箭头函数形式的匿名函数
setTimeout(() => {
  console.log('1秒后执行')
}, 1000)

4️⃣ 立即执行函数

定义后立即执行的函数。

(function() {
  console.log('立即执行')
})()

// 箭头函数形式
(() => {
  console.log('立即执行')
})()

用途:

  • 创建独立作用域,避免变量污染
  • 模块化代码
  • 初始化配置

5️⃣ 递归函数

函数调用自身。

function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)
}

console.log(factorial(5)) // 120

使用场景:

  • 遍历树形结构
  • 计算阶乘、斐波那契数列
  • 深度优先搜索

6️⃣ 回调函数

作为参数传递给另一个函数的函数。

function processData(data, callback) {
  // 处理数据
  const result = data.map(item => item * 2)
  // 调用回调函数
  callback(result)
}

processData([1, 2, 3], (result) => {
  console.log(result) // [2, 4, 6]
})

在 React 中广泛应用:

<button onClick={() => handleClick(id)}>
  点击我
</button>

7️⃣ 构造函数

用于创建对象的函数。

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.greet = function() {
  console.log(`我是${this.name},今年${this.age}岁`)
}

const person = new Person('王五', 25)
person.greet() // 我是王五,今年25岁

ES6 类语法:

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  
  greet() {
    console.log(`我是${this.name},今年${this.age}岁`)
  }
}

8️⃣ 闭包

函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。

function createCounter() {
  let count = 0
  
  return {
    increment: () => {
      count++
      console.log(count)
    },
    decrement: () => {
      count--
      console.log(count)
    }
  }
}

const counter = createCounter()
counter.increment() // 1
counter.increment() // 2
counter.decrement() // 1

闭包的应用场景:

  • 数据私有化
  • 柯里化
  • 模块模式
  • 事件处理程序

在 React Hooks 中的闭包问题:

function Counter() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count) // 永远打印 0,因为闭包捕获了初始值
    }, 1000)
    
    return () => clearInterval(timer)
  }, []) // 空依赖数组
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

解决方案:使用函数式更新或添加正确的依赖。

🚀 四、自定义 Hooks

自定义 Hooks 是一个函数,其名称以 use 开头,函数内部可以调用其他 Hooks。

🎯 自定义 Hooks 的优势

  1. 逻辑复用:将组件间的共享逻辑抽离到自定义 Hook 中
  2. 关注点分离:UI 组件更简单,只负责 HTML + CSS,好维护
  3. 复用性好:和组件一样,是前端团队的核心资产
  4. 业务逻辑更简单:好测试

📦 案例 1:useMouse Hook

监听鼠标位置的自定义 Hook。

import { useState, useEffect } from 'react'

export default function useMouse() {
  const [x, setX] = useState(0)
  const [y, setY] = useState(0)

  useEffect(() => {
    const update = (event) => {
      console.log('鼠标移动')
      setX(event.pageX)
      setY(event.pageY)
    }
    
    // 组件挂载时,监听 mousemove 事件
    window.addEventListener('mousemove', update)
    console.log('事件监听已添加')
    
    return () => {
      // 组件卸载时,移除 mousemove 事件
      // 防止内存泄漏
      console.log('清除事件监听')
      window.removeEventListener('mousemove', update)
    }
  }, [])

  return { x, y }
}

使用方式:

import useMouse from './hooks/useMouse'

function App() {
  const { x, y } = useMouse()
  
  return (
    <div>
      鼠标位置:{x} {y}
    </div>
  )
}

📦 案例 2:useTodos Hook

完整的待办事项管理 Hook。

import { useState, useEffect } from 'react'

const STORAGE_KEY = 'todos'

// 从 localStorage 加载 todos
function loadFromStorage() {
  const storedTodos = localStorage.getItem(STORAGE_KEY)
  return storedTodos ? JSON.parse(storedTodos) : []
}

// 保存 todos 到 localStorage
function saveToStorage(todos) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}

export default function useTodos() {
  // useState 接收函数计算同步
  const [todos, setTodos] = useState(() => loadFromStorage())

  // 监听 todos 变化,保存到 localStorage
  useEffect(() => {
    saveToStorage(todos)
  }, [todos])
  
  // 添加 todo
  const addTodo = (text) => {
    text = text.trim()
    if (text === '') {
      return
    }
    setTodos([
      ...todos,
      {
        id: Date.now(),
        text: text,
        completed: false
      }
    ])
  }
  
  // 删除 todo
  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id))
  }
  
  // 切换 todo 完成状态
  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) => {
        if (todo.id === id) {
          return {
            ...todo,
            completed: !todo.completed
          }
        }
        return todo
      })
    )
  }

  return {
    todos,
    addTodo,
    deleteTodo,
    toggleTodo
  }
}

🎨 组件实现

TodoInput 组件

import { useState } from 'react'

export default function TodoInput({ onAddTodo }) {
  const [text, setText] = useState('')
  
  const handleSubmit = (e) => {
    e.preventDefault()
    if (!text.trim()) return
    onAddTodo(text.trim())
    setText('')
  }
  
  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button type="submit">添加</button>
    </form>
  )
}

TodoItem 组件

export default function TodoItem({ todo, onDeleteTodo, onToggleTodo }) {
  return (
    <li className='todo-item'>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggleTodo(todo.id)}
      />
      <span className={todo.completed ? 'completed' : ''}>
        {todo.text}
      </span>
      <button onClick={() => onDeleteTodo(todo.id)}>删除</button>
    </li>
  )
}

TodoList 组件

import TodoItem from './TodoItem.jsx'

export default function TodoList({ todos, onDeleteTodo, onToggleTodo }) {
  return (
    <ul className='todo-list'>
      {todos.map((todo) => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onDeleteTodo={onDeleteTodo}
          onToggleTodo={onToggleTodo}
        />
      ))}
    </ul>
  )
}

App 主组件

import useTodos from './hooks/useTodos.js'
import TodoList from './components/TodoList.jsx'
import TodoInput from './components/TodoInput.jsx'

export default function App() {
  const {
    todos,
    addTodo,
    deleteTodo,
    toggleTodo,
  } = useTodos()

  return (
    <>
      <TodoInput onAddTodo={addTodo} />
      {todos.length > 0 ? (
        <TodoList
          todos={todos}
          onDeleteTodo={deleteTodo}
          onToggleTodo={toggleTodo}
        />
      ) : (
        <div>暂无待办事项</div>
      )}
    </>
  )
}

⚠️ 五、内存泄漏与清理

🔍 什么是内存泄漏

内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

🐌 在 React 中的常见场景

  1. 未清除的事件监听器
// 错误示例
useEffect(() => {
  window.addEventListener('mousemove', update)
  // 缺少清理函数
}, [])
  1. 未清除的定时器
// 错误示例
useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器')
  }, 1000)
  // 缺少清理函数
}, [])
  1. 未取消的网络请求
// 错误示例
useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data))
  // 组件卸载后,请求可能仍在进行
}, [])

✅ 正确的清理方式

事件监听器清理

useEffect(() => {
  const update = (event) => {
    setX(event.pageX)
    setY(event.pageY)
  }
  
  window.addEventListener('mousemove', update)
  
  return () => {
    window.removeEventListener('mousemove', update)
  }
}, [])

定时器清理

useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器运行')
  }, 1000)
  
  return () => {
    clearInterval(timer)
  }
}, [])

网络请求清理

useEffect(() => {
  const controller = new AbortController()
  
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(data => setData(data))
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err)
      }
    })
  
  return () => {
    controller.abort()
  }
}, [])

🎯 useEffect 清理函数执行时机

清理函数在以下时机执行:

  1. 组件卸载时:组件从 DOM 中移除时
  2. 下一次副作用执行前:当依赖数组变化,新的副作用执行前
useEffect(() => {
  console.log('副作用执行')
  
  return () => {
    console.log('清理函数执行')
  }
}, [count])

执行顺序:

  • 组件挂载:副作用执行
  • count 变化:清理函数执行 → 副作用执行
  • 组件卸载:清理函数执行

🎓 六、Hooks 使用规则

📏 两条黄金规则

  1. 只在函数最顶层调用 Hooks
    • 不要在循环、条件判断或嵌套函数中调用 Hooks
    • 确保 Hooks 在每次渲染时都以相同的顺序被调用
// ❌ 错误示例
if (count > 0) {
  useEffect(() => {
    // ...
  }, [])
}

// ✅ 正确示例
useEffect(() => {
  if (count > 0) {
    // ...
  }
}, [count])
  1. 只在 React 函数中调用 Hooks
    • 在 React 函数组件中调用 Hooks
    • 在自定义 Hooks 中调用 Hooks
// ❌ 错误示例
function regularFunction() {
  const [count, setCount] = useState(0)
}

// ✅ 正确示例
function Component() {
  const [count, setCount] = useState(0)
}

🔧 ESLint 插件

使用 eslint-plugin-react-hooks 来强制执行这些规则:

{
  "plugins": [
    "react-hooks"
  ],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

📊 七、常用内置 Hooks 补充

useMemo Hook

用于缓存计算结果,避免不必要的重复计算。

import { useMemo } from 'react'

function ExpensiveComponent({ items }) {
  const sortedItems = useMemo(() => {
    console.log('计算排序')
    return items.sort((a, b) => a.value - b.value)
  }, [items])
  
  return <div>{sortedItems.map(...)}</div>
}

useCallback Hook

用于缓存函数,避免子组件不必要的重新渲染。

import { useCallback } from 'react'

function ParentComponent() {
  const handleClick = useCallback(() => {
    console.log('点击')
  }, [])
  
  return <ChildComponent onClick={handleClick} />
}

useReducer Hook

用于复杂的状态管理,类似 Redux。

import { useReducer } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  
  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>增加</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>减少</button>
    </div>
  )
}

🎯 八、项目实战总结

📁 项目结构

hooks-demo/
├── src/
│   ├── components/
│   │   ├── TodoInput.jsx
│   │   ├── TodoItem.jsx
│   │   └── TodoList.jsx
│   ├── hooks/
│   │   ├── useMouse.js
│   │   └── useTodos.js
│   ├── App.jsx
│   ├── main.jsx
│   └── index.css
└── package.json

🔄 数据流

App (使用 useTodos)
  ├── TodoInput (接收 onAddTodo)
  └── TodoList (接收 todos, onDeleteTodo, onToggleTodo)
      └── TodoItem (接收 todo, onDeleteTodo, onToggleTodo)

💡 核心要点

  1. 自定义 Hooks 封装业务逻辑,使组件更简洁
  2. useEffect 的清理函数防止内存泄漏
  3. useState 的惰性初始化提高性能
  4. localStorage 持久化数据
  5. 组件化开发,关注点分离

🚀 九、最佳实践

✨ 命名规范

  • 自定义 Hooks 必须以 use 开头
  • 组件名使用 PascalCase
  • 函数名使用 camelCase

🎨 组件设计

  1. 单一职责:每个组件只做一件事
  2. Props 最小化:只传递必要的 props
  3. 组合优于继承:使用组合构建复杂组件

🔧 Hooks 设计

  1. 关注点分离:将相关逻辑放在一个 Hook 中
  2. 返回对象:便于解构使用
  3. 提供清理函数:避免副作用导致的内存泄漏

📊 性能优化

  1. 使用 useMemo 缓存计算结果
  2. 使用 useCallback 缓存函数
  3. 合理使用依赖数组
  4. 避免不必要的渲染

🎊 十、总结

React Hooks 是 React 开发的核心特性,它通过函数式编程思想,让我们能够更优雅地管理组件状态和副作用。

核心要点回顾:

  • useState:管理组件状态
  • useEffect:处理副作用,包括清理函数防止内存泄漏
  • useContext:跨层级传递数据
  • useRef:访问 DOM 和保存可变值
  • 自定义 Hooks:逻辑复用,提高代码可维护性

通过合理使用 Hooks,我们可以编写出更简洁、更易维护、更易测试的 React 代码。Hooks 让函数组件拥有了类组件的所有能力,同时避免了类组件的复杂性和 this 绑定问题。

在实际开发中,遵循 Hooks 的使用规则,合理设计自定义 Hooks,充分利用 React 生态中的各种 Hooks,将大大提升开发效率和代码质量。

【React-7/Lesson90(2025-12-29)】React useRef 完全指南🎯

📚 什么是 useRef

useRef 是 React 提供的一个 Hook,用于创建一个可变的 ref 对象。这个对象在整个组件生命周期内保持不变,其 .current 属性可以被修改而不会触发组件重新渲染。

import { useRef } from 'react'

const myRef = useRef(initialValue)
// myRef.current 可以被读取和修改

🔍 useRef 与 useEffect 的关系

在 React 中,useEffect 类似于 Vue 的 onMounted 生命周期钩子。它们都在组件挂载后执行副作用操作。

useEffect(() => {
  // 类似于 Vue 的 onMounted
  console.log('组件已挂载')
}, [])

🎨 useRef 的应用场景

1️⃣ DOM 节点的引用

useRef 最常见的用途之一是直接访问 DOM 元素。通过将 ref 传递给 JSX 元素的 ref 属性,React 会在组件渲染后将 DOM 节点赋值给 ref 的 .current 属性。

import { useRef, useEffect } from 'react'

export default function InputDemo() {
  const inputRef = useRef(null)
  
  useEffect(() => {
    console.log(inputRef.current)
    inputRef.current.focus()
  }, [])

  return (
    <>
      <input ref={inputRef} placeholder="自动聚焦这里" />
    </>
  )
}

关键点:

  • useRef(null) 创建初始值为 null 的 ref
  • ref={inputRef} 将 ref 绑定到 input 元素
  • useEffect 中可以访问 inputRef.current 获取真实的 DOM 节点
  • 可以调用 DOM 方法如 focus()scrollIntoView()

2️⃣ 可变对象的存储

useRef 可以存储任何可变值,这些值在组件重新渲染时保持不变,非常适合存储不需要触发重新渲染的数据。

import { useRef, useState, useEffect } from 'react'

export default function TimerDemo() {
  let intervalId = useRef(null)
  const [count, setCount] = useState(0)

  function start() {
    intervalId.current = setInterval(() => {
      console.log('tick~~~~')
    }, 1000)
    console.log(intervalId)
  }

  function stop() {
    clearInterval(intervalId.current)
  }

  useEffect(() => {
    console.log(intervalId.current)
  }, [count])
  
  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      {count}
      <button type="button" onClick={() => setCount(count + 1)}>count++</button>
    </>
  )
}

使用场景:

  • 存储定时器 ID(如上面的例子)
  • 存储动画帧 ID(requestAnimationFrame)
  • 存储 WebSocket 连接
  • 存储上一次的值用于比较
  • 存储任何不需要触发重新渲染的数据

3️⃣ 创建持久化的引用对象

useRef 创建的 ref 对象在组件的整个生命周期内保持不变,即使组件重新渲染,ref 对象本身也不会改变,只有其 .current 属性的值可能会改变。

export default function PersistentRefDemo() {
  const [count, setCount] = useState(0)
  const inputRef = useRef(null)
  
  console.log('组件重新渲染')
  console.log(inputRef.current)
  
  useEffect(() => {
    console.log('组件挂载时执行')
    inputRef.current?.focus()
  }, [])

  return (
    <>
      <input ref={inputRef} />
      {count}
      <button type="button" onClick={() => setCount(count + 1)}>count++</button>
    </>
  )
}

⚖️ useRef 与 useState 的比较

🟢 相同点

两者都是储存可变对象的容器

// useState
const [state, setState] = useState(initialValue)

// useRef
const ref = useRef(initialValue)

它们都可以:

  • 在组件中存储数据
  • 在多次渲染之间保持数据
  • 存储任何类型的值(对象、数组、函数等)

🔴 不同点

特性 useState useRef
响应式 ✅ 是响应式的 ❌ 不是响应式的
更新方式 通过 setState 函数 直接修改 .current
重新渲染 状态改变会触发重新渲染 修改 .current 不会触发重新渲染
返回值 返回 [值, 更新函数] 返回 { current: 值 }
适用场景 需要响应式更新的数据 不需要触发重新渲染的数据

useState - 响应式状态:

const [count, setCount] = useState(0)

// 每次调用 setCount 都会触发组件重新渲染
setCount(count + 1)

useRef - 可变对象的存储:

const countRef = useRef(0)

// 直接修改 .current 不会触发重新渲染
countRef.current = countRef.current + 1

💡 深入理解 useRef 的工作原理

🔄 Ref 对象的结构

useRef 返回的是一个普通对象:

{
  current: initialValue
}

这个对象在组件的整个生命周期内保持不变,React 会在渲染过程中更新其 .current 属性。

📝 何时更新 .current

  1. DOM 引用: React 在渲染完成后自动更新
  2. 手动更新: 可以在任何地方直接修改 .current
const ref = useRef(0)

// 在事件处理中修改
function handleClick() {
  ref.current += 1
}

// 在定时器中修改
useEffect(() => {
  const timer = setInterval(() => {
    ref.current += 1
  }, 1000)
  
  return () => clearInterval(timer)
}, [])

⚠️ 注意事项

  1. 不要在渲染过程中读取/写入 ref
// ❌ 错误:在渲染过程中修改 ref
function BadComponent() {
  const ref = useRef(0)
  ref.current += 1  // 这会导致问题
  return <div>{ref.current}</div>
}

// ✅ 正确:在事件处理或 useEffect 中修改
function GoodComponent()() {
  const ref = useRef(0)
  
  useEffect(() => {
    ref.current = 0
  }, [])
  
  return <div onClick={() => ref.current++}>Click me</div>
}
  1. Ref 的初始值只在第一次渲染时使用
const ref = useRef(0)
// 组件重新渲染时,ref.current 不会被重置为 0

🎯 实际应用示例

📝 示例 1:自动聚焦输入框

import { useRef, useEffect } from 'react'

export default function AutoFocusInput() {
  const inputRef = useRef(null)
  
  useEffect(() => {
    inputRef.current?.focus()
  }, [])

  return (
    <div>
      <input 
        ref={inputRef}
        type="text"
        placeholder="我会自动聚焦"
      />
    </div>
  )
}

⏱️ 示例 2:计时器管理

import { useRef, useState, useEffect } from 'react'

export default function Timer() {
  const timerRef = useRef(null)
  const [seconds, setSeconds] = useState(0)
  
  const start = () => {
    if (timerRef.current) return
    
    timerRef.current = setInterval(() => {
      setSeconds(prev => prev + 1)
    }, 1000)
  }
  
  const stop = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current)
      timerRef.current = null
    }
  }
  
  const reset = () => {
    stop()
    setSeconds(0)
  }
  
  useEffect(() => {
    return () => {
      stop()
    }
  }, [])

  return (
    <div>
      <p>计时: {seconds} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

📜 示例 3:滚动到元素

import { useRef } from 'react'

export default function ScrollToElement() {
  const targetRef = useRef(null)
  
  const scrollToTarget = () => {
    targetRef.current?.scrollIntoView({ 
      behavior: 'smooth',
      block: 'center'
    })
  }

  return (
    <div>
      <button onClick={scrollToTarget}>滚动到目标</button>
      
      <div style={{ height: '500px' }}>内容区域</div>
      
      <div 
        ref={targetRef}
        style={{ 
          padding: '20px',
          backgroundColor: 'lightblue'
        }}
      >
        目标元素
      </div>
      
      <div style={{ height: '500px' }}>更多内容</div>
    </div>
  )
}

🔄 示例 4:存储上一次的值

import { useRef, useState, useEffect } from 'react'

export default function PreviousValueDemo() {
  const [count, setCount] = useState(0)
  const prevCountRef = useRef(0)
  
  useEffect(() => {
    prevCountRef.current = count
  }, [count])

  return (
    <div>
      <p>当前值: {count}</p>
      <p>上一次的值: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
    </div>
  )
}

🎨 示例 5:Canvas 绘图

import { useRef, useEffect } from 'react'

export default function CanvasDemo() {
  const canvasRef = useRef(null)
  
  useEffect(() => {
    const canvas = canvasRef.current
    const ctx = canvas.getContext('2d')
    
    ctx.fillStyle = 'lightblue'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    
    ctx.fillStyle = 'red'
    ctx.beginPath()
    ctx.arc(100, 100, 50, 0, Math.PI * 2)
    ctx.fill()
  }, [])

  return (
    <canvas 
      ref={canvasRef}
      width={200}
      height={200}
    />
  )
}

🚀 高级技巧

💾 在自定义 Hook 中使用 useRef

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  }, [value])
  return ref.current
}

function Component() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)
  
  return (
    <div>
      <p>当前: {count}</p>
      <p>之前: {prevCount}</p>
    </div>
  )
}

🔗 多个 Ref 的管理

function MultipleRefsDemo() {
  const input1Ref = useRef(null)
  const input2Ref = useRef(null)
  const input3Ref = useRef(null)
  
  const focusNext = (currentRef, nextRef) => {
    if (currentRef.current?.value) {
      nextRef.current?.focus()
    }
  }

  return (
    <form>
      <input 
        ref={input1Ref}
        onChange={() => focusNext(input1Ref, input2Ref)}
      />
      <input 
        ref={input2Ref}
        onChange={() => focusNext(input2Ref, input3Ref)}
      />
      <input ref={input3Ref} />
    </form>
  )
}

📖 总结

useRef 是 React 中一个强大而灵活的 Hook,它的主要特点包括:

持久化引用 - 在组件生命周期内保持不变
不触发重新渲染 - 修改 .current 不会导致组件更新
DOM 访问 - 可以直接访问和操作 DOM 元素
存储可变数据 - 适合存储不需要响应式的数据
性能优化 - 避免不必要的状态更新和重新渲染

选择 useRef 的时机:

  • 需要直接访问 DOM 元素
  • 需要存储定时器、动画帧等 ID
  • 需要在渲染之间保持数据但不需要触发重新渲染
  • 需要存储上一次的值用于比较
  • 需要在回调函数中访问最新的值

选择 useState 的时机:

  • 需要响应式更新 UI
  • 状态改变需要触发组件重新渲染
  • 需要在多个组件间共享状态

掌握 useRef 的使用,能够让你在 React 开发中更加得心应手,写出更高效、更优雅的代码!🎉

【React-6/Lesson89(2025-12-27)】React Context 详解:跨层级组件通信的最佳实践📚

🎯 React 组件通信方式概览

在 React 开发中,组件之间的数据传递是核心问题。目前主要有四种通信方式:

1️⃣ 父子组件通信

这是最基础的通信方式,父组件通过 props 将数据传递给子组件。这种方式简单直接,适用于层级较浅的组件关系。

function Child({ message }) {
  return <div>{message}</div>
}

function Parent() {
  return <Child message="Hello from Parent" />
}

2️⃣ 子父组件通信

子组件通过回调函数将数据传递给父组件。父组件定义一个函数,通过 props 传递给子组件,子组件调用这个函数来传递数据。

function Child({ onMessage }) {
  return <button onClick={() => onMessage('Hello from Child')}>发送消息</button>
}

function Parent() {
  const handleMessage = (msg) => {
    console.log(msg)
  }
  return <Child onMessage={handleMessage} />
}

3️⃣ 兄弟组件通信

兄弟组件之间的通信通常需要通过共同的父组件作为中转。父组件维护共享状态,兄弟组件通过 props 接收和修改这个状态。

function SiblingA({ value, onChange }) {
  return <input value={value} onChange={(e) => onChange(e.target.value)} />
}

function SiblingB({ value }) {
  return <div>输入的值:{value}</div>
}

function Parent() {
  const [value, setValue] = useState('')
  return (
    <>
      <SiblingA value={value} onChange={setValue} />
      <SiblingB value={value} />
    </>
  )
}

4️⃣ 跨层级通信

当组件层级较深时,使用 props 一层层传递数据会变得非常繁琐。这就是 Context 要解决的问题。

🚀 Context 的诞生背景

痛点分析

在传统的父子组件通信中,如果需要将数据从顶层组件传递到深层嵌套的子组件,就必须经过每一层中间组件。这种方式存在以下问题:

传递路径过长:数据需要经过多个中间组件,每个组件都需要接收并传递 props,即使它们并不使用这些数据。

维护成本高:每次修改数据结构或添加新的 props,都需要修改整条传递链路上的所有组件。

代码冗余:中间组件充满了不相关的 props 传递,代码可读性降低。

现实类比

这就像古代的驿站传递制度:皇帝要给边疆的将军送一封密信,必须经过沿途的每个驿站,每个驿站都要接收并转发这封信,即使驿站官员并不关心信的内容。这种传递方式效率低下,而且容易在传递过程中出现问题。

就像《长安的荔枝》中描述的那样,为了将新鲜荔枝从岭南送到长安,需要经过无数驿站,每个驿站都要接力传递,成本极高,风险很大。

💡 Context 的核心思想

Context 提供了一种在组件树中共享数据的方式,无需通过 props 一层层传递。它的核心思想是:

数据在查找的上下文里:在最外层组件提供数据,任何层级的组件都可以直接访问这些数据。

主动查找能力:需要消费数据的组件拥有主动查找数据的能力,而不是被动接收。

规矩不变:父组件(外层组件)仍然负责持有和改变数据,只是传递方式从"一路传"变成了"全局提供"。

📖 Context 的三步使用法

第一步:创建 Context 容器

使用 createContext 创建一个 Context 对象,这个对象就是数据容器。可以传入一个默认值作为参数。

import { createContext } from 'react'

export const UserContext = createContext(null)

createContext 接受一个默认值参数,当组件在匹配的 Provider 之外使用 Context 时,会使用这个默认值。

第二步:提供数据

使用 Context.Provider 组件包裹需要共享数据的组件树,通过 value 属性提供数据。

export default function App() {
  const user = {
    name: "Andrew"
  }
  
  return (
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  )
}

Provider 组件接受一个 value 属性,这个值会被所有消费该 Context 的组件访问到。Provider 可以嵌套使用,内层的 Provider 会覆盖外层的值。

第三步:消费数据

使用 useContext Hook 在组件中消费 Context 数据。

import { useContext } from 'react'
import { UserContext } from '../App'

export default function UserInfo() {
  const user = useContext(UserContext)
  return (
    <div>{user.name}</div>
  )
}

useContext 接受一个 Context 对象作为参数,返回该 Context 的当前值。当 Context 的值发生变化时,使用该 Context 的组件会重新渲染。

🎨 实战案例:主题切换应用

需求分析

创建一个支持白天/夜间主题切换的应用,主题状态需要在整个应用中共享。这是一个典型的跨层级通信场景。

项目结构

theme-demo/
├── src/
│   ├── App.jsx
│   ├── contexts/
│   │   └── ThemeContext.jsx
│   ├── components/
│   │   ├── Header.jsx
│   │   └── Content.jsx
│   ├── pages/
│   │   └── Page.jsx
│   └── theme.css

创建 ThemeContext

首先创建主题 Context,包含主题状态和切换主题的方法。

import { createContext, useState, useEffect } from 'react'

export const ThemeContext = createContext(null)

export default function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'))
  }
  
  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme)
    document.body.setAttribute('data-theme', theme)
  }, [theme])
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

关键点解析

  • useState 管理主题状态,初始值为 'light'
  • toggleTheme 函数使用函数式更新,确保基于前一个状态进行切换
  • useEffect 监听主题变化,同步更新 htmlbody 元素的 data-theme 属性
  • ThemeProvider 作为高阶组件,包裹子组件并提供主题上下文

在应用中使用 ThemeProvider

在应用的根组件中使用 ThemeProvider 包裹整个组件树。

import ThemeProvider from './contexts/ThemeContext'
import Page from './pages/Page'

export default function App() {
  return (
    <>
      <ThemeProvider>
        <Page />
      </ThemeProvider>
    </>
  )
}

在组件中消费主题数据

在 Header 组件中消费主题数据,显示当前主题并提供切换按钮。

import { useContext } from 'react'
import { ThemeContext } from '../contexts/ThemeContext'

export default function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  
  return (
    <div style={{ padding: 24 }}>
      <h2>当前主题:{theme}</h2>
      <button className="button" onClick={toggleTheme}>
        切换主题
      </button>
    </div>
{

在 Page 组件中也可以消费主题数据,实现不同组件共享同一主题状态。

import { useContext } from 'react'
import { ThemeContext } from '../contexts/ThemeContext'
import Header from '../components/Header'
import Content from '../components/Content'

export default function Page() {
  const theme = useContext(ThemeContext)
  
  return (
    <div style={{ padding: 24 }}>
      <Header />
      <Content />
    </div>
  )
}

🎯 CSS 变量实现主题切换

使用 CSS 变量(Custom Properties)是实现主题切换的优雅方式。CSS 变量允许我们在不同的主题下动态改变样式。

定义 CSS 变量

theme.css 中定义主题相关的 CSS 变量。

:root {
  --bg-color: pink
  --text-color: #222
  --primary-color: #1677ff
}

[data-theme='dark'] {
  --bg-color: #141414
  --text-color: #f5f5f5
  --primary-color: #4e8cff
}

语法解析

  • :root 选择器匹配文档的根元素,在这里定义全局 CSS 变量
  • --bg-color--text-color 等是自定义属性名,必须以 -- 开头
  • [data-theme='dark'] 是属性选择器,匹配所有 data-theme 属性值为 'dark' 的元素
  • 在不同的选择器中重新定义变量值,实现主题切换

使用 CSS 变量

在样式中使用 var() 函数引用 CSS 变量。

body {
  margin: 0
  background-color: var(--bg-color)
  color: var(--text-color)
  transition: all 0.3s
}

.button {
  padding: 8px 16px
  background: var(--primary-color)
  color: #fff
  border: none
  cursor: pointer
}

关键特性

  • var(--bg-color) 引用之前定义的 CSS 变量
  • data-theme 属性改变时,CSS 变量的值会自动更新
  • transition: all 0.3s 实现主题切换的平滑过渡效果

JavaScript 控制 CSS 变量

通过 JavaScript 动态修改元素的 data-theme 属性,触发 CSS 变量的变化。

useEffect(() => {
  document.documentElement.setAttribute('data-theme', theme)
  document.body.setAttribute('data-theme', theme)
}, [theme])

当 theme 状态变化时,useEffect 会执行,更新 DOM 元素的属性,从而触发 CSS 变量的重新计算。

🔍 Context 的高级特性

默认值的作用

Context 的默认值在以下情况下使用:

  1. 组件在匹配的 Provider 之外使用 Context
  2. Provider 的 value 属性为 undefined
const MyContext = createContext('默认值')

function Component() {
  const value = useContext(MyContext)
  return <div>{value}</div>
}

export default function App() {
  return <Component />
}

在这个例子中,Component 会显示"默认值",因为它没有被任何 Provider 包裹。

Provider 嵌套

多个 Provider 可以嵌套使用,内层的 Provider 会覆盖外层的值。

const ThemeContext = createContext('light')
const ColorContext = createContext('blue')

function Child() {
  const theme = useContext(ThemeContext)
  const color = useContext(ColorContext)
  return <div>主题:{theme},颜色:{color}</div>
}

export default function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ColorContext.Provider value="red">
        <Child />
      </ColorContext.Provider>
    </ThemeContext.Provider>
  )
}

Child 组件会显示"主题:dark,颜色:red"。

Context 性能优化

当 Context 的值变化时,所有消费该 Context 的组件都会重新渲染。为了优化性能,可以:

  1. 拆分 Context:将频繁变化和不常变化的数据拆分到不同的 Context 中
const UserContext = createContext(null)
const ThemeContext = createContext(null)

function App() {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
  
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Child />
      </ThemeContext.Provider>
    </UserContext.Provider>
  )
}
  1. 使用 useMemo:避免不必要的对象重新创建
function App() {
  const [theme, setTheme] = useState('light')
  
  const contextValue = useMemo(() => ({
    theme,
    toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }), [theme])
  
  return (
    <ThemeContext.Provider value={contextValue}>
      <Child />
    </ThemeContext.Provider>
  )
}
  1. 使用 React.memo:避免不必要的组件重新渲染
const ExpensiveComponent = React.memo(function ExpensiveComponent() {
  const { theme } = useContext(ThemeContext)
  return <div>主题:{theme}</div>
})

📝 Context 最佳实践

1. 合理拆分 Context

不要将所有数据都放在一个 Context 中,应该根据功能模块合理拆分。

const UserContext = createContext(null)
const ThemeContext = createContext(null)
const LocaleContext = createContext(null)

2. 使用自定义 Hook

封装 Context 的消费逻辑,提供更友好的 API。

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内部使用')
  }
  return context
}

function Component() {
  const { theme, toggleTheme } = useTheme()
  return <button onClick={toggleTheme}>{theme}</button>
}

3. 提供默认值或错误处理

确保 Context 的使用是安全的,提供合理的默认值或错误处理。

export function useUser() {
  const user = useContext(UserContext)
  if (!user) {
    throw new Error('useUser 必须在 UserProvider 内部使用')
  }
  return user
}

4. 避免过度使用

Context 适合用于全局状态,如用户信息、主题、语言设置等。对于局部状态,仍然应该使用 props 或状态管理库。

5. 文档化 Context

为 Context 添加清晰的文档说明,包括提供的数据类型和使用方法。

/**
 * 用户上下文
 * @type {React.Context<{
 *   name: string
 *   email: string
 *   role: string
 * }>}
 */
export const UserContext = createContext(null)

🌟 总结

React Context 是解决跨层级组件通信的强大工具,它提供了优雅的数据共享方式,避免了 props 传递的繁琐。通过合理使用 Context,可以:

  • 简化组件间的数据传递
  • 提高代码的可维护性
  • 实现全局状态管理
  • 构建更灵活的应用架构

结合 CSS 变量,Context 可以轻松实现主题切换、国际化等全局功能。在实际开发中,应该根据具体需求选择合适的通信方式,Context 是工具箱中的重要一员,但不是唯一的解决方案。

❌