Skip to content

React Hooks 系统

本文介绍React Hooks的基本概念和使用方法,包括useState、useEffect、useContext等常用钩子,以及自定义Hook的创建。

什么是Hooks?

Hooks是React 16.8引入的特性,它允许你在不编写class的情况下使用state以及其他的React特性。Hooks是完全可选的,100%向后兼容,没有破坏性改动。

为什么使用Hooks?

  • 无需重构即可使用状态:在函数组件中使用状态逻辑,而不必转换为类组件
  • 复用状态逻辑:在不强制组件层次结构的情况下重用状态逻辑
  • 组织相关代码:将相互关联的代码(如设置订阅或获取数据)组合在一起,而不是按生命周期方法拆分
  • 使用函数而非类:避免类组件中的this问题,更容易理解

Hooks规则

  1. 只在最顶层使用Hooks:不要在循环、条件或嵌套函数中调用Hooks
  2. 只在React函数中调用Hooks:在React函数组件和自定义Hooks中调用Hooks,不要在普通JavaScript函数中调用

useState

useState是最基本的Hook,它让你在函数组件中添加本地状态。

基本用法

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

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

使用多个状态变量

jsx
function UserForm() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');

  return (
    <form>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        type="number"
        value={age}
        onChange={e => setAge(Number(e.target.value))}
        placeholder="Age"
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
    </form>
  );
}

使用对象状态

jsx
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser(prevUser => ({
      ...prevUser,
      [name]: name === 'age' ? Number(value) : value
    }));
  };

  return (
    <form>
      <input
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="age"
        type="number"
        value={user.age}
        onChange={handleChange}
        placeholder="Age"
      />
      <input
        name="email"
        value={user.email}
        onChange={handleChange}
        placeholder="Email"
      />
    </form>
  );
}

函数式更新

当新的状态依赖于之前的状态时,应该使用函数式更新:

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

  // 不好的方式 - 可能导致问题
  const handleIncrement = () => {
    setCount(count + 1); // 使用当前的count值
    setCount(count + 1); // 仍然使用相同的count值
  };

  // 好的方式 - 使用函数式更新
  const handleIncrementCorrect = () => {
    setCount(prevCount => prevCount + 1); // 使用前一个状态
    setCount(prevCount => prevCount + 1); // 使用更新后的前一个状态
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrementCorrect}>Increment twice</button>
    </div>
  );
}

惰性初始化

如果初始状态需要通过复杂计算获得,可以传递一个函数给useState

jsx
function createInitialTodos() {
  console.log('Creating initial todos - expensive calculation');
  return Array.from({ length: 50 }, (_, i) => ({ id: i, text: `Item ${i}` }));
}

function TodoList() {
  // 这个函数只在组件首次渲染时执行一次
  const [todos, setTodos] = useState(() => createInitialTodos());

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  );
}

useEffect

useEffect允许你在函数组件中执行副作用操作,如数据获取、订阅、手动DOM操作等。

基本用法

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

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

  // 类似于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 更新文档标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

依赖数组

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

  // 仅在count更改时运行
  useEffect(() => {
    document.title = `You clicked ${count} times`;
    console.log('Title effect ran');
  }, [count]); // 仅在count改变时更新

  // 仅在组件挂载时运行一次
  useEffect(() => {
    console.log('Component mounted');
    // 清理函数将在组件卸载时运行
    return () => {
      console.log('Component will unmount');
    };
  }, []); // 空依赖数组表示只在挂载和卸载时执行

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Enter your name"
      />
    </div>
  );
}

清理副作用

jsx
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // 设置订阅
    const connection = createConnection(roomId);
    connection.connect();
    
    connection.on('message', (message) => {
      setMessages(prev => [...prev, message]);
    });

    // 清理函数
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // 当roomId变化时重新订阅

  return (
    <div>
      <h1>Welcome to {roomId}!</h1>
      <ul>
        {messages.map(message => (
          <li key={message.id}>{message.text}</li>
        ))}
      </ul>
    </div>
  );
}

数据获取示例

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 重置状态
    setUser(null);
    setLoading(true);
    setError(null);
    
    // 定义异步函数
    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Failed to fetch user');
        }
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    // 调用异步函数
    fetchUser();

    // 可选的清理函数
    return () => {
      // 如果需要,可以在这里取消请求
    };
  }, [userId]); // 当userId变化时重新获取

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

useContext

useContext让你可以订阅React的Context,而不必使用Context.Consumer组件。

创建和使用Context

jsx
import React, { createContext, useContext, useState } from 'react';

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

// 提供Context的父组件
function App() {
  const [theme, setTheme] = useState('light');

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

  // 提供值给Context
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div className={`App ${theme}`}>
        <Header />
        <Main />
        <Footer />
      </div>
    </ThemeContext.Provider>
  );
}

// 使用Context的子组件
function Header() {
  // 使用useContext获取Context值
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header>
      <h1>My App</h1>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} mode
      </button>
    </header>
  );
}

function Main() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <main>
      <p>Current theme: {theme}</p>
    </main>
  );
}

function Footer() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <footer>
      <p>Footer in {theme} mode</p>
    </footer>
  );
}

使用多个Context

jsx
const UserContext = createContext();
const ThemeContext = createContext();

function App() {
  const [user, setUser] = useState({ name: 'John' });
  const [theme, setTheme] = useState('light');

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Layout />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

function Layout() {
  return (
    <div>
      <Header />
      <Content />
    </div>
  );
}

function Content() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);

  return (
    <section className={theme}>
      <h1>Welcome, {user.name}!</h1>
      <p>Theme: {theme}</p>
    </section>
  );
}

useReducer

useReduceruseState的替代方案,适用于复杂的状态逻辑。

基本用法

jsx
import React, { useReducer } from 'react';

// 定义reducer函数
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error(`Unsupported action type: ${action.type}`);
  }
}

function Counter() {
  // 使用useReducer,提供reducer函数和初始状态
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>
        Increment
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>
        Decrement
      </button>
      <button onClick={() => dispatch({ type: 'reset' })}>
        Reset
      </button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>
        Set to 10
      </button>
    </div>
  );
}

复杂状态管理

jsx
function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, {
        id: Date.now(),
        text: action.payload,
        completed: false
      }];
    case 'toggle':
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case 'delete':
      return state.filter(todo => todo.id !== action.payload);
    case 'clear_completed':
      return state.filter(todo => !todo.completed);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todosReducer, []);
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch({ type: 'add', payload: text });
    setText('');
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={text}
          onChange={e => setText(e.target.value)}
          placeholder="Add todo"
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li
            key={todo.id}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            <span onClick={() => dispatch({ type: 'toggle', payload: todo.id })}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'delete', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>

      {todos.some(todo => todo.completed) && (
        <button onClick={() => dispatch({ type: 'clear_completed' })}>
          Clear completed
        </button>
      )}
    </div>
  );
}

使用初始化函数

jsx
function init(initialCount) {
  return { count: initialCount };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({ initialCount = 0 }) {
  // 第三个参数是初始化函数
  const [state, dispatch] = useReducer(reducer, initialCount, init);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
        Reset
      </button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

useCallback

useCallback返回一个记忆化的回调函数,只有当依赖项变化时才会更新。

基本用法

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

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  // 没有使用useCallback - 每次渲染都会创建新函数
  const incrementWithoutCallback = () => {
    setCount(c => c + 1);
  };

  // 使用useCallback - 只有当依赖项变化时才会创建新函数
  const incrementWithCallback = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 空依赖数组意味着这个函数只会被创建一次

  // 依赖于某个状态的回调
  const incrementBy = useCallback((amount) => {
    setCount(c => c + amount);
  }, []); // 没有依赖于外部变量,所以空数组就足够了

  return (
    <div>
      <p>Count: {count}</p>
      <p>Other state: {otherState}</p>
      
      <button onClick={incrementWithCallback}>Increment (with callback)</button>
      <button onClick={incrementWithoutCallback}>Increment (without callback)</button>
      <button onClick={() => incrementBy(5)}>Increment by 5</button>
      
      <button onClick={() => setOtherState(o => o + 1)}>
        Update other state
      </button>
      
      <ChildComponent onIncrement={incrementWithCallback} />
    </div>
  );
}

// 使用React.memo优化子组件,只有当props变化时才重新渲染
const ChildComponent = React.memo(({ onIncrement }) => {
  console.log('ChildComponent rendered');
  return (
    <button onClick={onIncrement}>Increment from child</button>
  );
});

依赖项示例

jsx
function SearchResults() {
  const [query, setQuery] = useState('');
  const [page, setPage] = useState(1);

  // 这个函数依赖于query和page,所以它们应该在依赖数组中
  const fetchResults = useCallback(async () => {
    const response = await fetch(
      `https://api.example.com/search?q=${query}&page=${page}`
    );
    const data = await response.json();
    return data;
  }, [query, page]); // 依赖于query和page

  // 使用fetchResults...

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button onClick={() => setPage(p => p + 1)}>Next Page</button>
      <ResultList fetchResults={fetchResults} />
    </div>
  );
}

// 使用React.memo优化,只有当fetchResults变化时才重新渲染
const ResultList = React.memo(({ fetchResults }) => {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const getResults = async () => {
      const data = await fetchResults();
      setResults(data);
    };
    getResults();
  }, [fetchResults]); // 依赖于fetchResults

  return (
    <ul>
      {results.map(result => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  );
});

useMemo

useMemo返回一个记忆化的值,只有当依赖项变化时才重新计算。

基本用法

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

function ExpensiveCalculation({ a, b }) {
  // 使用useMemo缓存计算结果
  const result = useMemo(() => {
    console.log('Computing result...');
    // 模拟昂贵的计算
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += i;
    }
    return a * b + sum;
  }, [a, b]); // 只有当a或b变化时才重新计算

  return <div>Result: {result}</div>;
}

function App() {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <div>
        <input
          type="number"
          value={a}
          onChange={e => setA(Number(e.target.value))}
        />
        <input
          type="number"
          value={b}
          onChange={e => setB(Number(e.target.value))}
        />
      </div>
      
      <ExpensiveCalculation a={a} b={b} />
      
      <div>
        <p>Counter: {counter}</p>
        <button onClick={() => setCounter(c => c + 1)}>
          Increment counter (doesn't affect calculation)
        </button>
      </div>
    </div>
  );
}

避免不必要的重新渲染

jsx
function TodoList({ todos, filter }) {
  // 使用useMemo缓存过滤后的列表
  const filteredTodos = useMemo(() => {
    console.log('Filtering todos...');
    return todos.filter(todo => {
      if (filter === 'all') return true;
      if (filter === 'completed') return todo.completed;
      if (filter === 'active') return !todo.completed;
      return true;
    });
  }, [todos, filter]); // 只有当todos或filter变化时才重新计算

  return (
    <ul>
      {filteredTodos.map(todo => (
        <li key={todo.id}>
          {todo.text} {todo.completed ? '(completed)' : ''}
        </li>
      ))}
    </ul>
  );
}

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: true },
    { id: 2, text: 'Learn Hooks', completed: false },
    { id: 3, text: 'Build something awesome', completed: false }
  ]);
  const [filter, setFilter] = useState('all');
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <div>
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('active')}>Active</button>
        <button onClick={() => setFilter('completed')}>Completed</button>
      </div>
      
      <TodoList todos={todos} filter={filter} />
      
      <div>
        <p>Counter: {counter}</p>
        <button onClick={() => setCounter(c => c + 1)}>
          Increment counter (doesn't affect todos)
        </button>
      </div>
    </div>
  );
}

记忆化对象

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

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

  // 记忆化对象,避免每次渲染创建新对象
  const userStyles = useMemo(() => ({
    backgroundColor: user?.isPremium ? 'gold' : 'silver',
    padding: '10px',
    borderRadius: '5px',
    color: user?.isPremium ? 'black' : 'white'
  }), [user?.isPremium]);

  if (!user) return <div>Loading...</div>;

  return (
    <div style={userStyles}>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>{user.isPremium ? 'Premium User' : 'Regular User'}</p>
    </div>
  );
}

useRef

useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数。返回的对象在组件的整个生命周期内保持不变。

访问DOM元素

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

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

  // 点击按钮时聚焦输入框
  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}

保存可变值

jsx
function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  // 使用ref保存interval ID
  const intervalRef = useRef(null);
  
  // 使用ref保存上一次渲染时的时间,不会触发重新渲染
  const previousTimeRef = useRef(0);

  useEffect(() => {
    // 保存上一次的时间
    previousTimeRef.current = time;
  });

  useEffect(() => {
    if (isRunning) {
      intervalRef.current = setInterval(() => {
        setTime(t => t + 1);
      }, 1000);
    } else if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
    
    // 清理函数
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [isRunning]);

  const handleStartStop = () => {
    setIsRunning(!isRunning);
  };

  const handleReset = () => {
    setTime(0);
    setIsRunning(false);
  };

  return (
    <div>
      <p>Current time: {time} seconds</p>
      <p>Previous time: {previousTimeRef.current} seconds</p>
      <button onClick={handleStartStop}>
        {isRunning ? 'Stop' : 'Start'}
      </button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

避免不必要的重新渲染

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  // 使用ref保存计数,不会触发重新渲染
  const countRef = useRef(0);
  
  const incrementWithState = () => {
    setCount(count + 1); // 触发重新渲染
  };
  
  const incrementWithRef = () => {
    countRef.current += 1; // 不触发重新渲染
    console.log(`Current ref count: ${countRef.current}`);
  };

  console.log('Component rendered');

  return (
    <div>
      <p>State count: {count}</p>
      <p>Ref count: {countRef.current}</p>
      <button onClick={incrementWithState}>Increment state</button>
      <button onClick={incrementWithRef}>Increment ref</button>
    </div>
  );
}

useLayoutEffect

useLayoutEffectuseEffect相同,但它会在所有DOM变更之后同步调用。可以使用它来读取DOM布局并同步触发重渲染。

基本用法

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

function LayoutEffectExample() {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const divRef = useRef();

  // 在DOM更新后同步运行
  useLayoutEffect(() => {
    // 这会在浏览器绘制之前同步执行
    const { width, height } = divRef.current.getBoundingClientRect();
    setWidth(width);
    setHeight(height);
  }, []); // 仅在挂载时运行

  // 对比useEffect(会在浏览器绘制后异步执行)
  useEffect(() => {
    console.log('useEffect ran');
  }, []);

  return (
    <div>
      <div
        ref={divRef}
        style={{ width: '100px', height: '100px', background: 'red' }}
      >
        Target div
      </div>
      <p>Measured width: {width}px</p>
      <p>Measured height: {height}px</p>
    </div>
  );
}

防止闪烁

jsx
function TooltipPosition() {
  const [tooltipHeight, setTooltipHeight] = useState(0);
  const tooltipRef = useRef();
  
  // 使用useLayoutEffect防止闪烁
  useLayoutEffect(() => {
    const height = tooltipRef.current.offsetHeight;
    setTooltipHeight(height);
  }, []);
  
  const tooltipStyle = {
    position: 'absolute',
    top: `-${tooltipHeight}px`,
    left: 0,
    background: 'black',
    color: 'white',
    padding: '5px',
    borderRadius: '3px'
  };
  
  return (
    <div style={{ position: 'relative', marginTop: '50px' }}>
      <div ref={tooltipRef} style={tooltipStyle}>
        This is a tooltip
      </div>
      <button>Hover me</button>
    </div>
  );
}

useImperativeHandle

useImperativeHandle自定义使用ref时暴露给父组件的实例值。

jsx
import React, { useRef, useImperativeHandle, forwardRef } from 'react';

// 使用forwardRef获取父组件传递的ref
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  
  // 自定义暴露给父组件的实例值
  useImperativeHandle(ref, () => ({
    // 只暴露我们想要父组件访问的方法
    focus: () => {
      inputRef.current.focus();
    },
    blur: () => {
      inputRef.current.blur();
    },
    // 自定义方法
    setValue: (value) => {
      inputRef.current.value = value;
    }
  }));
  
  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const fancyInputRef = useRef();
  
  const focusInput = () => {
    fancyInputRef.current.focus();
  };
  
  const setInputValue = () => {
    fancyInputRef.current.setValue('Hello from parent!');
  };
  
  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={focusInput}>Focus Input</button>
      <button onClick={setInputValue}>Set Value</button>
    </div>
  );
}

useDebugValue

useDebugValue可用于在React DevTools中显示自定义hook的标签。

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

// 自定义Hook
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  // 在React DevTools中显示自定义标签
  useDebugValue(isOnline ? 'Online' : 'Offline');
  
  return isOnline;
}

// 延迟格式化
function useUserStatus(userId) {
  const [user, setUser] = useState(null);
  
  // 使用函数形式可以避免不必要的格式化
  useDebugValue(user, user => user ? `User: ${user.name}` : 'No user');
  
  // 获取用户逻辑...
  
  return user;
}

function StatusIndicator() {
  const isOnline = useOnlineStatus();
  
  return (
    <div>
      You are {isOnline ? 'online' : 'offline'}
    </div>
  );
}

自定义Hooks

自定义Hooks是一种重用状态逻辑的机制,它不复用state本身,而是复用状态逻辑。

创建自定义Hook

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

// 自定义Hook:获取窗口尺寸
function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组意味着这个effect只在挂载和卸载时运行
  
  return windowSize;
}

// 使用自定义Hook
function ResponsiveComponent() {
  const { width, height } = useWindowSize();
  
  return (
    <div>
      <p>Window width: {width}px</p>
      <p>Window height: {height}px</p>
      {width < 768 ? (
        <p>You are on a mobile device</p>
      ) : (
        <p>You are on a desktop</p>
      )}
    </div>
  );
}

带参数的自定义Hook

jsx
// 自定义Hook:获取API数据
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        if (isMounted) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    // 清理函数
    return () => {
      isMounted = false;
    };
  }, [url, JSON.stringify(options)]); // 依赖于url和options
  
  return { data, loading, error };
}

// 使用自定义Hook
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(
    `https://api.example.com/users/${userId}`
  );
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

组合多个Hooks

jsx
// 自定义Hook:表单处理
function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prevValues => ({
      ...prevValues,
      [name]: value
    }));
  };
  
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prevTouched => ({
      ...prevTouched,
      [name]: true
    }));
  };
  
  const handleSubmit = (onSubmit) => (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    // 这里可以添加验证逻辑
    
    onSubmit(values);
    setIsSubmitting(false);
  };
  
  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setIsSubmitting(false);
  };
  
  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  };
}

// 使用自定义Hook
function SignupForm() {
  const {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  } = useForm({
    username: '',
    email: '',
    password: ''
  });
  
  const submitForm = async (formValues) => {
    try {
      // 发送数据到服务器
      await fetch('https://api.example.com/signup', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formValues)
      });
      
      alert('Signup successful!');
      reset();
    } catch (error) {
      alert(`Error: ${error.message}`);
    }
  };
  
  return (
    <form onSubmit={handleSubmit(submitForm)}>
      <div>
        <label htmlFor="username">Username</label>
        <input
          id="username"
          name="username"
          type="text"
          value={values.username}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.username && errors.username && (
          <div className="error">{errors.username}</div>
        )}
      </div>
      
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && (
          <div className="error">{errors.email}</div>
        )}
      </div>
      
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          name="password"
          type="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && (
          <div className="error">{errors.password}</div>
        )}
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Sign Up'}
      </button>
    </form>
  );
}

Hooks的最佳实践

1. 遵循Hooks规则

  • 只在最顶层调用Hooks
  • 只在React函数组件和自定义Hooks中调用Hooks

2. 使用ESLint插件

bash
npm install eslint-plugin-react-hooks --save-dev

在ESLint配置中:

json
{
  "plugins": [
    "react-hooks"
  ],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

3. 正确管理依赖数组

jsx
// 不好的做法 - 缺少依赖项
function Example({ id }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData(id).then(setData);
  }, []); // 缺少id依赖项
  
  // ...
}

// 好的做法 - 包含所有依赖项
function Example({ id }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData(id).then(setData);
  }, [id]); // 正确包含id依赖项
  
  // ...
}

4. 避免过度使用状态

jsx
// 不好的做法 - 过度使用状态
function UserForm() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [address, setAddress] = useState('');
  const [city, setCity] = useState('');
  const [state, setState] = useState('');
  const [zip, setZip] = useState('');
  
  // 每个字段都需要单独的处理函数
  const handleFirstNameChange = (e) => setFirstName(e.target.value);
  const handleLastNameChange = (e) => setLastName(e.target.value);
  // ... 更多处理函数
  
  // ...
}

// 好的做法 - 使用单个状态对象
function UserForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    address: '',
    city: '',
    state: '',
    zip: ''
  });
  
  // 单个通用处理函数
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: value
    }));
  };
  
  // ...
}

5. 使用函数式更新

jsx
// 不好的做法 - 可能导致竞态条件
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    // 如果多次调用,可能不会正确更新
    setCount(count + 1);
  };
  
  // ...
}

// 好的做法 - 使用函数式更新
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    // 总是基于最新的状态更新
    setCount(prevCount => prevCount + 1);
  };
  
  // ...
}

6. 提取复杂逻辑到自定义Hooks

jsx
// 不好的做法 - 组件中包含复杂逻辑
function UserDashboard({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const userData = await fetchUser(userId);
        setUser(userData);
        const userPosts = await fetchPosts(userId);
        setPosts(userPosts);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [userId]);
  
  // 渲染逻辑...
}

// 好的做法 - 提取到自定义Hook
function useUserData(userId) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const userData = await fetchUser(userId);
        setUser(userData);
        const userPosts = await fetchPosts(userId);
        setPosts(userPosts);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [userId]);
  
  return { user, posts, loading, error };
}

function UserDashboard({ userId }) {
  const { user, posts, loading, error } = useUserData(userId);
  
  // 简化的渲染逻辑...
}

7. 使用useCallback和useMemo优化性能

jsx
// 不好的做法 - 每次渲染都创建新函数和计算值
function SearchResults({ query, threshold }) {
  const [results, setResults] = useState([]);
  
  // 每次渲染都会创建新函数
  const fetchResults = async () => {
    const data = await fetch(`/api/search?q=${query}`);
    const json = await data.json();
    setResults(json);
  };
  
  // 每次渲染都会重新计算
  const filteredResults = results.filter(
    item => item.score > threshold
  );
  
  // ...
}

// 好的做法 - 使用useCallback和useMemo
function SearchResults({ query, threshold }) {
  const [results, setResults] = useState([]);
  
  // 只有当query变化时才创建新函数
  const fetchResults = useCallback(async () => {
    const data = await fetch(`/api/search?q=${query}`);
    const json = await data.json();
    setResults(json);
  }, [query]);
  
  // 只有当results或threshold变化时才重新计算
  const filteredResults = useMemo(() => {
    return results.filter(item => item.score > threshold);
  }, [results, threshold]);
  
  // ...
}

8. 使用useReducer管理复杂状态

jsx
// 不好的做法 - 使用多个useState管理相关状态
function ShoppingCart() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const addItem = (item) => {
    setItems([...items, item]);
    setTotal(total + item.price);
  };
  
  const removeItem = (itemId) => {
    const item = items.find(i => i.id === itemId);
    setItems(items.filter(i => i.id !== itemId));
    setTotal(total - item.price);
  };
  
  // 更多操作...
}

// 好的做法 - 使用useReducer管理相关状态
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price
      };
    case 'REMOVE_ITEM':
      const item = state.items.find(i => i.id === action.payload);
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.payload),
        total: state.total - item.price
      };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: [],
    total: 0,
    loading: false,
    error: null
  });
  
  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };
  
  const removeItem = (itemId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: itemId });
  };
  
  // 更多操作...
}