useState:组件的"记忆"

import { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    //     ↑ 当前值  ↑ 改它的函数  ↑ 初值

    return (
        <button onClick={() => setCount(count + 1)}>
            Clicked {count} times
        </button>
    );
}

每次点击 → setCount 更新 → React 重新渲染组件 → 显示新值。

多个 state

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

    return (
        <form>
            <input value={name} onChange={e => setName(e.target.value)} />
            <input type="number" value={age} onChange={e => setAge(Number(e.target.value))} />
            <p>{name}, {age}</p>
        </form>
    );
}

函数式更新(基于上次值)

// ❌ 容易错
setCount(count + 1);

// ✓ 当依赖前次值时更稳
setCount(prev => prev + 1);

何时必须用函数式:在 setInterval / setTimeout / 异步代码里。

useEffect(() => {
    const id = setInterval(() => {
        setCount(prev => prev + 1);    // ✓ 不会"卡在" 0
    }, 1000);
    return () => clearInterval(id);
}, []);

对象 / 数组 state

永远返回新对象 / 数组——不要 mutate:

// ❌ 改了 React 不知道
user.name = 'Bob';
setUser(user);

// ✓ 返回新对象
setUser({ ...user, name: 'Bob' });

// 数组也是
setItems([...items, newItem]);                // 加
setItems(items.filter(i => i.id !== id));     // 删
setItems(items.map(i => i.id === id ? { ...i, done: true } : i));  // 改

事件处理

onClick

<button onClick={() => alert('hi')}>Click</button>

// 提取函数
function handleClick() {
    alert('hi');
}
<button onClick={handleClick}>Click</button>

// 拿事件对象
<button onClick={(e) => {
    console.log(e.currentTarget);
    e.preventDefault();
}}>Click</button>

⚠ 区别:

onClick={handleClick}      // ✓ 引用函数(React 在点击时调用)
onClick={handleClick()}    // ❌ 立即调用(页面一渲染就触发)

onChange(输入)

const [text, setText] = useState('');

<input
    value={text}
    onChange={(e) => setText(e.target.value)}
/>

这是受控组件——React 控制 input 的值。

onSubmit(表单)

function handleSubmit(e: React.FormEvent) {
    e.preventDefault();        // ★ 阻止页面刷新
    console.log('submitting');
}

<form onSubmit={handleSubmit}>
    <input name="email" />
    <button type="submit">Submit</button>
</form>

传参数

function handleDelete(id: number) {
    setItems(items.filter(i => i.id !== id));
}

{items.map(item => (
    <li key={item.id}>
        {item.name}
        <button onClick={() => handleDelete(item.id)}>×</button>
    </li>
))}

state 是异步的

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

function handleClick() {
    setCount(count + 1);
    console.log(count);    // 还是旧值!
    // count 在下次渲染时才更新
}

如果需要立即拿到新值:

function handleClick() {
    const newCount = count + 1;
    setCount(newCount);
    console.log(newCount);    // ✓ 拿到新值
}

完整例子:todo list

'use client';
import { useState } from 'react';

interface Todo {
    id: number;
    text: string;
    done: boolean;
}

export default function TodoApp() {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [input, setInput] = useState('');

    function add() {
        if (!input.trim()) return;
        setTodos([...todos, { id: Date.now(), text: input, done: false }]);
        setInput('');
    }

    function toggle(id: number) {
        setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
    }

    function remove(id: number) {
        setTodos(todos.filter(t => t.id !== id));
    }

    return (
        <div className="p-4">
            <input value={input} onChange={e => setInput(e.target.value)} />
            <button onClick={add}>Add</button>
            <ul>
                {todos.map(t => (
                    <li key={t.id}>
                        <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
                        <span style={{ textDecoration: t.done ? 'line-through' : 'none' }}>
                            {t.text}
                        </span>
                        <button onClick={() => remove(t.id)}>×</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

注意第一行 'use client'; —— Next.js 里使用 useState 需要标记客户端组件(Server Component 不能用 useState)。详见 09-pages-routes。

  • 永远返回新对象 / 数组,不要 mutate
  • onClick 别加 () —— 加了立即调用
  • state 异步——console.log(count) 后面拿到的是旧值
  • Next.js 用 useState 要 'use client' 标记

下一篇:Tailwind 样式集成。