Hooks、状态管理

React Hooks 与状态管理

一、useState - 组件状态

1. 基本用法

复制代码
import { useState } from 'react';

function Counter() {
  // 基本类型状态
  const [count, setCount] = useState(0);
  
  // 对象类型状态
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: ''
  });
  
  // 数组类型状态
  const [items, setItems] = useState([]);
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setCount(prev => prev - 1)}>减少</button>
      
      <input
        value={user.name}
        onChange={e => setUser({...user, name: e.target.value})}
        placeholder="姓名"
      />
    </div>
  );
}

2. 状态更新注意事项

复制代码
function UserProfile() {
  const [user, setUser] = useState({
    id: 1,
    name: '张三',
    profile: {
      age: 25,
      address: {
        city: '北京',
        street: '朝阳路'
      }
    }
  });
  
  // ❌ 错误:直接修改状态
  const updateWrong = () => {
    user.profile.age = 30; // 这不会触发重新渲染!
  };
  
  // ✅ 正确:创建新对象
  const updateCorrect = () => {
    setUser({
      ...user,
      profile: {
        ...user.profile,
        age: 30
      }
    });
  };
  
  // 对于深层嵌套对象,可以使用函数式更新
  const updateAddress = (newStreet) => {
    setUser(prevUser => ({
      ...prevUser,
      profile: {
        ...prevUser.profile,
        address: {
          ...prevUser.profile.address,
          street: newStreet
        }
      }
    }));
  };
  
  return (
    <div>
      <p>姓名: {user.name}</p>
      <p>年龄: {user.profile.age}</p>
      <p>地址: {user.profile.address.street}</p>
      <button onClick={updateCorrect}>更新年龄</button>
      <button onClick={() => updateAddress('长安街')}>更新地址</button>
    </div>
  );
}

二、useEffect - 副作用处理

1. 基本用法

复制代码
import { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [userId, setUserId] = useState(1);
  
  // 1. 只在组件挂载时执行(模拟componentDidMount)
  useEffect(() => {
    console.log('组件已挂载');
    
    // 清理函数(模拟componentWillUnmount)
    return () => {
      console.log('组件即将卸载');
    };
  }, []); // 空依赖数组
  
  // 2. 在userId变化时执行(模拟componentDidUpdate)
  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        );
        
        if (!response.ok) {
          throw new Error('请求失败');
        }
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    
    fetchData();
  }, [userId]); // 依赖数组包含userId
  
  // 3. 每次渲染后都执行
  useEffect(() => {
    console.log('组件渲染完成');
  }); // 没有依赖数组
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h1>用户信息</h1>
      <p>ID: {data.id}</p>
      <p>姓名: {data.name}</p>
      <p>邮箱: {data.email}</p>
      
      <div>
        <button onClick={() => setUserId(1)}>用户1</button>
        <button onClick={() => setUserId(2)}>用户2</button>
        <button onClick={() => setUserId(3)}>用户3</button>
      </div>
    </div>
  );
}

2. 副作用清理

复制代码
function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  useEffect(() => {
    let intervalId = null;
    
    if (isRunning) {
      intervalId = setInterval(() => {
        setSeconds(prev => prev + 1);
      }, 1000);
    }
    
    // 清理函数:组件卸载或依赖变化时执行
    return () => {
      if (intervalId) {
        clearInterval(intervalId);
        console.log('定时器已清理');
      }
    };
  }, [isRunning]); // 依赖isRunning
  
  return (
    <div>
      <h2>计时器: {seconds}秒</h2>
      <button onClick={() => setIsRunning(true)}>开始</button>
      <button onClick={() => setIsRunning(false)}>暂停</button>
      <button onClick={() => {
        setIsRunning(false);
        setSeconds(0);
      }}>重置</button>
    </div>
  );
}

三、useContext - 跨组件数据传递

1. 创建和使用Context

复制代码
import { createContext, useContext, useState } from 'react';

// 1. 创建Context
const ThemeContext = createContext('light');
const UserContext = createContext(null);

function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: '张三', role: 'admin' });
  
  return (
    // 2. 提供Context值
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={user}>
        <div className={`app ${theme}`}>
          <Header />
          <MainContent />
          <Footer />
        </div>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

function Header() {
  // 3. 使用Context
  const { theme, setTheme } = useContext(ThemeContext);
  const user = useContext(UserContext);
  
  return (
    <header>
      <h1>欢迎回来, {user.name}</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题: {theme === 'light' ? '🌙' : '☀️'}
      </button>
    </header>
  );
}

function MainContent() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <main>
      <p>当前主题: {theme}</p>
      <ThemedButton />
    </main>
  );
}

function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <button 
      style={{
        backgroundColor: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#333'
      }}
    >
      主题按钮
    </button>
  );
}

2. 自定义Context Hook

复制代码
// 创建自定义Context
const AuthContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  
  const login = async (email, password) => {
    // 模拟登录
    const mockUser = { 
      id: 1, 
      email, 
      name: '测试用户',
      token: 'fake-jwt-token'
    };
    
    setUser(mockUser);
    setIsAuthenticated(true);
    localStorage.setItem('user', JSON.stringify(mockUser));
  };
  
  const logout = () => {
    setUser(null);
    setIsAuthenticated(false);
    localStorage.removeItem('user');
  };
  
  const value = {
    user,
    isAuthenticated,
    login,
    logout
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// 自定义Hook
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth必须在AuthProvider内使用');
  }
  return context;
}

// 使用示例
function LoginPage() {
  const { login, isAuthenticated } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    login(email, password);
  };
  
  if (isAuthenticated) {
    return <Navigate to="/dashboard" />;
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="邮箱"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

四、useReducer - 复杂状态逻辑

复制代码
import { useReducer } from 'react';

// 1. 定义初始状态
const initialState = {
  loading: false,
  data: null,
  error: null,
  filter: 'all',
  sortBy: 'name'
};

// 2. 定义reducer函数
function apiReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return {
        ...state,
        loading: true,
        error: null
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        loading: false,
        data: action.payload,
        error: null
      };
    case 'FETCH_ERROR':
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    case 'SET_SORT':
      return {
        ...state,
        sortBy: action.payload
      };
    case 'UPDATE_ITEM':
      return {
        ...state,
        data: state.data.map(item => 
          item.id === action.payload.id 
            ? { ...item, ...action.payload.changes }
            : item
        )
      };
    default:
      return state;
  }
}

// 3. 创建自定义Hook
function useApi(endpoint) {
  const [state, dispatch] = useReducer(apiReducer, initialState);
  
  const fetchData = async () => {
    try {
      dispatch({ type: 'FETCH_START' });
      
      const response = await fetch(endpoint);
      const data = await response.json();
      
      dispatch({ 
        type: 'FETCH_SUCCESS', 
        payload: data 
      });
    } catch (error) {
      dispatch({ 
        type: 'FETCH_ERROR', 
        payload: error.message 
      });
    }
  };
  
  const setFilter = (filter) => {
    dispatch({ type: 'SET_FILTER', payload: filter });
  };
  
  const setSort = (sortBy) => {
    dispatch({ type: 'SET_SORT', payload: sortBy });
  };
  
  const updateItem = (id, changes) => {
    dispatch({ 
      type: 'UPDATE_ITEM', 
      payload: { id, changes } 
    });
  };
  
  return {
    state,
    fetchData,
    setFilter,
    setSort,
    updateItem
  };
}

// 4. 使用示例
function UserList() {
  const { 
    state: { data, loading, error, filter, sortBy },
    fetchData,
    setFilter,
    updateItem
  } = useApi('https://api.example.com/users');
  
  useEffect(() => {
    fetchData();
  }, []);
  
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  // 过滤和排序
  const filteredUsers = data
    ?.filter(user => {
      if (filter === 'active') return user.active;
      if (filter === 'inactive') return !user.active;
      return true;
    })
    .sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
  
  return (
    <div>
      <div>
        <button onClick={() => setFilter('all')}>全部</button>
        <button onClick={() => setFilter('active')}>活跃</button>
        <button onClick={() => setFilter('inactive')}>非活跃</button>
      </div>
      
      <ul>
        {filteredUsers?.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
            <button onClick={() => updateItem(user.id, { active: !user.active })}>
              {user.active ? '停用' : '激活'}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

五、自定义Hook

1. 数据获取Hook

复制代码
import { useState, useEffect, useCallback } from 'react';

function useFetch(url, options = {}) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });
  
  const [refetchIndex, setRefetchIndex] = useState(0);
  
  const refetch = useCallback(() => {
    setRefetchIndex(prev => prev + 1);
  }, []);
  
  useEffect(() => {
    if (!url) return;
    
    const controller = new AbortController();
    
    async function fetchData() {
      try {
        setState(prev => ({ ...prev, loading: true, error: null }));
        
        const response = await fetch(url, {
          ...options,
          signal: controller.signal
        });
        
        if (!response.ok) {
          throw new Error(`HTTP错误 ${response.status}`);
        }
        
        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        if (error.name !== 'AbortError') {
          setState(prev => ({ 
            ...prev, 
            loading: false, 
            error: error.message 
          }));
        }
      }
    }
    
    fetchData();
    
    return () => {
      controller.abort();
    };
  }, [url, refetchIndex, options]);
  
  return { ...state, refetch };
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useFetch(
    `https://api.example.com/users/${userId}`,
    { headers: { 'Authorization': 'Bearer token' } }
  );
  
  if (loading) return <div>加载用户信息...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={refetch}>重新加载</button>
    </div>
  );
}

2. 表单Hook

复制代码
import { useState, useCallback } from 'react';

function useForm(initialValues = {}, validations = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  const handleChange = useCallback((e) => {
    const { name, value, type, checked } = e.target;
    
    setValues(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
    
    // 如果字段已被触摸过,验证
    if (touched[name]) {
      validateField(name, value);
    }
  }, [touched]);
  
  const handleBlur = useCallback((e) => {
    const { name, value } = e.target;
    
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
    
    validateField(name, value);
  }, []);
  
  const validateField = useCallback((name, value) => {
    if (validations[name]) {
      const error = validations[name](value, values);
      setErrors(prev => ({
        ...prev,
        [name]: error || null
      }));
    }
  }, [values, validations]);
  
  const validateAll = useCallback(() => {
    const newErrors = {};
    let isValid = true;
    
    Object.keys(validations).forEach(name => {
      const error = validations[name](values[name], values);
      if (error) {
        newErrors[name] = error;
        isValid = false;
      }
    });
    
    setErrors(newErrors);
    setTouched(
      Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {})
    );
    
    return isValid;
  }, [values, validations]);
  
  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);
  
  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    validateAll,
    setValues,
    reset
  };
}

// 使用示例
function LoginForm() {
  const validations = {
    email: (value) => {
      if (!value) return '邮箱不能为空';
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        return '邮箱格式不正确';
      }
      return null;
    },
    password: (value) => {
      if (!value) return '密码不能为空';
      if (value.length < 6) return '密码至少6位';
      return null;
    }
  };
  
  const { 
    values, 
    errors, 
    touched, 
    handleChange, 
    handleBlur, 
    validateAll,
    reset 
  } = useForm({ email: '', password: '' }, validations);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateAll()) {
      console.log('表单数据:', values);
      // 提交逻辑
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          type="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="邮箱"
        />
        {touched.email && errors.email && (
          <span style={{ color: 'red' }}>{errors.email}</span>
        )}
      </div>
      
      <div>
        <input
          name="password"
          type="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="密码"
        />
        {touched.password && errors.password && (
          <span style={{ color: 'red' }}>{errors.password}</span>
        )}
      </div>
      
      <button type="submit">登录</button>
      <button type="button" onClick={reset}>重置</button>
    </form>
  );
}

六、状态管理最佳实践

1. 状态提升

复制代码
// 当多个组件需要共享状态时,将状态提升到最近的共同父组件
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  return (
    <div>
      <ChildA count={count} onIncrement={() => setCount(c => c + 1)} />
      <ChildB text={text} onTextChange={setText} />
      <ChildC count={count} text={text} />
    </div>
  );
}

2. 使用useReducer管理复杂状态

复制代码
// 当状态逻辑复杂时,使用useReducer
function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, {
    step: 1,
    personalInfo: { name: '', email: '' },
    address: { city: '', street: '' },
    errors: {},
    isSubmitting: false
  });
  
  const nextStep = () => {
    if (validateStep(state.step)) {
      dispatch({ type: 'NEXT_STEP' });
    }
  };
  
  return (
    <div>
      {state.step === 1 && (
        <Step1
          data={state.personalInfo}
          errors={state.errors}
          onChange={(data) => dispatch({ type: 'UPDATE_PERSONAL', payload: data })}
        />
      )}
      {state.step === 2 && (
        <Step2
          data={state.address}
          onChange={(data) => dispatch({ type: 'UPDATE_ADDRESS', payload: data })}
        />
      )}
      <button onClick={nextStep}>下一步</button>
    </div>
  );
}

3. Context + useReducer 模式

复制代码
// 创建全局状态
const StoreContext = createContext();

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(rootReducer, initialState);
  
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

function useStore() {
  const context = useContext(StoreContext);
  if (!context) {
    throw new Error('useStore必须在StoreProvider内使用');
  }
  return context;
}

七、性能优化

1. useMemo - 记忆计算结果

复制代码
import { useMemo } from 'react';

function ExpensiveComponent({ list, filter }) {
  // 只有当list或filter变化时才重新计算
  const filteredList = useMemo(() => {
    console.log('计算过滤列表...');
    return list.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [list, filter]); // 依赖数组
  
  // 复杂计算
  const stats = useMemo(() => {
    return {
      total: filteredList.length,
      average: filteredList.reduce((sum, item) => sum + item.value, 0) / filteredList.length,
      max: Math.max(...filteredList.map(item => item.value))
    };
  }, [filteredList]);
  
  return (
    <div>
      <p>总数: {stats.total}</p>
      <p>平均值: {stats.average.toFixed(2)}</p>
      <p>最大值: {stats.max}</p>
    </div>
  );
}

2. useCallback - 记忆函数

复制代码
import { useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 使用useCallback避免子组件不必要的重渲染
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 依赖数组为空,函数只创建一次
  
  const fetchData = useCallback(async (id) => {
    const response = await fetch(`/api/data/${id}`);
    return response.json();
  }, []); // 依赖数组为空
  
  return (
    <div>
      <Child onIncrement={increment} fetchData={fetchData} />
    </div>
  );
}

// 使用React.memo避免不必要的重渲染
const Child = React.memo(function Child({ onIncrement, fetchData }) {
  console.log('Child渲染了');
  
  return (
    <button onClick={onIncrement}>增加</button>
  );
});

八、第三方状态管理库(Zustand示例)

复制代码
import create from 'zustand';

// 创建store
const useStore = create((set, get) => ({
  // 状态
  count: 0,
  user: null,
  todos: [],
  loading: false,
  
  // Action
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  
  // 异步action
  fetchUser: async (id) => {
    set({ loading: true });
    
    try {
      const response = await fetch(`/api/users/${id}`);
      const user = await response.json();
      set({ user, loading: false });
    } catch (error) {
      console.error('获取用户失败:', error);
      set({ loading: false });
    }
  },
  
  // 在action中访问当前状态
  asyncAdd: async (amount) => {
    const { count } = get(); // 获取当前状态
    set({ loading: true });
    
    // 模拟API调用
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    set(state => ({ 
      count: state.count + amount,
      loading: false 
    }));
  },
  
  // 复杂更新
  addTodo: (text) => 
    set(state => ({
      todos: [...state.todos, { 
        id: Date.now(), 
        text, 
        completed: false 
      }]
    })),
  
  toggleTodo: (id) =>
    set(state => ({
      todos: state.todos.map(todo =>
        todo.id === id 
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    }))
}));

// 在组件中使用
function Counter() {
  const { count, increment, decrement, reset } = useStore();
  
  return (
    <div>
      <h1>计数: {count}</h1>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

// 选择器:只订阅需要的数据
function TodoList() {
  const todos = useStore(state => state.todos);
  const addTodo = useStore(state => state.addTodo);
  const toggleTodo = useStore(state => state.toggleTodo);
  
  return (
    <div>
      <button onClick={() => addTodo('新任务')}>添加任务</button>
      <ul>
        {todos.map(todo => (
          <li 
            key={todo.id}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
            onClick={() => toggleTodo(todo.id)}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

关键要点

  1. useState:用于组件内部状态

  2. useEffect:处理副作用(API调用、订阅、DOM操作)

  3. useContext:跨组件传递数据

  4. useReducer:复杂状态逻辑

  5. 自定义Hook:封装和复用逻辑

  6. 性能优化:useMemo、useCallback、React.memo

  7. 状态管理策略

    • 局部状态用useState

    • 组件间共享用状态提升

    • 复杂状态用useReducer

    • 全局状态用Context或第三方库

练习建议

  1. 创建一个计数器应用,使用所有状态管理方法实现

  2. 实现一个Todo应用,支持添加、删除、完成、过滤

  3. 创建一个用户管理面板,集成API调用

  4. 实现一个购物车,使用Context + useReducer

  5. 用Zustand重构上述应用,对比体验差异

相关推荐
lichenyang4532 小时前
从零开始构建 React 文档系统 - 完整实现指南
前端·javascript·react.js
landonVM2 小时前
Linux 上搭建 Web 服务器
linux·服务器·前端
css趣多多2 小时前
路由全局守卫
前端
AI视觉网奇2 小时前
huggingface-cli 安装笔记2026
前端·笔记
比特森林探险记2 小时前
组件通信 与 ⏳ 生命周期
前端·javascript·vue.js
2301_792580002 小时前
xuepso
java·服务器·前端
海绵宝龙3 小时前
Vue中nextTick
前端·javascript·vue.js
天生欧皇张狗蛋3 小时前
前端部署path问题
前端
H_z_q24013 小时前
Web前端制作一个评论发布案例
前端·javascript·css