React Hooks入门

React Hooks 深度解析 - 每个Hook的本质与实践

理解React Hooks的关键在于明白一个核心概念:React组件就像是一个函数,每次渲染都是一次全新的函数调用,所有的变量都会重新创建。Hooks就是React提供给我们的工具,让我们在这个"函数式"的世界里拥有记忆能力。

第一类:数据驱动 - 状态管理的基石

useState - React中的记忆细胞

想象你的大脑每次思考都会完全重置,但你有一个笔记本可以记录重要信息。useState就是这个笔记本,它帮助组件在重新渲染之间记住数据。

jsx 复制代码
// 基础用法
const [count, setCount] = useState(0)
// useState返回两个东西:
// 1. count - 当前的值(从笔记本读取)
// 2. setCount - 更新值的函数(在笔记本上写新内容)

// 为什么不能直接 count = 5?
// 因为这样React不知道数据变了,不会重新渲染
// 必须通过 setCount(5) 来告诉React:"数据变了,请重新渲染"

useState还有一些高级用法需要理解:

jsx 复制代码
// 函数式更新 - 当新值依赖于旧值时使用
const [count, setCount] = useState(0)
setCount(prevCount => prevCount + 1) // 推荐
setCount(count + 1) // 在某些情况下可能出现问题

// 惰性初始化 - 当初始值计算复杂时使用
const [data, setData] = useState(() => {
  // 这个函数只会在组件首次渲染时执行一次
  return expensiveCalculation()
})

useReducer - 状态变化的导演

当你的状态逻辑变得复杂,useState就像用螺丝刀修汽车一样力不从心。useReducer就是这时候的专业工具箱,它让复杂的状态变化变得可预测和可维护。

jsx 复制代码
// useReducer的三个核心概念:
// 1. State - 当前状态
// 2. Action - 描述"发生了什么"的对象
// 3. Reducer - 根据action来决定如何更新state的纯函数

function todoReducer(state, action) {
  // Reducer必须是纯函数:相同输入总是产生相同输出,不能有副作用
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false
        }]
      }
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      }
    default:
      return state // 永远要有默认情况
  }
}

// 使用useReducer
const initialState = { todos: [], filter: 'all' }
const [state, dispatch] = useReducer(todoReducer, initialState)

// 通过dispatch发送action来更新状态
dispatch({ type: 'ADD_TODO', payload: '学习React' })
dispatch({ type: 'TOGGLE_TODO', payload: 1 })

useReducer的优势在于它让状态变化变得可追踪。每个状态改变都有明确的原因(action),这对调试和理解代码非常有帮助。

useImmer - 让不可变更新变得自然

React要求状态更新必须是不可变的,这意味着你不能直接修改对象或数组,而必须创建新的副本。这个要求对于复杂的嵌套数据结构来说很繁琐。useImmer让你可以用"修改"的方式来写不可变更新。

jsx 复制代码
import { useImmer } from 'use-immer'

const [todos, updateTodos] = useImmer([
  { id: 1, text: '学习React', tags: ['前端', '框架'] }
])

// 传统的不可变更新写法(繁琐)
setTodos(todos.map(todo => 
  todo.id === 1 
    ? { ...todo, tags: [...todo.tags, '重要'] }
    : todo
))

// 使用useImmer的写法(直观)
updateTodos(draft => {
  const todo = draft.find(t => t.id === 1)
  if (todo) {
    todo.tags.push('重要') // 看起来像直接修改,实际上是不可变更新
  }
})

useSyncExternalStore - 与外部世界的桥梁

这个Hook是为了解决一个特殊问题:如何让React组件订阅外部数据源(比如浏览器API、第三方状态管理库)的变化。

jsx 复制代码
// 订阅浏览器的在线状态
function useOnlineStatus() {
  return useSyncExternalStore(
    // subscribe函数:告诉外部数据源如何通知我们变化
    (callback) => {
      window.addEventListener('online', callback)
      window.addEventListener('offline', callback)
      // 返回清理函数
      return () => {
        window.removeEventListener('online', callback)
        window.removeEventListener('offline', callback)
      }
    },
    // getSnapshot函数:获取当前的值
    () => navigator.onLine,
    // getServerSnapshot函数:服务端渲染时的值
    () => true
  )
}

// 使用
function App() {
  const isOnline = useOnlineStatus()
  return <div>{isOnline ? '在线' : '离线'}</div>
}

useTransition - 让界面保持响应

想象你在做饭,同时有客人按门铃。按门铃是紧急的(用户交互),做饭是重要但不紧急的(数据更新)。useTransition让你可以标记某些更新为"不紧急",确保紧急操作优先处理。

jsx 复制代码
function SearchResults() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleSearch = (newQuery) => {
    setQuery(newQuery) // 立即更新输入框(紧急)
    
    startTransition(() => {
      // 这里的更新被标记为"过渡性"的,不会阻塞紧急更新
      setResults(expensiveSearch(newQuery))
    })
  }

  return (
    <div>
      <input value={query} onChange={e => handleSearch(e.target.value)} />
      {isPending && <div>搜索中...</div>}
      {results.map(result => <div key={result.id}>{result.title}</div>)}
    </div>
  )
}

useDeferredValue - 延迟非关键更新

这是useTransition的伙伴,它让你可以延迟某个值的更新,直到更重要的更新完成。

jsx 复制代码
function App() {
  const [text, setText] = useState('')
  const deferredText = useDeferredValue(text)

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      {/* ExpensiveTree会使用延迟的值,不会阻塞输入 */}
      <ExpensiveTree text={deferredText} />
    </div>
  )
}

第二类:副作用 - 与外部世界交互

useEffect - React的瑞士军刀

useEffect可能是最难理解但最重要的Hook。它的核心思想是:当某些数据变化时,执行某些副作用操作。

jsx 复制代码
// useEffect的完整形态
useEffect(
  () => {
    // 副作用函数:在依赖变化后执行
    console.log('count变化了:', count)
    
    // 清理函数:在下次副作用执行前或组件卸载时执行
    return () => {
      console.log('清理上一次的副作用')
    }
  },
  [count] // 依赖数组:决定何时重新执行副作用
)

理解useEffect的关键是理解它的执行时机:

jsx 复制代码
// 1. 只在挂载时执行(相当于Vue的onMounted)
useEffect(() => {
  console.log('组件挂载了')
}, []) // 空依赖数组

// 2. 在特定数据变化时执行(相当于Vue的watch)
useEffect(() => {
  console.log('count变化了:', count)
}, [count]) // 监听count的变化

// 3. 每次渲染后都执行(通常不推荐)
useEffect(() => {
  console.log('每次渲染后执行')
}) // 没有依赖数组

// 4. 数据获取的典型模式
useEffect(() => {
  let cancelled = false // 防止竞态条件
  
  async function fetchData() {
    const response = await fetch(`/api/users/${userId}`)
    const data = await response.json()
    
    if (!cancelled) { // 只有请求没被取消才更新状态
      setUser(data)
    }
  }
  
  fetchData()
  
  return () => {
    cancelled = true // 组件卸载或userId变化时取消请求
  }
}, [userId])

useLayoutEffect - 同步的副作用

useLayoutEffect和useEffect的区别在于执行时机:useLayoutEffect在DOM更新后、浏览器绘制前同步执行,而useEffect在绘制后异步执行。

jsx 复制代码
function Component() {
  const [width, setWidth] = useState(0)
  const divRef = useRef()

  // 使用useLayoutEffect避免闪烁
  useLayoutEffect(() => {
    // 在浏览器绘制前同步测量DOM
    setWidth(divRef.current.offsetWidth)
  }, [])

  return <div ref={divRef}>宽度: {width}px</div>
}

什么时候使用useLayoutEffect?当你需要:

  • 测量DOM元素的尺寸或位置
  • 同步更新DOM以避免视觉闪烁
  • 在浏览器绘制前完成关键操作

useInsertionEffect - CSS-in-JS的专用工具

这是一个非常特殊的Hook,专门为CSS-in-JS库设计。它在DOM变更之前执行,比useLayoutEffect还要早。

jsx 复制代码
// 通常只在CSS-in-JS库的内部使用
useInsertionEffect(() => {
  // 在这里插入样式表
  const style = document.createElement('style')
  style.textContent = `.dynamic-class { color: red; }`
  document.head.appendChild(style)
  
  return () => {
    document.head.removeChild(style)
  }
}, [])

第三类:状态传递 - 跨越组件边界

useRef - React的"记事本"

useRef有两个主要用途:访问DOM元素和存储不触发重渲染的值。

jsx 复制代码
function TextInput() {
  const inputRef = useRef(null)
  const countRef = useRef(0)

  const focusInput = () => {
    inputRef.current.focus() // 访问DOM元素
  }

  const incrementCount = () => {
    countRef.current += 1 // 更新值但不触发重渲染
    console.log('Count:', countRef.current)
  }

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focusInput}>聚焦输入框</button>
      <button onClick={incrementCount}>增加计数(不重渲染)</button>
    </div>
  )
}

useRef和useState的区别:

  • useState的更新会触发重渲染,useRef不会
  • useState的值在每次渲染时都是新的,useRef.current在整个组件生命周期中保持引用不变

useImperativeHandle - 自定义组件的"接口"

这个Hook让你可以自定义通过ref暴露给父组件的值。它通常与forwardRef一起使用。

jsx 复制代码
import { forwardRef, useImperativeHandle, useRef } from 'react'

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef()
  const [value, setValue] = useState('')

  useImperativeHandle(ref, () => ({
    // 自定义暴露给父组件的方法
    focus: () => inputRef.current.focus(),
    clear: () => setValue(''),
    getValue: () => value,
    // 不暴露inputRef,保持封装性
  }))

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  )
})

// 父组件使用
function Parent() {
  const customInputRef = useRef()

  const handleAction = () => {
    customInputRef.current.focus()
    customInputRef.current.clear()
  }

  return (
    <div>
      <CustomInput ref={customInputRef} />
      <button onClick={handleAction}>聚焦并清空</button>
    </div>
  )
}

useContext - 数据的"广播系统"

useContext解决了props drilling的问题,让你可以在组件树中直接访问上层提供的数据。

jsx 复制代码
// 1. 创建Context
const ThemeContext = createContext()
const UserContext = createContext()

// 2. 提供数据
function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState({ name: '张三' })

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Header />
        <Main />
        <Footer />
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
}

// 3. 消费数据
function Header() {
  const { theme, setTheme } = useContext(ThemeContext)
  const { user } = useContext(UserContext)

  return (
    <header className={`header-${theme}`}>
      <h1>欢迎, {user.name}</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </header>
  )
}

// 创建自定义Hook来简化使用
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme必须在ThemeProvider内部使用')
  }
  return context
}

第四类:状态派生 - 性能优化的利器

useMemo - 昂贵计算的缓存

useMemo就像是一个聪明的助手,它会记住复杂计算的结果,只有当输入变化时才重新计算。

jsx 复制代码
function ProductList({ products, searchTerm, sortBy }) {
  // 没有useMemo:每次渲染都会重新过滤和排序
  // const filteredAndSortedProducts = products
  //   .filter(p => p.name.includes(searchTerm))
  //   .sort((a, b) => a[sortBy] - b[sortBy])

  // 使用useMemo:只有当依赖变化时才重新计算
  const filteredAndSortedProducts = useMemo(() => {
    console.log('重新计算产品列表') // 用于调试
    return products
      .filter(p => p.name.includes(searchTerm))
      .sort((a, b) => a[sortBy] - b[sortBy])
  }, [products, searchTerm, sortBy])

  return (
    <div>
      {filteredAndSortedProducts.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </div>
  )
}

何时使用useMemo?

  • 计算成本很高
  • 计算结果被传递给昂贵的子组件
  • 计算结果用作其他Hook的依赖

useCallback - 函数的"身份证"

在React中,每次渲染都会创建新的函数实例。useCallback可以保持函数的引用稳定,避免子组件不必要的重渲染。

jsx 复制代码
function TodoApp() {
  const [todos, setTodos] = useState([])
  const [filter, setFilter] = useState('all')

  // 没有useCallback:每次渲染都创建新函数
  // const handleToggle = (id) => {
  //   setTodos(todos.map(todo => 
  //     todo.id === id ? { ...todo, completed: !todo.completed } : todo
  //   ))
  // }

  // 使用useCallback:只有依赖变化时才创建新函数
  const handleToggle = useCallback((id) => {
    setTodos(todos => todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }, []) // 使用函数更新模式,所以不需要依赖todos

  const handleDelete = useCallback((id) => {
    setTodos(todos => todos.filter(todo => todo.id !== id))
  }, [])

  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active': return todos.filter(todo => !todo.completed)
      case 'completed': return todos.filter(todo => todo.completed)
      default: return todos
    }
  }, [todos, filter])

  return (
    <div>
      <FilterButtons filter={filter} onFilterChange={setFilter} />
      <TodoList 
        todos={filteredTodos}
        onToggle={handleToggle}
        onDelete={handleDelete}
      />
    </div>
  )
}

// 子组件使用React.memo来避免不必要的重渲染
const TodoList = React.memo(({ todos, onToggle, onDelete }) => {
  console.log('TodoList重新渲染') // 用于调试
  
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  )
})

第五类:工具类 - 开发辅助

useDebugValue - 调试信息的显示器

这个Hook让你在React开发者工具中显示自定义Hook的调试信息。

jsx 复制代码
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)
  
  // 在开发者工具中显示调试信息
  useDebugValue(count > 10 ? '高' : '低')
  
  // 或者使用格式化函数(只有在开发者工具打开时才会执行)
  useDebugValue(count, c => `计数: ${c}`)

  const increment = useCallback(() => setCount(c => c + 1), [])
  const decrement = useCallback(() => setCount(c => c - 1), [])

  return { count, increment, decrement }
}

useId - 唯一标识符的生成器

useId生成稳定的唯一ID,特别适用于可访问性属性。

jsx 复制代码
function FormField({ label, type = 'text' }) {
  const id = useId()
  const descriptionId = `${id}-description`

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        type={type}
        aria-describedby={descriptionId}
      />
      <div id={descriptionId}>
        这是{label}的说明文字
      </div>
    </div>
  )
}

总结:Hook的使用原则

理解了这些Hook之后,记住几个重要原则:

依赖诚实原则:Hook的依赖数组必须包含所有在副作用中使用的响应式值。不要试图"欺骗"React,这会导致难以调试的bug。

不要过度优化:useMemo和useCallback是优化工具,不是默认选择。先写出正确的代码,再在性能瓶颈处使用优化。

自定义Hook的力量:当你发现相同的逻辑在多个组件中重复时,考虑提取成自定义Hook。这是React中代码复用的最佳方式。

每个Hook都有其特定的使用场景和解决的问题。理解它们的本质比记住API更重要。当你遇到状态管理问题时,想想哪个Hook最适合解决这个具体问题,而不是死记硬背使用方法。

相关推荐
程序视点3 小时前
IObit Uninstaller Pro专业卸载,免激活版本,卸载清理注册表,彻底告别软件残留
前端·windows·后端
前端程序媛-Tian3 小时前
【dropdown组件填坑指南】—怎么实现下拉框的位置计算
前端·javascript·vue
嘉琪0013 小时前
实现视频实时马赛克
linux·前端·javascript
烛阴3 小时前
Smoothstep
前端·webgl
十盒半价4 小时前
React 性能优化秘籍:从渲染顺序到组件粒度
react.js·性能优化·trae
若梦plus4 小时前
Eslint中微内核&插件化思想的应用
前端·eslint
爱分享的程序员4 小时前
前端面试专栏-前沿技术:30.跨端开发技术(React Native、Flutter)
前端·javascript·面试
超级土豆粉4 小时前
Taro 位置相关 API 介绍
前端·javascript·react.js·taro
若梦plus4 小时前
Webpack中微内核&插件化思想的应用
前端·webpack
若梦plus4 小时前
微内核&插件化设计思想
前端