React Hooks 最佳实践与性能优化

(7 个月前)
Nick Chen
约 5 分钟

React Hooks 已经成为现代 React 开发的标准,但正确使用它们需要深入理解其工作原理。本文将分享一些高级技巧和最佳实践。

基础 Hooks 深度解析

useState 的高级用法

// 函数式更新,避免闭包陷阱
const [count, setCount] = useState(0);

// 不好的做法
const increment = () => {
  setTimeout(() => {
    setCount(count + 1); // 可能使用过期的 count 值
  }, 1000);
};

// 好的做法
const increment = () => {
  setTimeout(() => {
    setCount(prevCount => prevCount + 1); // 总是使用最新值
  }, 1000);
};

// 惰性初始化,避免重复计算
const [expensiveValue, setExpensiveValue] = useState(() => {
  return computeExpensiveValue();
});

useEffect 的细节处理

// 清理函数的重要性
useEffect(() => {
  const subscription = subscribe()

  return () => {
    subscription.unsubscribe()
  }
}, [])

// 依赖数组的优化
const [userId, setUserId] = useState(null)
const [userData, setUserData] = useState(null)

useEffect(() => {
  if (!userId)
    return

  let cancelled = false

  async function fetchUser() {
    try {
      const data = await api.getUser(userId)
      if (!cancelled) {
        setUserData(data)
      }
    }
    catch (error) {
      if (!cancelled) {
        console.error('Failed to fetch user:', error)
      }
    }
  }

  fetchUser()

  return () => {
    cancelled = true
  }
}, [userId])

自定义 Hooks 设计模式

数据获取 Hook

function useAsync(asyncFunction, dependencies = []) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  })

  useEffect(() => {
    let cancelled = false

    setState(prev => ({ ...prev, loading: true, error: null }))

    asyncFunction()
      .then((data) => {
        if (!cancelled) {
          setState({ data, loading: false, error: null })
        }
      })
      .catch((error) => {
        if (!cancelled) {
          setState({ data: null, loading: false, error })
        }
      })

    return () => {
      cancelled = true
    }
  }, dependencies)

  return state
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error } = useAsync(
    () => api.getUser(userId),
    [userId]
  )

  if (loading)
    return <div>Loading...</div>
  if (error) {
    return (
      <div>
        Error:
        {error.message}
      </div>
    )
  }

  return (
    <div>
      Hello,
      {user?.name}
      !
    </div>
  )
}

本地存储 Hook

function useLocalStorage(key, initialValue) {
  // 使用函数来避免在每次渲染时访问 localStorage
  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
    }
  })

  const setValue = useCallback((value) => {
    try {
      // 允许 value 是一个函数,类似于 useState
      const valueToStore = typeof value === '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])

  return [storedValue, setValue]
}

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

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme:
      {' '}
      {theme}
    </button>
  )
}

防抖 Hook

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)

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

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

  return debouncedValue
}

// 搜索组件示例
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('')
  const debouncedSearchTerm = useDebounce(searchTerm, 300)

  const { data: results, loading } = useAsync(
    () => debouncedSearchTerm ? api.search(debouncedSearchTerm) : Promise.resolve([]),
    [debouncedSearchTerm]
  )

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      {loading && <div>Searching...</div>}
      <ul>
        {results?.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}

性能优化技巧

useMemo 和 useCallback 的正确使用

function ExpensiveComponent({ items, onItemClick, filter }) {
  // 缓存昂贵的计算
  const filteredItems = useMemo(() => {
    return items.filter(item => item.category === filter)
  }, [items, filter])

  // 缓存事件处理函数,避免子组件不必要的重渲染
  const handleItemClick = useCallback((itemId) => {
    onItemClick(itemId, filter)
  }, [onItemClick, filter])

  return (
    <div>
      {filteredItems.map(item => (
        <ItemComponent
          key={item.id}
          item={item}
          onClick={handleItemClick}
        />
      ))}
    </div>
  )
}

// 使用 memo 包装子组件
const ItemComponent = React.memo(({ item, onClick }) => {
  return (
    <div onClick={() => onClick(item.id)}>
      {item.name}
    </div>
  )
})

状态结构优化

// 不好的做法:频繁的状态更新
function BadCounter() {
  const [count, setCount] = useState(0)
  const [lastUpdated, setLastUpdated] = useState(Date.now())

  const increment = () => {
    setCount(c => c + 1)
    setLastUpdated(Date.now()) // 两次 setState 调用
  }

  return (
    <div>
      Count:
      {count}
      {' '}
      (Updated:
      {lastUpdated}
      )
    </div>
  )
}

// 好的做法:合并相关状态
function GoodCounter() {
  const [state, setState] = useState({
    count: 0,
    lastUpdated: Date.now()
  })

  const increment = () => {
    setState(prev => ({
      count: prev.count + 1,
      lastUpdated: Date.now()
    }))
  }

  return (
    <div>
      Count:
      {state.count}
      {' '}
      (Updated:
      {state.lastUpdated}
      )
    </div>
  )
}

useReducer 用于复杂状态管理

const initialState = {
  items: [],
  loading: false,
  error: null,
  filter: 'all'
}

function todosReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, items: action.payload }
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload }
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] }
    case 'TOGGLE_ITEM':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        )
      }
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    default:
      return state
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todosReducer, initialState)

  const addTodo = useCallback((text) => {
    dispatch({
      type: 'ADD_ITEM',
      payload: { id: Date.now(), text, completed: false }
    })
  }, [])

  const toggleTodo = useCallback((id) => {
    dispatch({ type: 'TOGGLE_ITEM', payload: id })
  }, [])

  // 渲染逻辑...
}

常见陷阱和解决方案

闭包陷阱

// 问题:useEffect 中的闭包陷阱
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1); // count 永远是 0
    }, 1000);

    return () => clearInterval(interval);
  }, []); // 空依赖数组导致 count 被"困住"

  return <div>{count}</div>;
}

// 解决方案 1:使用函数式更新
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1); // 使用最新的 count
    }, 1000);

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

  return <div>{count}</div>;
}

// 解决方案 2:使用 useRef
function Timer() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;

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

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

  return <div>{count}</div>;
}

依赖数组优化

// 问题:对象和函数作为依赖
function UserProfile({ userId, config }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId, config).then(setUser);
  }, [userId, config]); // config 对象可能每次都是新的

  // ...
}

// 解决方案:使用 useMemo 和 useCallback
function UserProfile({ userId, config }) {
  const [user, setUser] = useState(null);

  const stableConfig = useMemo(() => config, [
    config.apiUrl,
    config.timeout,
    // 只依赖真正变化的属性
  ]);

  useEffect(() => {
    fetchUser(userId, stableConfig).then(setUser);
  }, [userId, stableConfig]);

  // ...
}

测试 Hooks

import { act, renderHook } from '@testing-library/react'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should increment counter', () => {
    const { result } = renderHook(() => useCounter(0))

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

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

  it('should reset counter', () => {
    const { result } = renderHook(() => useCounter(5))

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

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

总结

React Hooks 为我们提供了强大而灵活的状态管理和副作用处理能力。通过遵循最佳实践,我们可以:

  1. 避免常见陷阱:正确处理依赖数组和闭包问题
  2. 优化性能:合理使用 memoization 和状态结构
  3. 提高复用性:设计高质量的自定义 Hooks
  4. 增强可测试性:编写清晰、可测试的 Hook 逻辑

记住,Hooks 的力量在于组合,通过组合小而专注的 Hooks,我们可以构建出强大而maintainable的应用程序。


参考资源: