跳到主要内容

Props 和 State

React 组件通过两种方式处理数据:Props(属性)和 State(状态)。理解它们的区别和使用场景,是掌握 React 的第一步。

Props:组件的配置

Props 是从父组件传递给子组件的数据。可以把 Props 想象成函数的参数——父组件调用子组件时传入的"配置项"。

Props 的核心特点

只读性:子组件不应该修改接收到的 props。这条规则让组件的行为可预测,便于调试。

实时编辑器
function Welcome({ name, color = "#2563eb" }) {
  return (
    <div style={{ padding: '10px', borderLeft: `5px solid ${color}`, marginBottom: '10px' }}>
      <h3 style={{ color: color, margin: 0 }}>你好,{name}</h3>
      <p style={{ fontSize: '14px', color: '#666' }}>欢迎来到 React 世界</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <Welcome name="张三" color="#10b981" />
      <Welcome name="李四" color="#f59e0b" />
      <Welcome name="王五" />
    </div>
  );
}
结果
Loading...

上面的例子展示了 props 的几个用法:

  • 解构赋值从 props 对象中提取值
  • 默认值语法 color = "#2563eb"
  • 在 JSX 中使用 props 渲染动态内容

Props 传递模式

子组件向父组件传递数据:通过回调函数

实时编辑器
function Child({ onMessage }) {
  return (
    <button onClick={() => onMessage("来自子组件的问候!")}>
      向父组件发送消息
    </button>
  );
}

function Parent() {
  const [msg, setMsg] = useState("等待消息...");
  
  return (
    <div>
      <p style={{ color: '#666' }}>{msg}</p>
      <Child onMessage={(text) => setMsg(text)} />
    </div>
  );
}
结果
Loading...

这种"回调 props"模式是 React 中子组件与父组件通信的标准方式。


State:组件的记忆

State 是组件内部私有的数据,用于存储随时间变化的值。可以把 State 想象成组件的"记忆"——记录组件在某个时刻的状态。

useState Hook

实时编辑器
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ textAlign: 'center', padding: '20px', border: '1px dashed #ccc' }}>
      <h2 style={{ fontSize: '48px', margin: '10px 0' }}>{count}</h2>
      <div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
        <button onClick={() => setCount(count - 1)}>-1</button>
        <button onClick={() => setCount(count + 1)}>+1</button>
      </div>
    </div>
  );
}
结果
Loading...

State 更新的重要特性

异步更新:setState 不会立即改变 state,而是安排一次重新渲染。

// 错误:连续调用无法累加
function WrongExample() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1); // 基于旧的 count
setCount(count + 1); // 还是基于旧的 count
console.log(count); // 打印旧值
};

return <button onClick={handleClick}>点击</button>;
}

// 正确:使用函数式更新
function RightExample() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(prev => prev + 1); // 基于最新值
setCount(prev => prev + 1); // 基于最新值
// 最终 count 会增加 2
};

return <button onClick={handleClick}>点击</button>;
}

不可变性:不要直接修改 state 对象,而是创建新对象。

// 错误:直接修改
const [user, setUser] = useState({ name: '张三', age: 20 });
user.age = 21; // 不会触发重新渲染!
setUser(user);

// 正确:创建新对象
setUser({ ...user, age: 21 });

// 或使用 immer 库简化
import { produce } from 'immer';
setUser(produce(draft => { draft.age = 21; }));

Props vs State:核心对比

特性PropsState
来源父组件传入组件内部定义
可修改性只读(组件内不可改)可通过 setter 函数修改
作用组件的"配置参数"组件的"内部记忆"
触发更新父组件传入新值时调用 setter 函数时
初始值来源父组件决定组件自己决定

一个有用的思考方式:Props 像函数参数,State 像组件内部的变量


状态结构设计

好的状态结构能让组件更易维护、更少出bug。React 官方给出了几个指导原则。

避免冗余状态

不要存储可以从其他状态推导出的值。

// 不好的做法:存储了冗余的 fullName
function BadForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // 冗余!

// 需要同步更新三个状态,容易出错
}

// 好的做法:fullName 在渲染时计算
function GoodForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName; // 计算得出

return <p>欢迎,{fullName}</p>;
}

避免状态重复

当同一份数据在多个地方存储时,更新时容易遗漏某处。

// 不好的做法:items 和 selectedItem 都存储完整对象
const [items, setItems] = useState([...]);
const [selectedItem, setSelectedItem] = useState(items[0]);

// 如果更新 items 中的某项,selectedItem 不会自动更新!

// 好的做法:selectedItem 只存储 ID
const [items, setItems] = useState([...]);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item => item.id === selectedId);

合理拆分或合并状态

根据状态的更新频率和逻辑关联性来决定。

// 相关状态可以合并
const [position, setPosition] = useState({ x: 0, y: 0 });
// 而不是
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// 不相关的状态保持独立
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);

状态提升

当多个组件需要共享同一个状态时,将状态移动到它们最近的共同父组件中。

经典示例:同步输入

实时编辑器
function InputDisplay({ text, onTextChange }) {
  return (
    <input 
      value={text} 
      onChange={(e) => onTextChange(e.target.value)} 
      style={{ width: '100%', marginBottom: '10px', padding: '8px' }}
      placeholder="输入内容会同步到下方..."
    />
  );
}

function Mirror({ text }) {
  return (
    <div style={{ background: '#f3f4f6', padding: '10px', borderRadius: '4px' }}>
      镜像文本: <strong>{text}</strong>
    </div>
  );
}

function Parent() {
  const [text, setText] = useState("");
  return (
    <div>
      <InputDisplay text={text} onTextChange={setText} />
      <Mirror text={text} />
    </div>
  );
}
结果
Loading...

状态提升的核心思想:单一数据源。数据只存在于一个地方,其他组件通过 props 获取,通过回调更新。

状态提升 vs 双向绑定

Vue 等框架提供双向绑定,但 React 选择单向数据流:

数据流向:父 → 子(Props)
事件流向:子 → 父(Callbacks)

这种模式虽然需要更多代码,但数据流向清晰,便于调试和理解。


复杂状态管理:useReducer

当状态更新逻辑复杂时,useState 会变得难以维护。useReducer 提供了一种更结构化的方式。

useState vs useReducer

// 使用 useState:分散的更新逻辑
function TodoListUseState() {
const [todos, setTodos] = useState([]);

const handleAdd = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};

const handleToggle = (id) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};

const handleDelete = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
// ...
}

// 使用 useReducer:集中的更新逻辑
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'toggle':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'delete':
return state.filter(t => t.id !== action.id);
default:
throw new Error('Unknown action');
}
}

function TodoListUseReducer() {
const [todos, dispatch] = useReducer(todosReducer, []);

// 事件处理器变得简洁
const handleAdd = (text) => dispatch({ type: 'add', text });
const handleToggle = (id) => dispatch({ type: 'toggle', id });
const handleDelete = (id) => dispatch({ type: 'delete', id });
}

useReducer 适用场景

  • 状态逻辑复杂,有多个子值
  • 下一个状态依赖于之前的状态
  • 需要触发深层更新的复杂逻辑
  • 想要更易于测试的状态逻辑

跨组件状态共享:Context API

当状态需要在深层嵌套的组件间共享时,逐层传递 props 会很繁琐(称为"prop drilling")。Context API 解决了这个问题。

创建和使用 Context

// 1. 创建 Context
const ThemeContext = createContext('light');

// 2. 在父组件提供值
function App() {
const [theme, setTheme] = useState('dark');

return (
<ThemeContext.Provider value={theme}>
<DeepChild />
</ThemeContext.Provider>
);
}

// 3. 在深层子组件消费值
function DeepChild() {
const theme = useContext(ThemeContext);
return <div className={theme}>当前主题: {theme}</div>;
}

Context + useReducer:全局状态管理

实时编辑器
// 创建 Context
const CounterContext = createContext();

// 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;
  }
}

// Provider 组件
function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  
  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

// 子组件 A:显示计数
function CounterDisplay() {
  const { state } = useContext(CounterContext);
  return <h3 style={{ margin: '10px 0' }}>计数: {state.count}</h3>;
}

// 子组件 B:控制按钮
function CounterControls() {
  const { dispatch } = useContext(CounterContext);
  
  return (
    <div style={{ display: 'flex', gap: '10px' }}>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
    </div>
  );
}

// 组合使用
function App() {
  return (
    <CounterProvider>
      <div style={{ textAlign: 'center', padding: '20px' }}>
        <CounterDisplay />
        <CounterControls />
      </div>
    </CounterProvider>
  );
}
结果
Loading...

这种模式是 Zustand、Redux 等状态管理库的基础思想。


状态管理最佳实践

1. 状态就近原则

状态应该放在使用它的组件树的尽可能高的位置,但要足够低以覆盖所有需要它的组件。

App
├── Header # 不需要用户状态
├── Main
│ ├── Sidebar # 不需要用户状态
│ └── Content
│ ├── UserList # 需要用户状态
│ └── UserDetail # 需要用户状态
└── Footer # 不需要用户状态

# 用户状态应该放在 Content 组件,而不是 App

2. 区分 UI 状态和业务数据

// UI 状态(本地管理)
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState('info');

// 业务数据(可能需要共享或持久化)
const [user, setUser] = useState(null);
const [products, setProducts] = useState([]);

3. 何时使用外部状态管理库

对于大多数应用,React 内置的状态管理已经足够。考虑使用 Zustand、Jotai 或 Redux 的情况:

  • 状态需要在完全不相关的组件间共享
  • 状态需要持久化到 localStorage
  • 需要复杂的状态更新逻辑
  • 需要时间旅行调试

小结

  • Props 是父组件传入的配置,只读不可修改
  • State 是组件内部的状态,可以通过 setter 函数更新
  • 状态提升用于兄弟组件间的状态共享
  • useReducer 适合复杂的状态更新逻辑
  • Context API 解决深层组件的状态传递问题
  • 单向数据流是 React 状态管理的核心理念

练习

  1. 创建一个 Toggle 组件,点击按钮切换"开"和"关"状态
  2. 实现一个 TemperatureConverter,在摄氏度和华氏度之间双向同步(状态提升练习)
  3. 构建一个显示用户信息的卡片,Props 传入用户信息,State 控制卡片的展开/收起
  4. 使用 useReducer 重写一个简单的购物车逻辑(添加、删除、修改数量)
  5. 使用 Context 实现一个主题切换功能(明/暗模式)