React学习教程,从入门到精通,React Hook 详解 —— 语法知识点、使用方法与案例代码(26)

React Hook 详解 —— 语法知识点、使用方法与案例代码


🎯 本章目标

全面掌握 React Hook 的核心概念与实战用法,包括:

  • ✅ Hook 技术介绍
  • ✅ State Hook(useState)
  • ✅ Effect Hook(useEffect)
  • ✅ React 内置 Hook(useContext、useReducer、useRef、useMemo、useCallback 等)
  • ✅ 自定义 Hook
  • ✅ 注意事项与最佳实践
  • ✅ 综合性实战案例

一、Hook 技术介绍

Hook 是 React 16.8 新增特性,让你在不编写 class 的情况下使用 state 以及其他 React 特性。

✅ 为什么使用 Hook?

  • 函数组件也能拥有状态和生命周期
  • 逻辑复用更简单(自定义 Hook)
  • 代码更简洁、易测试、易理解
  • 避免“Wrapper Hell”(组件嵌套地狱)

⚠️ Hook 使用规则

  1. 只能在函数组件或自定义 Hook 中调用
  2. 必须在顶层调用(不能在循环、条件、嵌套函数中)
  3. 命名以 use 开头

二、State Hook —— useState

用于在函数组件中声明状态变量。

2.1 基础语法

const [state, setState] = useState(initialValue);
  • state:当前状态值
  • setState:更新状态的函数
  • initialValue:初始值,可以是任意类型(对象、数组、函数等)

2.2 案例1:计数器

import React, { useState } from 'react';

function Counter() {
  // 声明一个叫 count 的状态变量,初始值为 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={() => setCount(count + 1)}>
        ➕ 增加
      </button>
      <button onClick={() => setCount(count - 1)}>
        ➖ 减少
      </button>
      <button onClick={() => setCount(0)}>
        🔄 重置
      </button>
    </div>
  );
}

export default Counter;

✅ 注:setCount 是异步的,多次调用可能合并。如需基于前一状态更新,应使用函数形式:

setCount(prevCount => prevCount + 1);

2.3 案例2:对象状态更新(表单)

import React, { useState } from 'react';

function UserProfile() {
  // 初始化对象状态
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: ''
  });

  // 处理输入变化
  const handleChange = (e) => {
    const { name, value } = e.target;
    // 使用函数式更新,避免状态覆盖
    setUser(prevUser => ({
      ...prevUser, // 展开旧状态
      [name]: value // 动态键名更新
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`提交数据: ${JSON.stringify(user, null, 2)}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="姓名"
        required
      />
      <input
        name="email"
        type="email"
        value={user.email}
        onChange={handleChange}
        placeholder="邮箱"
        required
      />
      <input
        name="age"
        type="number"
        value={user.age}
        onChange={handleChange}
        placeholder="年龄"
      />
      <button type="submit">提交</button>
    </form>
  );
}

export default UserProfile;

三、Effect Hook —— useEffect

用于处理副作用(如数据获取、订阅、手动 DOM 操作等),替代 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount

3.1 基础语法

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数(可选)
  };
}, [dependencies]); // 依赖数组(可选)

3.2 案例1:组件挂载时执行(模拟 componentDidMount)

import React, { useEffect } from 'react';

function Welcome() {
  useEffect(() => {
    console.log('✅ 组件已挂载');

    // 可选:返回清理函数
    return () => {
      console.log('🧹 组件将卸载');
    };
  }, []); // 空数组 = 仅在挂载/卸载时执行

  return <h1>欢迎使用 React Hook!</h1>;
}

export default Welcome;

3.3 案例2:依赖更新时执行(模拟 componentDidUpdate)

import React, { useState, useEffect } from 'react';

function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    console.log('⏰ 时间更新:', time.toLocaleTimeString());

    // 设置定时器
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);

    // 清理定时器(避免内存泄漏)
    return () => {
      clearInterval(timer);
      console.log('🧹 定时器已清理');
    };
  }, []); // 仅在组件挂载时设置一次

  return <h2>当前时间: {time.toLocaleTimeString()}</h2>;
}

export default Clock;

3.4 案例3:依赖特定状态(监听 count 变化)

import React, { useState, useEffect } from 'react';

function EffectDemo() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 仅当 count 变化时执行
  useEffect(() => {
    console.log(`🔢 count 变为: ${count}`);
  }, [count]); // 依赖数组包含 count

  // 仅当 name 变化时执行
  useEffect(() => {
    console.log(`📛 name 变为: ${name}`);
  }, [name]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>增加 Count</button>

      <p>Name: {name}</p>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="输入名字"
      />
    </div>
  );
}

export default EffectDemo;

四、React 内置 Hook


4.1 useContext —— 访问上下文

用于在组件树中共享数据(如主题、用户信息),避免逐层传递 props。

// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';

// 创建 Context
const ThemeContext = createContext();

// 提供者组件
export 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>
  );
}

// 自定义 Hook 方便使用
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用');
  }
  return context;
}
// App.js
import React from 'react';
import { ThemeProvider, useTheme } from './ThemeContext';

function ThemedButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === 'dark' ? '#333' : '#eee',
        color: theme === 'dark' ? '#fff' : '#000',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer'
      }}
    >
      当前主题: {theme} 🌓
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <div style={{ padding: '20px' }}>
        <h1> useContext 示例</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

export default App;

4.2 useReducer —— 复杂状态逻辑

适用于状态逻辑较复杂、包含多个子值、或下一个 state 依赖于前一个 state 的情况。

import React, { useReducer } from 'react';

// 定义 reducer 函数
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: Date.now(),
          text: action.payload,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

function TodoApp() {
  // 初始化状态和 dispatch
  const [todos, dispatch] = useReducer(todoReducer, []);

  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch({ type: 'ADD_TODO', payload: inputValue });
      setInputValue('');
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="添加待办事项"
        />
        <button type="submit">添加</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{
            textDecoration: todo.completed ? 'line-through' : 'none',
            color: todo.completed ? '#888' : '#000'
          }}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            />
            {todo.text}
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}></button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

4.3 useRef —— 访问 DOM 或保存可变值

用于获取 DOM 元素引用,或保存在渲染之间不变的可变值(不会触发重渲染)。

import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
  // 创建 ref
  const inputRef = useRef(null);

  const focusInput = () => {
    // 聚焦输入框
    inputRef.current.focus();
  };

  // 组件挂载后自动聚焦
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        placeholder="点击按钮或自动聚焦"
      />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

export default TextInputWithFocusButton;
// 保存上一次值(不触发重渲染)
function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    // 更新 ref(不会触发重渲染)
    prevCountRef.current = count;
  }, [count]); // 仅当 count 变化时更新

  const prevCount = prevCountRef.current;

  return (
    <div>
      <h3>当前: {count}, 上次: {prevCount !== undefined ? prevCount : '无'}</h3>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

4.4 useMemo —— 缓存计算结果

用于缓存昂贵的计算结果,避免不必要的重复计算。

import React, { useState, useMemo } from 'react';

function ExpensiveComponent({ list, filterText }) {
  // 模拟昂贵计算:过滤 + 映射
  const filteredList = useMemo(() => {
    console.log('🔍 执行过滤计算');
    return list
      .filter(item => item.name.toLowerCase().includes(filterText.toLowerCase()))
      .map(item => ({
        ...item,
        displayName: `${item.name} (${item.age})`
      }));
  }, [list, filterText]); // 仅当 list 或 filterText 变化时重新计算

  return (
    <ul>
      {filteredList.map(item => (
        <li key={item.id}>{item.displayName}</li>
      ))}
    </ul>
  );
}

function App() {
  const [filter, setFilter] = useState('');
  const [count, setCount] = useState(0); // 无关状态

  // 模拟大型列表
  const users = [
    { id: 1, name: '张三', age: 25 },
    { id: 2, name: '李四', age: 30 },
    { id: 3, name: '王五', age: 35 },
    { id: 4, name: '赵六', age: 28 },
  ];

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="过滤用户..."
      />
      <button onClick={() => setCount(c => c + 1)}>改变无关状态: {count}</button>

      <ExpensiveComponent list={users} filterText={filter} />
    </div>
  );
}

export default App;

✅ 控制台只会打印一次 “执行过滤计算”,除非 filterusers 变化。


4.5 useCallback —— 缓存函数

用于缓存函数引用,避免子组件因函数引用变化而重新渲染。

import React, { useState, useCallback } from 'react';

// 模拟“昂贵”的子组件(带 memo)
const Child = React.memo(({ onClick, name }) => {
  console.log(`👶 ${name} 重新渲染`);
  return <button onClick={onClick}>点击 {name}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // ❌ 每次渲染都创建新函数(导致 Child 重新渲染)
  // const handleClick = () => {
  //   console.log('点击了');
  // };

  // ✅ 使用 useCallback 缓存函数
  const handleClick = useCallback(() => {
    console.log('点击了');
  }, []); // 无依赖,函数引用不变

  return (
    <div>
      <h3>父组件状态: {count}</h3>
      <button onClick={() => setCount(c => c + 1)}>增加父组件状态</button>

      {/* 传递缓存函数,避免 Child 不必要重渲染 */}
      <Child onClick={handleClick} name="子组件A" />
      <Child onClick={handleClick} name="子组件B" />
    </div>
  );
}

export default Parent;

✅ 控制台只会在首次渲染时打印 “👶 子组件A 重新渲染” 和 “👶 子组件B 重新渲染”,之后点击“增加父组件状态”不会导致子组件重新渲染。


五、自定义 Hook

将组件逻辑提取到可重用的函数中。

5.1 案例1:自定义计数器 Hook

// hooks/useCounter.js
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  return {
    count,
    increment,
    decrement,
    reset
  };
}
// 组件中使用
import React from 'react';
import { useCounter } from './hooks/useCounter';

function CounterA() {
  const { count, increment, decrement, reset } = useCounter(10);

  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
      <h4>计数器A (初始值10)</h4>
      <p>: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

function CounterB() {
  const { count, increment } = useCounter(0);

  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
      <h4>计数器B (初始值0)</h4>
      <p>: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <CounterA />
      <CounterB />
    </div>
  );
}

export default App;

5.2 案例2:自定义 localStorage Hook

// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';

export 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('localStorage 读取失败:', error);
      return initialValue;
    }
  });

  // 当 storedValue 变化时,更新 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error('localStorage 写入失败:', error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}
// 使用
import React from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';

function ThemeSwitcher() {
  const [theme, setTheme] = useLocalStorage('app-theme', 'light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <div
      style={{
        background: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#000',
        minHeight: '100vh',
        padding: '20px'
      }}
    >
      <h1>主题: {theme}</h1>
      <button onClick={toggleTheme}>切换主题</button>
      <p>刷新页面,主题状态将被保留!</p>
    </div>
  );
}

export default ThemeSwitcher;

六、注意事项

6.1 Hook 调用顺序必须一致

❌ 错误示例:

function BadComponent({ condition }) {
  const [count, setCount] = useState(0);

  if (condition) {
    // 条件调用 Hook —— 违反规则!
    const [name, setName] = useState('张三');
  }

  useEffect(() => {
    // ...
  }, []);

  return <div>...</div>;
}

✅ 正确做法:

function GoodComponent({ condition }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('张三'); // 始终调用

  useEffect(() => {
    // ...
  }, []);

  return <div>...</div>;
}

6.2 不要在循环、条件或嵌套函数中调用 Hook

❌ 错误:

function BadList({ items }) {
  items.forEach(item => {
    const [state, setState] = useState(null); // 在循环中调用 —— 错误!
  });
}

6.3 使用 ESLint 插件避免错误

npm install eslint-plugin-react-hooks --save-dev
// .eslintrc
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

6.4 useEffect 依赖数组注意事项

❌ 错误:忘记添加依赖

function BadEffect({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    // userId 变化时不会重新执行!
    fetch(`/api/user/${userId}`)
      .then(res => res.json())
      .then(setData);
  }, []); // ❌ 缺少 userId 依赖

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

✅ 正确:

useEffect(() => {
  fetch(`/api/user/${userId}`)
    .then(res => res.json())
    .then(setData);
}, [userId]); // ✅ 添加依赖

✅ 更安全:使用 eslint-plugin-react-hooks 自动检测依赖


七、综合性案例:Todo 应用(包含多种 Hook)

// hooks/useTodos.js
import { useReducer, useEffect } from 'react';

const TODO_STORAGE_KEY = 'react-todos';

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: Date.now(), text: action.text, completed: false }];
    case 'TOGGLE':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    case 'DELETE':
      return state.filter(todo => todo.id !== action.id);
    case 'LOAD':
      return action.todos;
    default:
      return state;
  }
}

export function useTodos() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  // 从 localStorage 加载
  useEffect(() => {
    const saved = localStorage.getItem(TODO_STORAGE_KEY);
    if (saved) {
      dispatch({ type: 'LOAD', todos: JSON.parse(saved) });
    }
  }, []);

  // 保存到 localStorage
  useEffect(() => {
    localStorage.setItem(TODO_STORAGE_KEY, JSON.stringify(todos));
  }, [todos]);

  const addTodo = (text) => dispatch({ type: 'ADD', text });
  const toggleTodo = (id) => dispatch({ type: 'TOGGLE', id });
  const deleteTodo = (id) => dispatch({ type: 'DELETE', id });

  return {
    todos,
    addTodo,
    toggleTodo,
    deleteTodo
  };
}
// components/TodoForm.js
import React, { useState } from 'react';

export default function TodoForm({ onAdd }) {
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text);
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="添加新任务..."
        style={{ padding: '8px', marginRight: '8px', width: '200px' }}
      />
      <button type="submit" style={{ padding: '8px 16px' }}>
        添加
      </button>
    </form>
  );
}
// components/TodoItem.js
import React from 'react';

export default function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li style={{
      display: 'flex',
      alignItems: 'center',
      padding: '8px',
      borderBottom: '1px solid #eee',
      textDecoration: todo.completed ? 'line-through' : 'none',
      color: todo.completed ? '#888' : '#000'
    }}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        style={{ marginRight: '10px' }}
      />
      <span style={{ flex: 1 }}>{todo.text}</span>
      <button
        onClick={() => onDelete(todo.id)}
        style={{
          background: 'none',
          border: 'none',
          color: 'red',
          cursor: 'pointer',
          fontSize: '18px'
        }}
      ></button>
    </li>
  );
}
// App.js
import React from 'react';
import TodoForm from './components/TodoForm';
import TodoItem from './components/TodoItem';
import { useTodos } from './hooks/useTodos';

function App() {
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();

  const activeCount = todos.filter(t => !t.completed).length;
  const completedCount = todos.filter(t => t.completed).length;

  return (
    <div style={{ maxWidth: '500px', margin: '50px auto', padding: '20px' }}>
      <h1>📝 Todo 应用</h1>
      <TodoForm onAdd={addTodo} />

      <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
        <span>活动: {activeCount}</span>
        <span>已完成: {completedCount}</span>
      </div>

      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
          />
        ))}
      </ul>

      {todos.length === 0 && (
        <p style={{ textAlign: 'center', color: '#888' }}>暂无任务,添加一个吧!</p>
      )}
    </div>
  );
}

export default App;

✅ 功能亮点:

  • 使用 useReducer 管理复杂状态
  • 使用 useEffect 同步 localStorage
  • 自定义 Hook useTodos 封装逻辑
  • 组件拆分清晰
  • 状态持久化

📌 本章小结

Hook 名称用途关键点
useState声明状态支持函数式更新,避免状态覆盖
useEffect处理副作用依赖数组控制执行时机,注意清理函数
useContext访问上下文避免 prop drilling,全局状态共享
useReducer复杂状态管理适合多个子状态、状态转换逻辑复杂场景
useRef访问 DOM / 保存可变值不触发重渲染,用于聚焦、计时器、前值保存等
useMemo缓存计算结果优化性能,避免重复昂贵计算
useCallback缓存函数引用避免子组件不必要重渲染
自定义 Hook逻辑复用命名以 use 开头,可组合多个 Hook

🚀 最佳实践

  • 优先使用 useState + useEffect
  • 复杂状态用 useReducer
  • 性能优化用 useMemo / useCallback
  • 逻辑复用写自定义 Hook
  • 始终遵守 Hook 规则
  • 使用 ESLint 插件辅助

掌握这些 Hook,你已能应对绝大多数 React 函数组件开发场景!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值