React Hooks彻底改变了我们编写React组件的方式,让函数组件拥有了类组件的所有能力,同时还带来了更简洁的代码组织方式和更好的逻辑复用。本文将深入探讨Hooks的内部原理、最佳实践和性能优化策略。

Hooks基础回顾

基本Hooks类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useLayoutEffect, useImperativeHandle } from 'react'

// useState - 状态管理
const [count, setCount] = useState(0)

// useEffect - 副作用处理
useEffect(() => {
  document.title = `Count: ${count}`
  return () => {
    // 清理函数
  }
}, [count])

// useContext - Context消费
const theme = useContext(ThemeContext)

// useReducer - 复杂状态管理
const [state, dispatch] = useReducer(reducer, initialState)

// useCallback - 函数缓存
const handleClick = useCallback(() => {
  setCount(count + 1)
}, [count])

// useMemo - 值缓存
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(count)
}, [count])

// useRef - 引用管理
const inputRef = useRef(null)

// useLayoutEffect - 同步副作用
useLayoutEffect(() => {
  // 在DOM更新后同步执行
})

// useImperativeHandle - 暴露实例方法
useImperativeHandle(ref, () => ({
  focus: () => inputRef.current.focus()
}), [])

Hooks内部原理

1. Hooks的数据结构

React使用链表来存储Hooks的状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 简化的Hooks内部实现
let currentlyRenderingComponent = null
let workInProgressHook = null
let currentHook = null

// Hook节点结构
const hook = {
  memoizedState: null,  // Hook的当前状态
  baseState: null,     // 基础状态
  baseQueue: null,     // 基础更新队列
  queue: null,         // 更新队列
  next: null          // 下一个Hook
}

// Hook初始化
function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  }

  if (workInProgressHook === null) {
    currentlyRenderingComponent.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return workInProgressHook
}

// Hook更新
function updateWorkInProgressHook() {
  let nextCurrentHook
  if (currentHook === null) {
    const current = currentlyRenderingComponent.alternate.memoizedState
    nextCurrentHook = current.next
  } else {
    nextCurrentHook = currentHook.next
  }

  let nextWorkInProgressHook
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingComponent.memoizedState
  } else {
    nextWorkInProgressHook = workInProgressHook.next
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook
    nextWorkInProgressHook = createHook(nextCurrentHook)
  } else {
    // 没有对应的Hook,可能是新增的Hook
    workInProgressHook = createHook(null)
  }

  currentHook = nextCurrentHook
  return workInProgressHook
}

2. useState的实现原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// useState的简化实现
function useState(initialState) {
  return useReducer(
    (state, action) => {
      return typeof action === 'function' ? action(state) : action
    },
    typeof initialState === 'function' ? initialState() : initialState
  )
}

// useReducer的简化实现
function useReducer(reducer, initialState, init) {
  const hook = updateWorkInProgressHook()
  const queue = hook.queue

  // 执行更新队列
  if (numberOfLanes > 1) {
    return hook.memoizedState
  }

  if (queue !== null) {
    let newState = hook.baseState
    let update = queue.first
    let didSkip = false

    while (update !== null) {
      const action = update.action
      if (shouldSkipUpdate(update.lane)) {
        didSkip = true
      } else {
        newState = reducer(newState, action)
      }
      update = update.next
    }

    if (!didSkip) {
      hook.baseState = newState
      hook.baseQueue = null
    }

    hook.memoizedState = newState
  }

  const dispatch = dispatchAction.bind(null, currentlyRenderingComponent, queue)
  return [hook.memoizedState, dispatch]
}

function dispatchAction(component, queue, action) {
  const update = {
    lane,
    action,
    next: null
  }

  const pending = queue.pending
  if (pending === null) {
    update.next = update
  } else {
    update.next = pending.next
    pending.next = update
  }
  queue.pending = update

  scheduleUpdateOnFiber(component, lane)
}

3. useEffect的实现原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function useEffect(create, deps) {
  return mountEffectImpl(
    UpdateEffect | PassEffect,
    HookPassive,
    create,
    deps
  )
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = mountWorkInProgressHook()
  const nextDeps = deps === undefined ? null : deps
  currentlyRenderingComponent.flags |= fiberFlags
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps)
}

function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null
  }

  let componentUpdateQueue = currentlyRenderingComponent.updateQueue
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue()
    currentlyRenderingComponent.updateQueue = componentUpdateQueue
    componentUpdateQueue.lastEffect = effect.next = effect
  } else {
    const lastEffect = componentUpdateQueue.lastEffect
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect
    } else {
      const firstEffect = lastEffect.next
      lastEffect.next = effect
      effect.next = firstEffect
      componentUpdateQueue.lastEffect = effect
    }
  }
  return effect
}

自定义Hooks开发

1. 基础自定义Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// useCounter - 计数器Hook
function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue)

  const increment = useCallback(() => {
    setCount(prevCount => prevCount + step)
  }, [step])

  const decrement = useCallback(() => {
    setCount(prevCount => prevCount - step)
  }, [step])

  const reset = useCallback(() => {
    setCount(initialValue)
  }, [initialValue])

  return { count, increment, decrement, reset }
}

// 使用示例
function Counter() {
  const { count, increment, decrement, reset } = useCounter(0, 2)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+2</button>
      <button onClick={decrement}>-2</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

2. 复杂业务逻辑Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// useApi - API请求Hook
function useApi(url, options = {}) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  const fetchData = useCallback(async () => {
    try {
      setLoading(true)
      setError(null)
      const response = await fetch(url, options)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const result = await response.json()
      setData(result)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }, [url, options])

  useEffect(() => {
    fetchData()
  }, [fetchData])

  const refetch = useCallback(() => {
    fetchData()
  }, [fetchData])

  return { data, loading, error, refetch }
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useApi(`/api/users/${userId}`)

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>
  if (!user) return <div>No user found</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={refetch}>Refresh</button>
    </div>
  )
}

3. localStorage同步Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// useLocalStorage - localStorage同步Hook
function useLocalStorage(key, initialValue) {
  // 获取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error)
      return initialValue
    }
  })

  // 设置值到localStorage和state
  const setValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error)
    }
  }, [key, storedValue])

  // 监听其他tab的变化
  useEffect(() => {
    const handleStorageChange = (e) => {
      if (e.key === key) {
        try {
          setStoredValue(JSON.parse(e.newValue))
        } catch (error) {
          console.error(`Error parsing storage value for key "${key}":`, error)
        }
      }
    }

    window.addEventListener('storage', handleStorageChange)
    return () => window.removeEventListener('storage', handleStorageChange)
  }, [key])

  return [storedValue, setValue]
}

// 使用示例
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [language, setLanguage] = useLocalStorage('language', 'en')

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="en">English</option>
        <option value="zh">中文</option>
      </select>
    </div>
  )
}

4. 防抖Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// useDebounce - 防抖Hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => {
      clearTimeout(handler)
    }
  }, [value, delay])

  return debouncedValue
}

// useDebouncedCallback - 防抖回调Hook
function useDebouncedCallback(callback, delay, deps = []) {
  const callbackRef = useRef(callback)
  const timeoutRef = useRef(null)

  useEffect(() => {
    callbackRef.current = callback
  }, [callback])

  return useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }

    timeoutRef.current = setTimeout(() => {
      callbackRef.current(...args)
    }, delay)
  }, [delay, ...deps])
}

// 使用示例
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 500)

  const handleSearch = useDebouncedCallback((term) => {
    // 执行搜索API调用
    console.log('Searching for:', term)
  }, 500)

  useEffect(() => {
    if (debouncedSearchTerm) {
      handleSearch(debouncedSearchTerm)
    }
  }, [debouncedSearchTerm, handleSearch])

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  )
}

性能优化策略

1. useCallback和useMemo的正确使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ❌ 过度使用useCallback
function BadExample({ items, onItemClick }) {
  const handleClick = useCallback((item) => {
    onItemClick(item.id)
  }, [onItemClick]) // 每次onItemClick变化都创建新函数

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => handleClick(item)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
}

// ✅ 合理使用useCallback
function GoodExample({ items, onItemClick }) {
  const handleClick = useCallback((itemId) => {
    onItemClick(itemId)
  }, [onItemClick])

  return (
    <ul>
      {items.map(item => (
        <Item
          key={item.id}
          item={item}
          onClick={handleClick}
        />
      ))}
    </ul>
  )
}

// 子组件
const Item = React.memo(({ item, onClick }) => (
  <li onClick={() => onClick(item.id)}>
    {item.name}
  </li>
))

2. 状态优化策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ❌ 不必要的状态分离
function BadCounter() {
  const [count, setCount] = useState(0)
  const [double, setDouble] = useState(0)

  useEffect(() => {
    setDouble(count * 2)
  }, [count])

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {double}</p>
    </div>
  )
}

// ✅ 使用派生状态
function GoodCounter() {
  const [count, setCount] = useState(0)
  const double = useMemo(() => count * 2, [count])

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {double}</p>
    </div>
  )
}

// ✅ 复杂状态的优化
function useComplexState(initialState) {
  const [state, setState] = useState(initialState)

  const updateState = useCallback((updater) => {
    setState(prevState => {
      const newState = typeof updater === 'function' ? updater(prevState) : updater
      return { ...prevState, ...newState }
    })
  }, [])

  return [state, updateState]
}

3. 组件渲染优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ✅ 使用React.memo优化函数组件
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  )
}, (prevProps, nextProps) => {
  // 自定义比较函数
  return prevProps.data.length === nextProps.data.length
})

// ✅ 使用useMemo缓存复杂计算
function DataProcessor({ items }) {
  const processedData = useMemo(() => {
    return items
      .filter(item => item.active)
      .map(item => ({
        ...item,
        value: complexCalculation(item)
      }))
      .sort((a, b) => a.value - b.value)
  }, [items])

  return <DataList data={processedData} />
}

// ✅ 使用useRef避免不必要的重新渲染
function Timer() {
  const [count, setCount] = useState(0)
  const intervalRef = useRef(null)

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1)
    }, 1000)

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current)
      }
    }
  }, [])

  return <div>Count: {count}</div>
}

常见陷阱和解决方案

1. Hooks调用规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ❌ 在条件语句中调用Hook
function BadComponent({ condition }) {
  if (condition) {
    const [state, setState] = useState(0) // 错误!
  }

  useEffect(() => {
    // ...
  }, []) // 错误!Hook调用顺序不稳定

  return <div>Bad Component</div>
}

// ✅ 正确的Hook调用
function GoodComponent({ condition }) {
  const [state, setState] = useState(0)

  useEffect(() => {
    if (condition) {
      // 在effect内部使用条件逻辑
      console.log('Condition is true')
    }
  }, [condition])

  return <div>Good Component</div>
}

2. 闭包陷阱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// ❌ 闭包陷阱
function CounterWithBug() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1) // 总是从0开始
    }, 1000)

    return () => clearInterval(interval)
  }, [count]) // 每次count变化都重新创建interval

  return <div>Count: {count}</div>
}

// ✅ 使用函数式更新
function CounterWithFix() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1) // 使用函数式更新
    }, 1000)

    return () => clearInterval(interval)
  }, []) // 只在组件挂载时创建一次

  return <div>Count: {count}</div>
}

// ✅ 使用useRef存储可变值
function CounterWithRef() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)

  useEffect(() => {
    countRef.current = count
  })

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(countRef.current + 1)
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return <div>Count: {count}</div>
}

3. 依赖数组陷阱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// ❌ 缺少依赖项
function BadExample({ id }) {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetchData(id).then(setData)
  }, []) // 缺少id依赖,当id变化时不会重新获取数据

  return <div>{data?.name}</div>
}

// ✅ 正确的依赖项
function GoodExample({ id }) {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetchData(id).then(setData)
  }, [id]) // 包含所有外部依赖

  return <div>{data?.name}</div>
}

// ✅ 使用自定义Hook管理复杂依赖
function useApiData(url, dependencies = []) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    let isMounted = true

    setLoading(true)
    fetch(url)
      .then(response => response.json())
      .then(result => {
        if (isMounted) {
          setData(result)
        }
      })
      .catch(error => {
        if (isMounted) {
          console.error('API Error:', error)
        }
      })
      .finally(() => {
        if (isMounted) {
          setLoading(false)
        }
      })

    return () => {
      isMounted = false
    }
  }, [url, ...dependencies])

  return { data, loading }
}

高级Hooks模式

1. 状态机Hook

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
// useStateMachine - 状态机Hook
function useStateMachine(initialState, transitions) {
  const [currentState, setCurrentState] = useState(initialState)
  const [context, setContext] = useState({})

  const transition = useCallback((action, payload) => {
    setCurrentState(prevState => {
      const stateConfig = transitions[prevState]
      if (!stateConfig || !stateConfig.on || !stateConfig.on[action]) {
        console.warn(`No transition defined for action ${action} in state ${prevState}`)
        return prevState
      }

      const transitionConfig = stateConfig.on[action]
      const nextState = typeof transitionConfig.target === 'function'
        ? transitionConfig.target(prevState, payload)
        : transitionConfig.target

      if (transitionConfig.action) {
        transitionConfig.action(prevState, payload, context)
      }

      return nextState
    })
  }, [transitions, context])

  const can = useCallback((action) => {
    const stateConfig = transitions[currentState]
    return stateConfig && stateConfig.on && stateConfig.on[action]
  }, [currentState, transitions])

  return { currentState, transition, can, context, setContext }
}

// 使用示例 - 登录状态机
const loginMachine = {
  idle: {
    on: {
      login: {
        target: 'loading',
        action: (state, payload) => ({ ...state, username: payload.username })
      }
    }
  },
  loading: {
    on: {
      success: 'authenticated',
      error: 'error'
    }
  },
  authenticated: {
    on: {
      logout: 'idle'
    }
  },
  error: {
    on: {
      retry: 'loading',
      cancel: 'idle'
    }
  }
}

function LoginComponent() {
  const { currentState, transition, can } = useStateMachine('idle', loginMachine)

  const handleLogin = async (username, password) => {
    transition('login', { username })

    try {
      await performLogin(username, password)
      transition('success')
    } catch (error) {
      transition('error', { error: error.message })
    }
  }

  return (
    <div>
      <p>Current state: {currentState}</p>
      {currentState === 'idle' && (
        <button onClick={() => handleLogin('user', 'pass')}>Login</button>
      )}
      {currentState === 'loading' && <p>Loading...</p>}
      {currentState === 'authenticated' && (
        <div>
          <p>Authenticated!</p>
          <button onClick={() => transition('logout')}>Logout</button>
        </div>
      )}
      {currentState === 'error' && (
        <div>
          <p>Error occurred</p>
          <button onClick={() => transition('retry')}>Retry</button>
          <button onClick={() => transition('cancel')}>Cancel</button>
        </div>
      )}
    </div>
  )
}

2. 虚拟列表Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// useVirtualList - 虚拟列表Hook
function useVirtualList(items, itemHeight, containerHeight) {
  const [scrollTop, setScrollTop] = useState(0)

  const visibleStart = Math.floor(scrollTop / itemHeight)
  const visibleEnd = Math.min(
    visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  )

  const visibleItems = useMemo(() => {
    return items.slice(visibleStart, visibleEnd).map((item, index) => ({
      ...item,
      index: visibleStart + index
    }))
  }, [items, visibleStart, visibleEnd])

  const totalHeight = items.length * itemHeight
  const offsetY = visibleStart * itemHeight

  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop)
  }, [])

  return {
    visibleItems,
    totalHeight,
    offsetY,
    handleScroll
  }
}

// 使用示例
function VirtualList({ items, itemHeight = 50, containerHeight = 400 }) {
  const {
    visibleItems,
    totalHeight,
    offsetY,
    handleScroll
  } = useVirtualList(items, itemHeight, containerHeight)

  return (
    <div style={{ height: containerHeight, overflow: 'auto' }} onScroll={handleScroll}>
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div
              key={item.id}
              style={{
                height: itemHeight,
                borderBottom: '1px solid #eee',
                display: 'flex',
                alignItems: 'center',
                padding: '0 16px',
                background: index % 2 === 0 ? '#f9f9f9' : 'white'
              }}
            >
              {item.name}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

3. 表单管理Hook

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// useForm - 表单管理Hook
function useForm(initialValues, validationSchema = {}) {
  const [values, setValues] = useState(initialValues)
  const [errors, setErrors] = useState({})
  const [touched, setTouched] = useState({})

  const setValue = useCallback((name, value) => {
    setValues(prev => ({ ...prev, [name]: value }))
  }, [])

  const setError = useCallback((name, error) => {
    setErrors(prev => ({ ...prev, [name]: error }))
  }, [])

  const setTouched = useCallback((name, isTouched = true) => {
    setTouched(prev => ({ ...prev, [name]: isTouched }))
  }, [])

  const handleChange = useCallback((e) => {
    const { name, value, type, checked } = e.target
    const fieldValue = type === 'checkbox' ? checked : value

    setValue(name, fieldValue)

    if (touched[name]) {
      validateField(name, fieldValue)
    }
  }, [setValue, touched, validationSchema])

  const validateField = useCallback((name, value) => {
    if (!validationSchema[name]) {
      setError(name, '')
      return true
    }

    const validator = validationSchema[name]
    const result = validator(value)

    if (typeof result === 'string') {
      setError(name, result)
      return false
    } else if (result === false) {
      setError(name, 'Invalid field')
      return false
    } else {
      setError(name, '')
      return true
    }
  }, [validationSchema, setError])

  const handleBlur = useCallback((e) => {
    const { name, value } = e.target
    setTouched(name, true)
    validateField(name, value)
  }, [setTouched, validateField])

  const validate = useCallback(() => {
    let isValid = true
    const newErrors = {}

    Object.keys(validationSchema).forEach(name => {
      if (!validateField(name, values[name])) {
        isValid = false
        newErrors[name] = errors[name]
      }
    })

    setErrors(newErrors)
    return isValid
  }, [validationSchema, values, errors, validateField])

  const reset = useCallback(() => {
    setValues(initialValues)
    setErrors({})
    setTouched({})
  }, [initialValues])

  return {
    values,
    errors,
    touched,
    setValue,
    setError,
    setTouched,
    handleChange,
    handleBlur,
    validate,
    reset,
    isValid: Object.keys(errors).every(key => !errors[key])
  }
}

// 使用示例
function LoginForm() {
  const validationSchema = {
    email: (value) => {
      if (!value) return 'Email is required'
      if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid'
      return true
    },
    password: (value) => {
      if (!value) return 'Password is required'
      if (value.length < 6) return 'Password must be at least 6 characters'
      return true
    }
  }

  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    validate,
    reset,
    isValid
  } = useForm(
    { email: '', password: '' },
    validationSchema
  )

  const handleSubmit = (e) => {
    e.preventDefault()
    if (validate()) {
      console.log('Form submitted:', values)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Email:</label>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && (
          <span style={{ color: 'red' }}>{errors.email}</span>
        )}
      </div>
      <div>
        <label>Password:</label>
        <input
          type="password"
          name="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && (
          <span style={{ color: 'red' }}>{errors.password}</span>
        )}
      </div>
      <button type="submit" disabled={!isValid}>
        Submit
      </button>
      <button type="button" onClick={reset}>
        Reset
      </button>
    </form>
  )
}

Hooks测试策略

1. 测试自定义Hooks

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
// 使用 @testing-library/react-hooks
import { renderHook, act } from '@testing-library/react-hooks'

// 测试useCounter Hook
import { useCounter } from './useCounter'

describe('useCounter', () => {
  test('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter())

    expect(result.current.count).toBe(0)
    expect(typeof result.current.increment).toBe('function')
    expect(typeof result.current.decrement).toBe('function')
    expect(typeof result.current.reset).toBe('function')
  })

  test('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(5))

    expect(result.current.count).toBe(5)
  })

  test('should increment count', () => {
    const { result } = renderHook(() => useCounter(0, 1))

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

  test('should decrement count', () => {
    const { result } = renderHook(() => useCounter(5, 2))

    act(() => {
      result.current.decrement()
    })

    expect(result.current.count).toBe(3)
  })

  test('should reset count', () => {
    const { result } = renderHook(() => useCounter(0))

    act(() => {
      result.current.increment()
      result.current.increment()
    })
    expect(result.current.count).toBe(2)

    act(() => {
      result.current.reset()
    })
    expect(result.current.count).toBe(0)
  })
})

// 测试useApi Hook
import { useApi } from './useApi'

// Mock fetch
global.fetch = jest.fn()

describe('useApi', () => {
  beforeEach(() => {
    fetch.mockClear()
  })

  test('should fetch data on mount', async () => {
    const mockData = { id: 1, name: 'Test' }
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockData
    })

    const { result, waitForNextUpdate } = renderHook(() => useApi('/api/test'))

    expect(result.current.loading).toBe(true)
    expect(result.current.data).toBe(null)
    expect(result.current.error).toBe(null)

    await waitForNextUpdate()

    expect(result.current.loading).toBe(false)
    expect(result.current.data).toEqual(mockData)
    expect(result.current.error).toBe(null)
  })

  test('should handle fetch error', async () => {
    fetch.mockRejectedValueOnce(new Error('Network error'))

    const { result, waitForNextUpdate } = renderHook(() => useApi('/api/test'))

    await waitForNextUpdate()

    expect(result.current.loading).toBe(false)
    expect(result.current.data).toBe(null)
    expect(result.current.error).toBe('Network error')
  })
})

2. 集成测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 测试使用Hooks的组件
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { Counter } from './Counter'

describe('Counter Component', () => {
  test('should render initial count', () => {
    render(<Counter />)
    expect(screen.getByText('Count: 0')).toBeInTheDocument()
  })

  test('should increment count when button clicked', async () => {
    render(<Counter />)

    const incrementButton = screen.getByText('+1')
    fireEvent.click(incrementButton)

    await waitFor(() => {
      expect(screen.getByText('Count: 1')).toBeInTheDocument()
    })
  })
})

总结

React Hooks为我们带来了强大的能力,但也需要正确理解和使用:

核心要点

  1. 理解原理:Hooks基于链表结构,调用顺序必须一致
  2. 合理优化:避免过度优化,正确使用useCallback和useMemo
  3. 避免陷阱:注意闭包、依赖数组、调用规则等问题
  4. 自定义Hooks:提取逻辑复用,保持组件简洁
  5. 测试策略:充分测试自定义Hooks的行为

最佳实践

  • 遵循Hooks规则
  • 合理使用性能优化Hooks
  • 提取复用逻辑为自定义Hooks
  • 编写可测试的Hooks
  • 注意内存泄漏和清理

通过深入理解Hooks的原理和最佳实践,你可以编写出更简洁、更高效、更易维护的React组件。Hooks不仅是技术工具,更是构建现代化React应用的设计思维。