Skip to content

useState Hook

本文介绍React的useState Hook,包括基本用法、状态更新逻辑、函数式更新以及使用注意事项。

基本概念

useState是React提供的一个Hook,允许函数组件添加和管理本地状态。它是React Hooks API中最基础也是最常用的Hook之一。

基本用法

导入useState

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

声明状态变量

jsx
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>
  );
}

useState返回一个数组,包含两个元素:

  1. 当前状态值
  2. 更新状态的函数

我们使用数组解构来获取这两个值。

使用多个状态变量

可以在同一个组件中多次调用useState来声明多个状态变量:

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"
        type="email"
      />
    </form>
  );
}

使用对象作为状态

也可以使用对象来组织相关的状态:

jsx
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: ''
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser({
      ...user, // 保留其他字段
      [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"
        type="email"
      />
    </form>
  );
}

状态更新

基本更新

调用状态更新函数会触发组件重新渲染:

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

函数式更新

如果新的状态依赖于之前的状态,应该使用函数式更新形式:

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    // 函数式更新,prevCount是前一个状态值
    setCount(prevCount => prevCount + 1);
  };
  
  // 这在处理异步更新时特别重要
  const incrementThreeTimes = () => {
    // 错误方式:这样只会+1,因为所有setCount都基于同一个count值
    // setCount(count + 1);
    // setCount(count + 1);
    // setCount(count + 1);
    
    // 正确方式:使用函数式更新,确保每次更新都基于最新状态
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={incrementThreeTimes}>+3</button>
    </div>
  );
}

更新对象状态

更新对象状态时,需要创建一个新对象,而不是修改原对象:

jsx
function UserProfile() {
  const [profile, setProfile] = useState({
    name: 'John',
    email: 'john@example.com',
    preferences: {
      theme: 'light',
      notifications: true
    }
  });
  
  const updateTheme = (theme) => {
    // 错误方式:直接修改原对象
    // profile.preferences.theme = theme;
    // setProfile(profile);
    
    // 正确方式:创建新对象
    setProfile({
      ...profile,
      preferences: {
        ...profile.preferences,
        theme
      }
    });
  };
  
  return (
    <div>
      <h2>{profile.name}</h2>
      <p>{profile.email}</p>
      <p>Theme: {profile.preferences.theme}</p>
      <button onClick={() => updateTheme('dark')}>Dark Theme</button>
      <button onClick={() => updateTheme('light')}>Light Theme</button>
    </div>
  );
}

更新数组状态

更新数组状态时,同样需要创建新数组:

jsx
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  
  const addTodo = () => {
    if (input.trim()) {
      // 创建新数组
      setTodos([...todos, { id: Date.now(), text: input, completed: false }]);
      setInput('');
    }
  };
  
  const toggleTodo = (id) => {
    // 映射创建新数组
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };
  
  const removeTodo = (id) => {
    // 过滤创建新数组
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <div>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="Add a todo"
      />
      <button onClick={addTodo}>Add</button>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

惰性初始化

如果初始状态需要通过复杂计算获得,可以传递一个函数给useState,该函数只会在组件的初始渲染时被调用:

jsx
function ExpensiveInitialState() {
  // 这个函数只在组件首次渲染时执行一次
  const [state, setState] = useState(() => {
    console.log('Computing initial state...');
    // 假设这是一个昂贵的计算
    return computeExpensiveValue();
  });
  
  return (
    <div>
      <p>Value: {state}</p>
      <button onClick={() => setState(state + 1)}>Update</button>
    </div>
  );
}

function computeExpensiveValue() {
  // 模拟复杂计算
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += i;
  }
  return result;
}

使用localStorage持久化状态

结合useStateuseEffect可以实现状态的持久化:

jsx
function PersistentCounter() {
  // 从localStorage读取初始值,如果没有则使用0
  const [count, setCount] = useState(() => {
    const savedCount = localStorage.getItem('count');
    return savedCount !== null ? parseInt(savedCount, 10) : 0;
  });
  
  // 当count变化时,保存到localStorage
  useEffect(() => {
    localStorage.setItem('count', count.toString());
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

自定义Hook封装状态逻辑

可以创建自定义Hook来封装和复用状态逻辑:

jsx
// 自定义Hook: useLocalStorage
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;
    }
  });
  
  // 更新函数,同时更新状态和localStorage
  const setValue = value => {
    try {
      // 允许值是函数,类似于useState
      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];
}

// 使用自定义Hook
function PersistentForm() {
  const [name, setName] = useLocalStorage('name', '');
  const [email, setEmail] = useLocalStorage('email', '');
  
  return (
    <form>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
        type="email"
      />
      <p>Refresh the page to see the persistence in action!</p>
    </form>
  );
}

常见陷阱和最佳实践

1. 状态更新是异步的

状态更新可能是异步的,不要依赖前一个setState调用后立即获取更新后的状态:

jsx
function AsyncUpdateExample() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1);
    // 这里的count仍然是旧值
    console.log(count); // 仍然是更新前的值
    
    // 如果需要在更新后执行操作,使用useEffect
  };
  
  // 当count变化时执行
  useEffect(() => {
    console.log('Count updated:', count);
  }, [count]);
  
  return (
    <button onClick={handleClick}>Increment</button>
  );
}

2. 避免过多的状态变量

当多个状态变量相互关联时,考虑合并它们:

jsx
// 不推荐:使用多个独立状态
function UserForm() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [isValid, setIsValid] = useState(false);
  const [errors, setErrors] = useState({});
  
  // 处理逻辑复杂,需要同步多个状态
  // ...
}

// 推荐:合并相关状态
function UserForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [formState, setFormState] = useState({
    isValid: false,
    errors: {}
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };
  
  // 验证逻辑更清晰
  useEffect(() => {
    // 验证表单并更新formState
    // ...
  }, [formData]);
  
  // ...
}

3. 使用函数式更新处理频繁更新

当状态更新依赖于之前的状态,特别是在短时间内多次更新时,使用函数式更新:

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  // 不好的做法:可能导致更新丢失
  const badIncrement = () => {
    setCount(count + 1);
  };
  
  // 好的做法:确保基于最新状态更新
  const goodIncrement = () => {
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={goodIncrement}>Increment</button>
    </div>
  );
}

4. 避免在渲染期间创建新对象或函数

每次渲染时创建新对象或函数可能导致不必要的重新渲染,考虑使用useMemouseCallback

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

function ExpensiveCalculation({ a, b }) {
  // 不好的做法:每次渲染都创建新对象
  const badObject = { a, b, sum: a + b };
  
  // 好的做法:仅在依赖变化时创建新对象
  const goodObject = useMemo(() => ({ a, b, sum: a + b }), [a, b]);
  
  // 不好的做法:每次渲染都创建新函数
  const badHandleClick = () => {
    console.log(a + b);
  };
  
  // 好的做法:仅在依赖变化时创建新函数
  const goodHandleClick = useCallback(() => {
    console.log(a + b);
  }, [a, b]);
  
  return (
    <div>
      <p>Sum: {goodObject.sum}</p>
      <button onClick={goodHandleClick}>Log Sum</button>
    </div>
  );
}

5. 使用状态初始化函数避免重复计算

jsx
// 不好的做法:每次渲染都会执行昂贵计算
function BadComponent() {
  // 即使这个值只在初始化时使用,但计算会在每次渲染时执行
  const initialState = performExpensiveCalculation();
  const [state, setState] = useState(initialState);
  // ...
}

// 好的做法:只在初始化时执行昂贵计算
function GoodComponent() {
  const [state, setState] = useState(() => performExpensiveCalculation());
  // ...
}

与其他Hooks的结合使用

useState + useEffect

jsx
function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);
    
    // 清除函数,组件卸载时调用
    return () => clearInterval(interval);
  }, []); // 空依赖数组表示只在挂载和卸载时执行
  
  return <p>Seconds: {seconds}</p>;
}

useState + useContext

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

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

// 提供Context的组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 使用Context的组件
function ThemedButton() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <button
      style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#333' : '#fff' }}
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      Toggle Theme
    </button>
  );
}

// 应用组件
function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>Themed App</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

useState + useReducer

对于复杂的状态逻辑,可以考虑使用useReducer代替useState

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 };
    default:
      return state;
  }
}

function Counter() {
  // 使用useReducer代替useState处理复杂逻辑
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  
  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>
  );
}

总结

useState Hook是React函数组件中管理状态的基础工具,它简单易用,但也有一些需要注意的细节:

  • 使用函数式更新处理依赖于之前状态的更新
  • 更新对象和数组时创建新的副本,不要直接修改
  • 对于复杂的初始状态计算,使用惰性初始化
  • 考虑将相关状态合并为一个对象
  • 对于复杂的状态逻辑,考虑使用useReducer
  • 结合其他Hooks如useEffectuseContext等可以构建强大的状态管理解决方案

掌握useState是学习React Hooks的第一步,它为函数组件提供了与类组件相当的状态管理能力,同时保持了函数组件的简洁性和可组合性。