React(useImmer) 补充篇

useImmer

概述

useImmer 是基于 immer 库实现的一个 React Hook,它让你可以像修改可变数据一样来修改不可变数据。immer 是一个不可变的数据结构库,完全符合 React 的不可变性原则。

核心特性

  • 🚀 简化状态更新:无需手动创建不可变更新
  • 🛡️ 类型安全:完整的 TypeScript 支持
  • 性能优化:只在真正需要时创建新对象
  • 🎯 直观语法:像修改可变数据一样编写代码

安装

bash 复制代码
npm install immer use-immer

API 参考

useImmer

typescript 复制代码
function useImmer<S>(initialState: S | (() => S)): [S, (f: (draft: Draft<S>) => void | S) => void]

参数:

  • initialState: 初始状态值或返回初始状态的函数

返回值:

  • state: 当前状态
  • setState: 更新状态的函数

useImmerReducer

typescript 复制代码
function useImmerReducer<R extends Reducer<any, any>, I>(
  reducer: R,
  initializerArg: I & InitializerArg<R>,
  initializer?: (arg: I & InitializerArg<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>]

使用指南

1. 处理嵌套对象

useImmer 在处理深层嵌套对象时特别有用,无需手动展开每一层:

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

interface User {
  name: string
  age: number
  profile: {
    avatar: string
    bio: string
    preferences: {
      theme: 'light' | 'dark'
      notifications: boolean
    }
  }
}

export default function UserProfile() {
  const [user, setUser] = useImmer<User>({
    name: '大满zs',
    age: 25,
    profile: {
      avatar: '/avatar.jpg',
      bio: '前端开发者',
      preferences: {
        theme: 'light',
        notifications: true
      }
    }
  })

  const updateTheme = () => {
    setUser(draft => {
      draft.profile.preferences.theme = 'dark'
    })
  }

  const updateBio = (newBio: string) => {
    setUser(draft => {
      draft.profile.bio = newBio
    })
  }

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>年龄: {user.age}</p>
      <p>个人简介: {user.profile.bio}</p>
      <p>主题: {user.profile.preferences.theme}</p>
      
      <button onClick={updateTheme}>切换主题</button>
      <button onClick={() => updateBio('热爱编程的开发者')}>
        更新简介
      </button>
    </div>
  )
}

2. 处理数组

数组操作变得异常简单,所有原生数组方法都可以直接使用:

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

interface Todo {
  id: number
  text: string
  completed: boolean
}

export default function TodoList() {
  const [todos, setTodos] = useImmer<Todo[]>([])

  const addTodo = (text: string) => {
    setTodos(draft => {
      draft.push({
        id: Date.now(),
        text,
        completed: false
      })
    })
  }

  const toggleTodo = (id: number) => {
    setTodos(draft => {
      const todo = draft.find(t => t.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    })
  }

  const removeTodo = (id: number) => {
    setTodos(draft => {
      const index = draft.findIndex(t => t.id === id)
      if (index > -1) {
        draft.splice(index, 1)
      }
    })
  }

  const clearCompleted = () => {
    setTodos(draft => {
      return draft.filter(todo => !todo.completed)
    })
  }

  return (
    <div className="todo-list">
      <h2>待办事项 ({todos.length})</h2>
      
      <div className="add-todo">
        <input 
          type="text" 
          placeholder="添加新待办..."
          onKeyPress={(e) => {
            if (e.key === 'Enter' && e.currentTarget.value.trim()) {
              addTodo(e.currentTarget.value.trim())
              e.currentTarget.value = ''
            }
          }}
        />
      </div>

      <ul>
        {todos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>

      {todos.some(t => t.completed) && (
        <button onClick={clearCompleted}>清除已完成</button>
      )}
    </div>
  )
}

3. 处理基本类型

对于基本类型,useImmer 的行为与 useState 完全一致:

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

export default function Counter() {
  const [count, setCount] = useImmer(0)
  const [isVisible, setIsVisible] = useImmer(true)

  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)
  const reset = () => setCount(0)
  const toggleVisibility = () => setIsVisible(!isVisible)

  return (
    <div className="counter">
      {isVisible && (
        <>
          <h2>计数器: {count}</h2>
          <div className="controls">
            <button onClick={decrement}>-</button>
            <button onClick={increment}>+</button>
            <button onClick={reset}>重置</button>
          </div>
        </>
      )}
      <button onClick={toggleVisibility}>
        {isVisible ? '隐藏' : '显示'}计数器
      </button>
    </div>
  )
}

useImmerReducer 使用

useImmerReducer 结合了 useReducer 和 immer 的优势,让 reducer 函数更加简洁:

tsx 复制代码
import { useImmerReducer } from 'use-immer'

interface State {
  count: number
  history: number[]
  isLoading: boolean
}

type Action = 
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'RESET' }
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'ADD_TO_HISTORY' }

const initialState: State = {
  count: 0,
  history: [],
  isLoading: false
}

function counterReducer(draft: State, action: Action) {
  switch (action.type) {
    case 'INCREMENT':
      draft.count += 1
      break
    case 'DECREMENT':
      draft.count -= 1
      break
    case 'RESET':
      draft.count = 0
      break
    case 'SET_LOADING':
      draft.isLoading = action.payload
      break
    case 'ADD_TO_HISTORY':
      draft.history.push(draft.count)
      break
  }
}

export default function AdvancedCounter() {
  const [state, dispatch] = useImmerReducer(counterReducer, initialState)

  const handleIncrement = () => {
    dispatch({ type: 'SET_LOADING', payload: true })
    
    // 模拟异步操作
    setTimeout(() => {
      dispatch({ type: 'INCREMENT' })
      dispatch({ type: 'ADD_TO_HISTORY' })
      dispatch({ type: 'SET_LOADING', payload: false })
    }, 500)
  }

  return (
    <div className="advanced-counter">
      <h2>高级计数器</h2>
      
      <div className="display">
        <span>当前值: {state.count}</span>
        {state.isLoading && <span className="loading">加载中...</span>}
      </div>

      <div className="controls">
        <button 
          onClick={handleIncrement}
          disabled={state.isLoading}
        >
          增加
        </button>
        <button 
          onClick={() => dispatch({ type: 'DECREMENT' })}
          disabled={state.isLoading}
        >
          减少
        </button>
        <button 
          onClick={() => dispatch({ type: 'RESET' })}
          disabled={state.isLoading}
        >
          重置
        </button>
      </div>

      {state.history.length > 0 && (
        <div className="history">
          <h3>历史记录:</h3>
          <ul>
            {state.history.map((value, index) => (
              <li key={index}>{value}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}

与 useState 的对比

特性 useState useImmer
基本类型 ✅ 简单直接 ✅ 相同体验
对象更新 ❌ 需要手动展开 ✅ 直接修改
数组操作 ❌ 需要创建新数组 ✅ 使用原生方法
嵌套更新 ❌ 复杂且易错 ✅ 简单直观
性能 ✅ 优秀 ✅ 优秀(immer 优化)

注意事项

  1. 不要直接修改 draft 外的对象:immer 只能追踪在 draft 函数内的修改
  2. 返回值处理:如果更新函数返回一个值,它会替换整个状态
  3. 异步操作:在异步回调中使用 setState 时要注意闭包问题

总结

useImmer 是一个强大的状态管理工具,特别适合处理复杂的状态结构。它让不可变更新变得简单直观,同时保持了 React 的性能优势。无论是简单的计数器还是复杂的表单状态,useImmer 都能提供优雅的解决方案。

相关链接

相关推荐
小公主7 分钟前
还在等后端接口?vite-plugin-mock 教你前端自造接口跑起来!
前端
Ryan今天学习了吗7 分钟前
💥总结你需要知道有关 JSON 的一切
前端·javascript
1024小神9 分钟前
cocos控制角色玩家飞机只可以在一定范围内移动,不能越界
前端·javascript
晴殇i9 分钟前
Ultracite:告别 ESLint 和 Prettier,迎接 AI 时代的代码格式化新标准
前端·程序员·前端框架
拾光拾趣录9 分钟前
组合总和:深度解析电商促销系统的核心算法实践
前端·算法
三年三月10 分钟前
022-自定义顶点颜色实现渐变
前端·three.js
1024小神13 分钟前
cocos开发2d游戏的时候,模拟背景无限循环移动的思路和实现方法
前端·javascript
我想说一句14 分钟前
React性能优化:深入理解useMemo、useCallback与memo
前端·前端框架
顾辰呀23 分钟前
css flex 一行2个元素 不能挤压空间
前端·css·css3
潜心专研的小张同学24 分钟前
vue3实现高性能pdf预览器功能可行性方案及实践(pdfjs-dist5.x插件使用及自定义修改)
前端·vue.js