React Hooks 详解

什么是 Hooks

Hooks 是 React 16.8 引入的新特性,它允许你在函数组件中使用状态和其他 React 特性,而无需编写类组件。

内置 Hooks

useState - 状态管理

基本用法

import React, { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

函数式更新

function Counter() {
  const [count, setCount] = useState(0);
 
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };
 
  const incrementBy = (amount) => {
    setCount(prevCount => prevCount + amount);
  };
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={() => incrementBy(5)}>+5</button>
    </div>
  );
}

对象状态更新

function Form() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: 0
  });
 
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
 
  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
    </form>
  );
}

useEffect - 副作用处理

基本用法

import React, { useState, useEffect } from 'react';
 
function Example() {
  const [count, setCount] = useState(0);
 
  // 每次渲染后执行
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

只在挂载时执行

function Example() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    console.log('Component mounted');
  }, []); // 空依赖数组
 
  return <div>Count: {count}</div>;
}

依赖项变化时执行

function Example() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
 
  useEffect(() => {
    console.log('Count changed:', count);
  }, [count]); // 只有 count 变化时执行
 
  useEffect(() => {
    console.log('Name or count changed');
  }, [name, count]); // name 或 count 变化时执行
 
  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
}

清理函数

function Timer() {
  const [seconds, setSeconds] = useState(0);
 
  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
 
    // 清理函数
    return () => {
      clearInterval(timer);
    };
  }, []);
 
  return <div>Seconds: {seconds}</div>;
}

数据获取

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    let cancelled = false;
 
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        
        if (!cancelled) {
          setUser(userData);
        }
      } catch (error) {
        if (!cancelled) {
          console.error('Error fetching user:', error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };
 
    fetchUser();
 
    return () => {
      cancelled = true;
    };
  }, [userId]);
 
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;
 
  return <div>{user.name}</div>;
}

useContext - 上下文共享

创建 Context

import React, { createContext, useContext } from 'react';
 
const ThemeContext = createContext();
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
 
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
 
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
 
  return (
    <button 
      onClick={toggleTheme}
      style={{ 
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff'
      }}
    >
      Toggle Theme
    </button>
  );
}
 
// 使用
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

useReducer - 复杂状态管理

基本用法

import React, { useReducer } from 'react';
 
const initialState = { count: 0 };
 
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error();
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
 
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

复杂状态管理

const initialState = {
  todos: [],
  filter: 'all',
  loading: false
};
 
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    case 'SET_LOADING':
      return {
        ...state,
        loading: action.payload
      };
    default:
      return state;
  }
}
 
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
 
  const addTodo = (text) => {
    dispatch({
      type: 'ADD_TODO',
      payload: { id: Date.now(), text, completed: false }
    });
  };
 
  const toggleTodo = (id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };
 
  return (
    <div>
      {/* 组件内容 */}
    </div>
  );
}

useMemo - 性能优化

import React, { useState, useMemo } from 'react';
 
function ExpensiveComponent({ items, filter }) {
  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value...');
    return items
      .filter(item => item.category === filter)
      .reduce((sum, item) => sum + item.value, 0);
  }, [items, filter]); // 只有 items 或 filter 变化时才重新计算
 
  return <div>Total value: {expensiveValue}</div>;
}
 
function App() {
  const [items, setItems] = useState([
    { id: 1, category: 'A', value: 10 },
    { id: 2, category: 'B', value: 20 },
    { id: 3, category: 'A', value: 30 }
  ]);
  const [filter, setFilter] = useState('A');
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveComponent items={items} filter={filter} />
    </div>
  );
}

useCallback - 函数缓存

import React, { useState, useCallback, memo } from 'react';
 
const ChildComponent = memo(({ onClick, name }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>{name}</button>;
});
 
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
 
  // 使用 useCallback 缓存函数
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 空依赖数组,函数永远不会重新创建
 
  const handleNameChange = useCallback((e) => {
    setName(e.target.value);
  }, []);
 
  return (
    <div>
      <input value={name} onChange={handleNameChange} />
      <ChildComponent onClick={handleClick} name="Click me" />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
}

useRef - DOM 引用和可变值

DOM 引用

import React, { useRef, useEffect } from 'react';
 
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
 
  const onButtonClick = () => {
    inputEl.current.focus();
  };
 
  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}

保存可变值

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef();
 
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
 
    return () => clearInterval(intervalRef.current);
  }, []);
 
  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

自定义 Hooks

数据获取 Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    let cancelled = false;
 
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch(url);
        const result = await response.json();
        
        if (!cancelled) {
          setData(result);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };
 
    fetchData();
 
    return () => {
      cancelled = true;
    };
  }, [url]);
 
  return { data, loading, error };
}
 
// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>User not found</div>;
 
  return <div>{user.name}</div>;
}

本地存储 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);
      return initialValue;
    }
  });
 
  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };
 
  return [storedValue, setValue];
}
 
// 使用
function App() {
  const [name, setName] = useLocalStorage('name', '');
 
  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
    </div>
  );
}

表单处理 Hook

function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
 
  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
    
    // 清除对应字段的错误
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };
 
  const handleSubmit = (onSubmit) => (e) => {
    e.preventDefault();
    onSubmit(values);
  };
 
  const setError = (field, message) => {
    setErrors(prev => ({
      ...prev,
      [field]: message
    }));
  };
 
  const reset = () => {
    setValues(initialValues);
    setErrors({});
  };
 
  return {
    values,
    errors,
    handleChange,
    handleSubmit,
    setError,
    reset
  };
}
 
// 使用
function ContactForm() {
  const { values, errors, handleChange, handleSubmit, setError } = useForm({
    name: '',
    email: '',
    message: ''
  });
 
  const onSubmit = (data) => {
    console.log('Form submitted:', data);
  };
 
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="name"
        value={values.name}
        onChange={handleChange}
        placeholder="Name"
      />
      {errors.name && <span>{errors.name}</span>}
      
      <input
        name="email"
        value={values.email}
        onChange={handleChange}
        placeholder="Email"
      />
      {errors.email && <span>{errors.email}</span>}
      
      <button type="submit">Submit</button>
    </form>
  );
}

Hooks 规则

1. 只在顶层调用 Hooks

// ❌ 错误 - 在条件语句中调用
function Example() {
  if (someCondition) {
    const [count, setCount] = useState(0); // 错误!
  }
}
 
// ✅ 正确 - 在顶层调用
function Example() {
  const [count, setCount] = useState(0);
  
  if (someCondition) {
    // 其他逻辑
  }
}

2. 只在 React 函数中调用 Hooks

// ✅ 正确 - 在 React 函数组件中
function MyComponent() {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}
 
// ✅ 正确 - 在自定义 Hook 中
function useCustomHook() {
  const [count, setCount] = useState(0);
  return [count, setCount];
}
 
// ❌ 错误 - 在普通函数中
function regularFunction() {
  const [count, setCount] = useState(0); // 错误!
}

性能优化技巧

避免不必要的重新渲染

import React, { memo, useMemo, useCallback } from 'react';
 
const ExpensiveChild = memo(({ data, onClick }) => {
  console.log('ExpensiveChild rendered');
  
  const processedData = useMemo(() => {
    return data.map(item => item * 2);
  }, [data]);
 
  return (
    <div>
      {processedData.map(item => (
        <div key={item} onClick={() => onClick(item)}>
          {item}
        </div>
      ))}
    </div>
  );
});
 
function Parent() {
  const [count, setCount] = useState(0);
  const [data] = useState([1, 2, 3, 4, 5]);
 
  const handleClick = useCallback((item) => {
    console.log('Clicked:', item);
  }, []);
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveChild data={data} onClick={handleClick} />
    </div>
  );
}

标签

React Hooks useState useEffect useContext useReducer 性能优化