Skip to content

useState.jsx

文件信息

  • 📄 原文件:01_useState.jsx
  • 🔤 语言:jsx

React useState Hook useState 是 React 最基础的 Hook,用于在函数组件中添加状态。 状态变化会触发组件重新渲染。

完整代码

jsx
/**
 * ============================================================
 *                    React useState Hook
 * ============================================================
 * useState 是 React 最基础的 Hook,用于在函数组件中添加状态。
 * 状态变化会触发组件重新渲染。
 * ============================================================
 */

import React, { useState } from 'react';

// ============================================================
//                    1. useState 基础
// ============================================================

/**
 * 【useState 是什么】
 *
 * useState 让函数组件拥有状态管理能力:
 * - 返回一个数组:[当前状态值, 更新状态的函数]
 * - 状态更新会触发组件重新渲染
 * - 每次渲染都有自己的状态快照
 *
 * 【语法】
 * const [state, setState] = useState(initialValue);
 *
 * - state: 当前状态值
 * - setState: 更新状态的函数
 * - initialValue: 初始值(只在首次渲染时使用)
 */

// --- 基本计数器 ---
function Counter() {
    // 声明状态变量 count,初始值为 0
    // setCount 是更新 count 的函数
    const [count, setCount] = useState(0);

    // 点击处理函数
    const handleIncrement = () => {
        setCount(count + 1);  // 更新状态
    };

    const handleDecrement = () => {
        setCount(count - 1);
    };

    const handleReset = () => {
        setCount(0);
    };

    return (
        <div className="counter">
            <h2>计数器: {count}</h2>
            <button onClick={handleDecrement}>-1</button>
            <button onClick={handleReset}>重置</button>
            <button onClick={handleIncrement}>+1</button>
        </div>
    );
}


// ============================================================
//                    2. 不同类型的状态
// ============================================================

/**
 * 【状态可以是任意类型】
 *
 * - 基本类型:number, string, boolean
 * - 引用类型:object, array
 *
 * 【注意】
 * 对于对象和数组,需要创建新的引用才能触发更新
 * 不能直接修改原对象/数组
 */

// --- 字符串状态 ---
function TextInput() {
    const [text, setText] = useState('');

    const handleChange = (event) => {
        setText(event.target.value);
    };

    return (
        <div>
            <input
                type="text"
                value={text}
                onChange={handleChange}
                placeholder="输入文字..."
            />
            <p>你输入了: {text}</p>
            <p>字符数: {text.length}</p>
        </div>
    );
}

// --- 布尔状态 ---
function Toggle() {
    const [isOn, setIsOn] = useState(false);

    // 切换状态
    const toggle = () => {
        setIsOn(!isOn);
    };

    return (
        <div>
            <button onClick={toggle}>
                {isOn ? '关闭' : '开启'}
            </button>
            <p>状态: {isOn ? 'ON' : 'OFF'}</p>
        </div>
    );
}

// --- 对象状态 ---
function UserForm() {
    // 对象作为状态
    const [user, setUser] = useState({
        name: '',
        email: '',
        age: 0,
    });

    // 更新对象的某个属性
    // 必须创建新对象,不能直接修改
    const handleNameChange = (event) => {
        setUser({
            ...user,           // 展开旧对象
            name: event.target.value,  // 覆盖 name
        });
    };

    const handleEmailChange = (event) => {
        setUser({
            ...user,
            email: event.target.value,
        });
    };

    // 通用的字段更新函数
    const handleFieldChange = (field) => (event) => {
        setUser({
            ...user,
            [field]: event.target.value,
        });
    };

    return (
        <div>
            <h3>用户表单</h3>
            <input
                type="text"
                value={user.name}
                onChange={handleNameChange}
                placeholder="姓名"
            />
            <input
                type="email"
                value={user.email}
                onChange={handleEmailChange}
                placeholder="邮箱"
            />
            <input
                type="number"
                value={user.age}
                onChange={handleFieldChange('age')}
                placeholder="年龄"
            />
            <pre>{JSON.stringify(user, null, 2)}</pre>
        </div>
    );
}

// --- 数组状态 ---
function TodoList() {
    const [todos, setTodos] = useState([
        { id: 1, text: '学习 React', completed: false },
        { id: 2, text: '写代码', completed: true },
    ]);
    const [inputText, setInputText] = useState('');

    // 添加项目(创建新数组)
    const addTodo = () => {
        if (!inputText.trim()) return;

        const newTodo = {
            id: Date.now(),  // 简单的唯一 ID
            text: inputText,
            completed: false,
        };

        // 方式1: 展开运算符
        setTodos([...todos, newTodo]);

        // 方式2: concat
        // setTodos(todos.concat(newTodo));

        setInputText('');  // 清空输入
    };

    // 删除项目(过滤创建新数组)
    const deleteTodo = (id) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    // 切换完成状态(map 创建新数组)
    const toggleTodo = (id) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };

    return (
        <div>
            <h3>待办列表</h3>
            <div>
                <input
                    type="text"
                    value={inputText}
                    onChange={(e) => setInputText(e.target.value)}
                    placeholder="添加待办..."
                    onKeyDown={(e) => e.key === 'Enter' && addTodo()}
                />
                <button onClick={addTodo}>添加</button>
            </div>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span style={{
                            textDecoration: todo.completed ? 'line-through' : 'none'
                        }}>
                            {todo.text}
                        </span>
                        <button onClick={() => deleteTodo(todo.id)}>删除</button>
                    </li>
                ))}
            </ul>
            <p>总数: {todos.length}, 已完成: {todos.filter(t => t.completed).length}</p>
        </div>
    );
}


// ============================================================
//                    3. 函数式更新
// ============================================================

/**
 * 【函数式更新】
 *
 * 当新状态依赖于旧状态时,应该使用函数式更新:
 * setState(prevState => newState)
 *
 * 【为什么需要函数式更新】
 * - 状态更新是异步的
 * - 多次调用 setState 可能会被合并
 * - 函数式更新确保获取最新的状态值
 */

// --- 问题示例 ---
function ProblemCounter() {
    const [count, setCount] = useState(0);

    // ❌ 错误:多次调用只会增加 1
    const incrementThreeTimes = () => {
        // 这三次调用都基于同一个 count 快照
        setCount(count + 1);  // count = 0, 设置为 1
        setCount(count + 1);  // count = 0, 设置为 1
        setCount(count + 1);  // count = 0, 设置为 1
        // 结果:count = 1(不是 3)
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={incrementThreeTimes}>
                增加三次(错误方式)
            </button>
        </div>
    );
}

// --- 正确示例 ---
function CorrectCounter() {
    const [count, setCount] = useState(0);

    // ✅ 正确:使用函数式更新
    const incrementThreeTimes = () => {
        // 每次都基于最新的 prevCount
        setCount(prevCount => prevCount + 1);  // 0 -> 1
        setCount(prevCount => prevCount + 1);  // 1 -> 2
        setCount(prevCount => prevCount + 1);  // 2 -> 3
        // 结果:count = 3
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={incrementThreeTimes}>
                增加三次(正确方式)
            </button>
        </div>
    );
}

// --- 函数式更新用于对象和数组 ---
function ShoppingCart() {
    const [cart, setCart] = useState([]);

    const addItem = (item) => {
        // 函数式更新确保获取最新的 cart
        setCart(prevCart => {
            // 检查是否已存在
            const existingItem = prevCart.find(i => i.id === item.id);

            if (existingItem) {
                // 已存在,增加数量
                return prevCart.map(i =>
                    i.id === item.id
                        ? { ...i, quantity: i.quantity + 1 }
                        : i
                );
            }

            // 不存在,添加新项
            return [...prevCart, { ...item, quantity: 1 }];
        });
    };

    const removeItem = (id) => {
        setCart(prevCart => prevCart.filter(item => item.id !== id));
    };

    const updateQuantity = (id, quantity) => {
        setCart(prevCart =>
            prevCart.map(item =>
                item.id === id
                    ? { ...item, quantity: Math.max(0, quantity) }
                    : item
            ).filter(item => item.quantity > 0)  // 移除数量为 0 的
        );
    };

    return (
        <div>
            <h3>购物车</h3>
            {cart.map(item => (
                <div key={item.id}>
                    <span>{item.name} x {item.quantity}</span>
                    <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
                    <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
                    <button onClick={() => removeItem(item.id)}>删除</button>
                </div>
            ))}
            <button onClick={() => addItem({ id: 1, name: '商品A', price: 10 })}>
                添加商品A
            </button>
            <button onClick={() => addItem({ id: 2, name: '商品B', price: 20 })}>
                添加商品B
            </button>
        </div>
    );
}


// ============================================================
//                    4. 惰性初始化
// ============================================================

/**
 * 【惰性初始化】
 *
 * 如果初始状态需要复杂计算,可以传递函数:
 * useState(() => computeExpensiveValue())
 *
 * 【好处】
 * - 函数只在首次渲染时执行
 * - 避免每次渲染都进行不必要的计算
 */

// --- 惰性初始化示例 ---
function LazyInitExample() {
    // ❌ 不好:每次渲染都会计算
    // const [value, setValue] = useState(expensiveComputation());

    // ✅ 好:只在首次渲染时计算
    const [value, setValue] = useState(() => {
        console.log('惰性初始化执行');
        // 模拟复杂计算
        return heavyComputation();
    });

    return (
        <div>
            <p>值: {value}</p>
            <button onClick={() => setValue(v => v + 1)}>增加</button>
        </div>
    );
}

function heavyComputation() {
    // 模拟耗时计算
    let result = 0;
    for (let i = 0; i < 1000; i++) {
        result += i;
    }
    return result;
}

// --- 从 localStorage 读取初始值 ---
function PersistentCounter() {
    // 惰性初始化:从 localStorage 读取
    const [count, setCount] = useState(() => {
        const saved = localStorage.getItem('counter');
        return saved ? parseInt(saved, 10) : 0;
    });

    // 状态变化时保存到 localStorage
    const updateCount = (newCount) => {
        setCount(newCount);
        localStorage.setItem('counter', newCount.toString());
    };

    return (
        <div>
            <p>持久化计数: {count}</p>
            <button onClick={() => updateCount(count + 1)}>增加</button>
            <button onClick={() => updateCount(0)}>重置</button>
        </div>
    );
}


// ============================================================
//                    5. 多个状态
// ============================================================

/**
 * 【多个状态】
 *
 * 可以在一个组件中使用多个 useState:
 * - 每个状态独立管理
 * - 按逻辑分组状态
 *
 * 【何时使用多个 useState vs 一个对象】
 * - 相关的数据放在一起(用对象)
 * - 不相关的数据分开(用多个 useState)
 */

// --- 多个独立状态 ---
function MultipleStates() {
    // 不相关的状态分开管理
    const [name, setName] = useState('');
    const [age, setAge] = useState(0);
    const [isSubscribed, setIsSubscribed] = useState(false);

    return (
        <div>
            <input
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="姓名"
            />
            <input
                type="number"
                value={age}
                onChange={(e) => setAge(parseInt(e.target.value) || 0)}
                placeholder="年龄"
            />
            <label>
                <input
                    type="checkbox"
                    checked={isSubscribed}
                    onChange={(e) => setIsSubscribed(e.target.checked)}
                />
                订阅通知
            </label>
            <p>
                {name}, {age}岁, {isSubscribed ? '已订阅' : '未订阅'}
            </p>
        </div>
    );
}

// --- 相关状态用对象 ---
function FormWithObject() {
    // 相关的表单数据用一个对象
    const [formData, setFormData] = useState({
        username: '',
        password: '',
        confirmPassword: '',
    });

    // UI 状态单独管理
    const [isSubmitting, setIsSubmitting] = useState(false);
    const [errors, setErrors] = useState({});

    const handleChange = (field) => (event) => {
        setFormData(prev => ({
            ...prev,
            [field]: event.target.value,
        }));
    };

    const handleSubmit = async (event) => {
        event.preventDefault();

        // 验证
        const newErrors = {};
        if (!formData.username) {
            newErrors.username = '用户名不能为空';
        }
        if (formData.password !== formData.confirmPassword) {
            newErrors.confirmPassword = '密码不匹配';
        }

        if (Object.keys(newErrors).length > 0) {
            setErrors(newErrors);
            return;
        }

        setIsSubmitting(true);
        // 模拟提交
        await new Promise(resolve => setTimeout(resolve, 1000));
        setIsSubmitting(false);

        console.log('提交成功', formData);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input
                    value={formData.username}
                    onChange={handleChange('username')}
                    placeholder="用户名"
                />
                {errors.username && <span className="error">{errors.username}</span>}
            </div>
            <div>
                <input
                    type="password"
                    value={formData.password}
                    onChange={handleChange('password')}
                    placeholder="密码"
                />
            </div>
            <div>
                <input
                    type="password"
                    value={formData.confirmPassword}
                    onChange={handleChange('confirmPassword')}
                    placeholder="确认密码"
                />
                {errors.confirmPassword && (
                    <span className="error">{errors.confirmPassword}</span>
                )}
            </div>
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? '提交中...' : '提交'}
            </button>
        </form>
    );
}


// ============================================================
//                    6. 状态提升
// ============================================================

/**
 * 【状态提升】
 *
 * 当多个组件需要共享状态时:
 * 1. 将状态提升到最近的共同父组件
 * 2. 通过 props 传递状态和更新函数
 *
 * 这是 React 数据流的核心模式
 */

// 子组件:温度输入
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
    const scaleNames = {
        c: '摄氏度',
        f: '华氏度',
    };

    return (
        <div>
            <label>输入{scaleNames[scale]}:</label>
            <input
                type="number"
                value={temperature}
                onChange={(e) => onTemperatureChange(e.target.value)}
            />
        </div>
    );
}

// 父组件:管理共享状态
function TemperatureCalculator() {
    // 状态提升到父组件
    const [temperature, setTemperature] = useState('');
    const [scale, setScale] = useState('c');

    // 转换函数
    const toCelsius = (fahrenheit) => {
        return ((fahrenheit - 32) * 5 / 9).toFixed(2);
    };

    const toFahrenheit = (celsius) => {
        return ((celsius * 9 / 5) + 32).toFixed(2);
    };

    // 处理摄氏度变化
    const handleCelsiusChange = (temp) => {
        setScale('c');
        setTemperature(temp);
    };

    // 处理华氏度变化
    const handleFahrenheitChange = (temp) => {
        setScale('f');
        setTemperature(temp);
    };

    // 计算显示值
    const celsius = scale === 'f' ? toCelsius(parseFloat(temperature)) : temperature;
    const fahrenheit = scale === 'c' ? toFahrenheit(parseFloat(temperature)) : temperature;

    return (
        <div>
            <h3>温度转换器</h3>
            <TemperatureInput
                scale="c"
                temperature={celsius}
                onTemperatureChange={handleCelsiusChange}
            />
            <TemperatureInput
                scale="f"
                temperature={fahrenheit}
                onTemperatureChange={handleFahrenheitChange}
            />
            {temperature && (
                <p>
                    {celsius}°C = {fahrenheit}°F
                </p>
            )}
        </div>
    );
}


// ============================================================
//                    7. useState 最佳实践
// ============================================================

/**
 * 【最佳实践总结】
 *
 * 1. 最小化状态
 *    - 只存储必要的数据
 *    - 派生数据用计算代替存储
 *
 * 2. 状态结构
 *    - 相关数据放在一起
 *    - 避免深层嵌套
 *
 * 3. 更新方式
 *    - 依赖旧值时用函数式更新
 *    - 对象/数组必须创建新引用
 *
 * 4. 性能考虑
 *    - 复杂初始值用惰性初始化
 *    - 频繁更新考虑使用 useReducer
 */

// --- 派生状态示例 ---
function ProductFilter() {
    const [products] = useState([
        { id: 1, name: 'iPhone', category: '电子', price: 999 },
        { id: 2, name: 'MacBook', category: '电子', price: 1999 },
        { id: 3, name: 'T恤', category: '服装', price: 29 },
        { id: 4, name: '牛仔裤', category: '服装', price: 59 },
    ]);

    const [searchTerm, setSearchTerm] = useState('');
    const [selectedCategory, setSelectedCategory] = useState('全部');

    // ✅ 派生数据:通过计算得到,不需要额外的状态
    const filteredProducts = products.filter(product => {
        const matchesSearch = product.name
            .toLowerCase()
            .includes(searchTerm.toLowerCase());
        const matchesCategory =
            selectedCategory === '全部' ||
            product.category === selectedCategory;

        return matchesSearch && matchesCategory;
    });

    const categories = ['全部', ...new Set(products.map(p => p.category))];

    return (
        <div>
            <input
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
                placeholder="搜索商品..."
            />
            <select
                value={selectedCategory}
                onChange={(e) => setSelectedCategory(e.target.value)}
            >
                {categories.map(cat => (
                    <option key={cat} value={cat}>{cat}</option>
                ))}
            </select>

            <ul>
                {filteredProducts.map(product => (
                    <li key={product.id}>
                        {product.name} - ${product.price}
                    </li>
                ))}
            </ul>
            <p>显示 {filteredProducts.length} / {products.length} 个商品</p>
        </div>
    );
}


// ============================================================
//                    导出
// ============================================================

export {
    Counter,
    TextInput,
    Toggle,
    UserForm,
    TodoList,
    ProblemCounter,
    CorrectCounter,
    ShoppingCart,
    LazyInitExample,
    PersistentCounter,
    MultipleStates,
    FormWithObject,
    TemperatureCalculator,
    ProductFilter,
};

export default function UseStateTutorial() {
    return (
        <div className="tutorial">
            <h1>useState Hook 教程</h1>
            <Counter />
            <Toggle />
            <TextInput />
            <TodoList />
            <TemperatureCalculator />
        </div>
    );
}

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布