27
Mar

React 学习笔记(三):React的一些Hooks

avatar
ByKK
2025-03-27/8 Min Read
0
0
(AI总结) useState 和 useEffect 是构建组件的基础,但要构建可扩展、可维护且高性能的应用,还需要掌握 React 提供的更专业的 Hooks。本章将深入探讨用于状态共享、复杂逻辑管理、性能优化和逻辑复用的核心 Hooks。

useStateuseEffect 是基础。但在实际开发中,会遇到很多它们无法优雅解决的场景。这篇笔记记录剩下的几个核心 Hooks,以及它们在真实代码中的使用场景、坑点和技巧。

useContext - 终结 Prop Drilling

  • 核心目的: 干掉"道具钻探"(Prop Drilling)。当数据需要跨越多个层级时,用它来建立一个"直通车",避免中间组件无意义的透传。

  • 关键坑点与解决方案:性能问题 useContext 的大坑在于,只要 Providervalue 引用地址一变,所有消费这个 Context 的组件都会重渲染。

    问题示例: 下面的 Toolbar 组件根本没用到 counter,但因为 App 组件的 counter 状态变化导致重渲,value 属性被重新创建了一个新对象 {},导致 Toolbar 也跟着重渲。

    // 问题代码:不必要的重渲染
    import { createContext, useContext, useState } from 'react'
     
    const AppContext = createContext(null)
     
    function Toolbar() {
      // 这个组件理论上只关心 theme,但也被迫重渲了
      console.log('Toolbar re-rendered unnecessarily!')
      const { theme } = useContext(AppContext)
      return <div>Current theme: {theme}</div>
    }
     
    function App() {
      const [theme, setTheme] = useState('dark')
      const [counter, setCounter] = useState(0) // 一个无关的状态
     
      // 每次 App 重渲 (例如点击 button), value 都会是一个全新的对象
      const value = { theme, setTheme }
     
      return (
        <AppContext.Provider value={value}>
          <button onClick={() => setCounter(c => c + 1)}>Re-render App ({counter})</button>
          <Toolbar />
        </AppContext.Provider>
      )
    }

    解决方案:拆分 Context,或对 value 进行 useMemo 缓存 这是解决上述性能问题的最佳实践。把"不变的"和"常变的"数据分开。

    // 最佳实践:拆分 Context
    const ThemeContext = createContext(null);       // 用于传递 theme
    const ThemeDispatchContext = createContext(null); // 用于传递 setTheme
     
    function App() {
      const [theme, setTheme] = useState('dark');
      // ...
     
      return (
        <ThemeContext.Provider value={theme}>
          <ThemeDispatchContext.Provider value={setTheme}>
            {/* ... */}
          </ThemeDispatchContext.Provider>
        </ThemeContext.Provider>
      );
    }

    这样,只消费 ThemeDispatchContext 的组件就不会因为 theme 的变化而重渲。

useReducer - 替代繁杂的 useState

  • 使用时机: 当一个状态有多种更新逻辑,或者下一个状态依赖于复杂的前置状态时,用 useReducer 会让代码更清晰。

核心语法:

const [state, dispatch] = useReducer(reducer, initialState, init?)

实际使用示例:购物车状态管理

import { useReducer } from 'react';
 
// 定义状态类型
interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}
 
interface CartState {
  items: CartItem[];
  total: number;
}
 
// 定义操作类型
type CartAction = 
  | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
  | { type: 'REMOVE_ITEM'; payload: number }
  | { type: 'UPDATE_QUANTITY'; payload: { id: number; quantity: number } }
  | { type: 'CLEAR_CART' };
 
// 初始状态
const initialState: CartState = {
  items: [],
  total: 0
};
 
// Reducer 函数
function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find(item => item.id === action.payload.id);
      
      if (existingItem) {
        // 如果商品已存在,增加数量
        const updatedItems = state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
        
        return {
          items: updatedItems,
          total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
        };
      } else {
        // 如果商品不存在,添加新商品
        const newItem = { ...action.payload, quantity: 1 };
        const updatedItems = [...state.items, newItem];
        
        return {
          items: updatedItems,
          total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
        };
      }
    }
    
    case 'REMOVE_ITEM': {
      const updatedItems = state.items.filter(item => item.id !== action.payload);
      
      return {
        items: updatedItems,
        total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
      };
    }
    
    case 'UPDATE_QUANTITY': {
      const updatedItems = state.items.map(item =>
        item.id === action.payload.id
          ? { ...item, quantity: Math.max(0, action.payload.quantity) }
          : item
      ).filter(item => item.quantity > 0); // 移除数量为 0 的商品
      
      return {
        items: updatedItems,
        total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
      };
    }
    
    case 'CLEAR_CART':
      return initialState;
      
    default:
      return state;
  }
}
 
// 购物车组件
function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState);
 
  const addItem = (item: Omit<CartItem, 'quantity'>) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };
 
  const removeItem = (id: number) => {
    dispatch({ type: 'REMOVE_ITEM', payload: id });
  };
 
  const updateQuantity = (id: number, quantity: number) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  };
 
  const clearCart = () => {
    dispatch({ type: 'CLEAR_CART' });
  };
 
  return (
    <div className="p-4">
      <h2 className="text-xl font-bold mb-4">购物车</h2>
      
      {state.items.length === 0 ? (
        <p className="text-gray-500">购物车是空的</p>
      ) : (
        <>
          <div className="space-y-2 mb-4">
            {state.items.map(item => (
              <div key={item.id} className="flex justify-between items-center p-2 border rounded">
                <div>
                  <h3 className="font-medium">{item.name}</h3>
                  <p className="text-sm text-gray-600"{item.price}</p>
                </div>
                <div className="flex items-center gap-2">
                  <button
                    onClick={() => updateQuantity(item.id, item.quantity - 1)}
                    className="px-2 py-1 bg-gray-200 rounded"
                  >
                    -
                  </button>
                  <span className="w-8 text-center">{item.quantity}</span>
                  <button
                    onClick={() => updateQuantity(item.id, item.quantity + 1)}
                    className="px-2 py-1 bg-gray-200 rounded"
                  >
                    +
                  </button>
                  <button
                    onClick={() => removeItem(item.id)}
                    className="ml-2 text-red-500"
                  >
                    删除
                  </button>
                </div>
              </div>
            ))}
          </div>
          
          <div className="flex justify-between items-center">
            <p className="text-lg font-bold">总计: ¥{state.total}</p>
            <button
              onClick={clearCart}
              className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
            >
              清空购物车
            </button>
          </div>
        </>
      )}
      
      {/* 示例商品 */}
      <div className="mt-6">
        <h3 className="font-bold mb-2">添加商品:</h3>
        <div className="space-y-2">
          <button
            onClick={() => addItem({ id: 1, name: '苹果', price: 5 })}
            className="block w-full text-left p-2 border rounded hover:bg-gray-50"
          >
            苹果 - ¥5
          </button>
          <button
            onClick={() => addItem({ id: 2, name: '香蕉', price: 3 })}
            className="block w-full text-left p-2 border rounded hover:bg-gray-50"
          >
            香蕉 - ¥3
          </button>
        </div>
      </div>
    </div>
  );
}

useMemo & useCallback - 性能优化工具

  • 使用前提: 不要过早优化,不要滥用。只有在通过 Profiler 发现明确的性能瓶颈时才应该使用。

  • 核心坑点:在依赖项数组中使用了不稳定的引用(函数/对象) 这是导致 useMemo / useCallback 优化失效,甚至起反作用的根源。

    问题示例: 下面的 options 对象在 Parent 组件每次渲染时都会被重新创建,导致 useMemo 的依赖项 [options] 每次都变化,缓存永远不会命中。

    function Parent() {
      const [text, setText] = useState('');
     
      // 错误:options 在每次渲染时都是一个新对象
      const options = { delimiter: '-' }; 
     
      return <ChildComponent options={options} text={text} />;
    }
     
    function ChildComponent({ options, text }) {
      // 这里的 useMemo 完全无效,因为 options 引用总是在变
      const processedText = useMemo(() => {
        console.log('Expensive calculation running...');
        return text.split('').join(options.delimiter);
      }, [text, options]);
     
      return <div>{processedText}</div>;
    }

    解决方案: 将不稳定的依赖项本身也用 useMemouseCallback 缓存起来。

    function Parent() {
      const [text, setText] = useState('');
     
      // 正确:用 useMemo 缓存 options 对象,它的引用只有在 {} 内部值变化时才变
      // 在这个例子中,它永远不会变。
      const options = useMemo(() => ({ delimiter: '-' }), []); 
     
      return <ChildComponent options={options} text={text} />;
    }
    // ... ChildComponent 保持不变,现在它的 useMemo 可以正常工作了

    useCallback 的主要场景示例: useCallbackuseMemo 针对函数的语法糖。它的核心用途是配合 React.memo

    import { useState, useCallback, memo } from 'react';
     
    // 1.用 React.memo 包裹子组件,让它只有在 props 变化时才重渲
    const MyButton = memo(({ onClick }) => {
      console.log('MyButton re-rendered');
      return <button onClick={onClick}>Click me</button>;
    });
     
    function App() {
      const [count, setCount] = useState(0);
     
      // 2. 如果不用 useCallback,每次 App 重渲,这个函数都是新的
      // const handleClick = () => { console.log('clicked!'); };
     
      // 3. 用 useCallback 包裹后,handleClick 的引用就稳定了
      const handleClick = useCallback(() => {
        console.log('clicked!');
      }, []); // 空数组,这个函数永不改变
     
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(c => c + 1)}>Increment Count</button>
          {/* 当点击 Increment 时,App 重渲,但 MyButton 不会重渲 */}
          <MyButton onClick={handleClick} /> 
        </div>
      );
    }

自定义 Hooks - 逻辑复用的最佳实践

  • 核心思想: 抽离和复用带状态的逻辑。可以看作是 Vue 的 Composable 函数。

  • 关键认知:复用的是逻辑,不是状态 两个不同的组件同时调用 useMyHook(),它们会得到两套完全独立、互不相干的内部状态。

  • 实用技巧与示例:创建泛用的 useFetch Hook 这是一个非常实用的自定义 Hook,封装了数据请求的完整逻辑,包括 loading、error 和请求取消。

    import { useState, useEffect } from "react";
     
    function useFetch<T>(
      url: string,
      options?: RequestInit
    ): { data: T | null; loading: boolean; error: Error | null } {
      const [data, setData] = useState<T | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<Error | null>(null);
     
      useEffect(() => {
        // 每次请求前,重置状态
        setLoading(true);
        setData(null);
        setError(null);
     
        const controller = new AbortController();
        const signal = controller.signal;
     
        fetch(url, { ...options, signal })
          .then((res) => {
            if (!res.ok) {
              throw new Error(`HTTP error! status: ${res.status}`);
            }
            return res.json();
          })
          .then((data) => {
            setData(data);
          })
          .catch((err) => {
            if (err.name !== "AbortError") {
              setError(err);
            }
          })
          .finally(() => {
            setLoading(false);
          });
     
        // 清除函数:在组件卸载或 url 变化导致重新请求时,取消上一次的请求
        return () => {
          controller.abort();
        };
      }, [url]); // 仅当 url 变化时重新执行
     
      return { data, loading, error };
    }
     
    // 如何在组件中使用
    function UserProfile({ userId }) {
      const {
        data: user,
        loading,
        error,
      } = useFetch<{ name: string; email: string }>(
        `https://jsonplaceholder.typicode.com/users/${userId}`
      );
     
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error: {error.message}</p>;
     
      return (
        <div>
          <h1>{user?.name}</h1>
          <p>{user?.email}</p>
        </div>
      );
    }

实用技巧与最佳实践

1. 何时使用不同的 Hooks

  • useState: 简单的组件状态管理
  • useReducer: 复杂的状态逻辑,多种操作类型
  • useContext: 跨组件共享数据,避免 prop drilling
  • useMemo/useCallback: 性能优化,配合 React.memo 使用

2. 自定义 Hooks 的设计原则

  • 单一职责: 每个 Hook 只处理一个特定的逻辑
  • 可复用性: 设计通用的接口,避免硬编码
  • 类型安全: 使用 TypeScript 提供完整的类型支持
// 好的自定义 Hook 设计
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
 
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
 
  return [storedValue, setValue] as const;
}
 
// 使用示例
function UserSettings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'zh-CN');
 
  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">浅色主题</option>
        <option value="dark">深色主题</option>
      </select>
      
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="zh-CN">中文</option>
        <option value="en-US">English</option>
      </select>
    </div>
  );
}

3. 性能优化的最佳实践

  • 避免过早优化: 先确保功能正确,再考虑性能
  • 使用 React DevTools Profiler: 识别真正的性能瓶颈
  • 合理使用 React.memo: 配合 useCallback 和 useMemo 使用
// 性能优化的正确方式
import { memo, useCallback, useMemo } from 'react';
 
// 使用 memo 包装组件
const ExpensiveComponent = memo(({ data, onItemClick }) => {
  // 使用 useMemo 缓存计算结果
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      processed: item.value * 2
    }));
  }, [data]);
 
  return (
    <div>
      {processedData.map(item => (
        <div key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}: {item.processed}
        </div>
      ))}
    </div>
  );
});
 
function ParentComponent() {
  const [data, setData] = useState([]);
  
  // 使用 useCallback 缓存函数
  const handleItemClick = useCallback((id) => {
    console.log('Item clicked:', id);
  }, []);
 
  return (
    <ExpensiveComponent 
      data={data} 
      onItemClick={handleItemClick} 
    />
  );
}

本篇总结:

  • 状态管理进阶: 使用 useReducer 处理复杂状态逻辑,useContext 解决跨组件数据共享
  • 性能优化: 合理使用 useMemouseCallback,避免不必要的重新渲染
  • 逻辑复用: 通过自定义 Hooks 封装和复用复杂逻辑
  • 最佳实践: 遵循单一职责、类型安全、性能优化的原则

掌握这些高级 Hooks,你就能构建出更加健壮、高效和可维护的 React 应用!