Skip to content
← 返回React列表
组件与状态

React 基础:组件、状态与 Hooks ​

前提、环境与入口 ​

先读 JavaScript 的函数、数组方法、模块与异步,再读本篇。React 根据组件的 props 与 state 描述界面;学习目标是把“数据怎样流动”与“外部系统何时同步”分清楚。

已有项目先按锁文件安装依赖并阅读 package.json 的脚本。新项目查 React 创建应用指南:生产应用根据路由、数据加载和部署需求选择框架;只练习组件时也可以使用官方列出的构建工具。Create React App 已弃用,创建新项目时按当前官方指南选择工具。

下面代码假定项目已经支持 JSX,并有 <div id="root"></div>。入口通常由工具生成;手工挂载的基本形式为:

jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode><App /></StrictMode>
);

ReactDOM.render 属于旧入口,不应与以上写法混用。JSX 是 JavaScript 语法扩展,需要构建工具转换,不是浏览器直接识别的 HTML。

JSX 与列表 ​

表达式放在 {} 中,条件用三元表达式或先在 JavaScript 中计算;if、变量声明等语句不能直接放进 JSX 表达式位置。

jsx
const songs = [{ id: 1, name: 'Song A' }, { id: 2, name: 'Song B' }];

export default function App() {
  const visible = songs.length > 0;
  return (
    <main className="songs">
      <h1 style={{ color: 'navy' }}>歌曲</h1>
      {visible ? <ul>{songs.map((song) => <li key={song.id}>{song.name}</li>)}</ul> : <p>暂无歌曲</p>}
    </main>
  );
}
  • 组件名以大写字母开头;返回一个根结构,可用 Fragment <>...</> 包裹多个节点。
  • 标签必须闭合,使用 className、htmlFor 等 DOM 属性名;aria-*、data-* 仍保留连字符。
  • 字符串、数字和元素可直接渲染;普通对象不能直接作为子节点。布尔值、null、undefined 通常不显示内容。
  • key 在同级列表中稳定且唯一,用于识别项身份,不只是消除警告。可插入、删除或重排的列表不要随意用索引当 key。
  • style 接收对象,颜色应写字符串;外部 CSS 仍可通过 import './app.css' 引入。

状态、事件与表单 ​

useState 保存组件状态,更新函数安排后续渲染;当前事件处理器仍看到本次渲染的状态快照。基于旧状态更新时用函数形式,多个连续更新也能正确累积。

jsx
import { useState } from 'react';

export default function App() {
  const [name, setName] = useState('');
  const [count, setCount] = useState(0);
  return (
    <main>
      <label htmlFor="name">名字</label>
      <input id="name" value={name} onChange={(event) => setName(event.target.value)} />
      <p>Hello, {name || 'visitor'}!</p>
      <button onClick={() => setCount((value) => value + 1)}>点击 {count} 次</button>
    </main>
  );
}

受控输入把 value 与状态连接并同步处理 onChange。非受控输入可用 defaultValue,在需要时通过 ref 或表单数据读取;不要在一个输入的生命周期中随意在两种模式间切换。

Hooks 通常在函数组件或自定义 Hook 顶层调用,不放在条件、循环或普通嵌套回调里。useState(() => calculate()) 可延迟计算初始值,计算函数必须纯净;开发 Strict Mode 可能额外调用它以检查问题。初始值不会在每次 props 变化时自动重置状态。

不可变更新与组件通信 ​

父组件通过 props 传数据,通过回调接收子组件操作;兄弟组件共享状态时,把它提升到最近公共父组件。props 和已有 state 都按只读快照对待。

jsx
import { useState } from 'react';

function Task({ task, onRemove }) {
  return <li>{task.name} <button onClick={() => onRemove(task.id)}>删除</button></li>;
}

export default function App() {
  const [tasks, setTasks] = useState([
    { id: 1, name: '学习 JSX' }, { id: 2, name: '练习状态更新' },
  ]);
  function remove(id) {
    setTasks((current) => current.filter((task) => task.id !== id));
  }
  return <ul>{tasks.map((task) => <Task key={task.id} task={task} onRemove={remove} />)}</ul>;
}

添加用 [...current, newTask],更新某项用 map 并创建新对象;展开只复制一层,嵌套对象也要复制需要修改的层。不要用 push、直接修改属性再把同一引用交回更新函数。

children 表示组件标签内部传入的内容;函数可作为约定的 render prop 传递,但不能直接当可显示文本渲染。深层组件共享数据可用 Context,先确认普通 props 是否已足够:

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

const ThemeContext = createContext('light');
function Panel() {
  const theme = useContext(ThemeContext);
  return <section className={theme}>内容</section>;
}
export default function App() {
  return <ThemeContext.Provider value="dark"><Panel /></ThemeContext.Provider>;
}

此处使用兼容 React 18/19 的 Provider 写法。类型约束可按项目使用 TypeScript;React 19 已移除函数组件的 propTypes 检查和 defaultProps 支持,函数默认值用参数解构 function List({ pageSize = 10 })。默认值只针对缺失或 undefined,不会替换 null。详见 React 19 升级指南。

Effect:同步外部系统 ​

Effect 用于订阅、定时器、网络等外部系统同步。由 props/state 可直接计算的值通常在渲染时计算;用户点击引发的操作通常放事件处理器,不必都转成 Effect。

依赖写法常见执行范围
无依赖数组每次提交后运行
[]挂载后运行,卸载清理;开发 Strict Mode 有额外检查周期
[query]挂载及依赖变化后重新同步

依赖应包含 Effect 中使用的响应式值。重新同步前先清理上次 Effect,卸载也清理。不要用“只执行一次”的假设规避清理。

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

function useWindowScroll() {
  const [y, setY] = useState(0);
  useEffect(() => {
    function update() { setY(window.scrollY); }
    update();
    window.addEventListener('scroll', update, { passive: true });
    return () => window.removeEventListener('scroll', update);
  }, []);
  return y;
}

export default function App() {
  const y = useWindowScroll();
  return <p style={{ minHeight: '200vh' }}>滚动距离:{y}</p>;
}

监听器放在 Effect 中,不能每次渲染都注册。定时器同理:保存 setInterval 返回的 ID,在清理函数里 clearInterval(id)。useEffect 参考说明了依赖和开发模式行为。

请求:调用、取消与错误 ​

Effect 回调本身不写成 async,因为它应返回清理函数或不返回值。内部定义异步函数并实际调用;下面组件需要一个返回 JSON 的同源接口。

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

export default function Items({ url }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    let ignore = false;
    setData(null);
    setError(null);
    async function load() {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const value = await response.json();
        if (!ignore) setData(value);
      } catch (failure) {
        if (!ignore) setError(failure);
      }
    }
    load();
    return () => { ignore = true; controller.abort(); };
  }, [url]);
  if (error) return <p role="alert">读取失败:{error.message}</p>;
  if (data === null) return <p>加载中…</p>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

取消与 ignore 防止已失效任务覆盖当前状态。真实应用还可能需要缓存、去重、服务端数据加载;采用框架或数据层提供的机制时不必重复手写一套请求 Effect。

Ref 与浏览器存储 ​

ref 保存不触发重渲染的值,也可关联 DOM。需要显示在界面的数据用 state。函数组件没有类实例;可通过组件定义的 ref 接口访问 DOM 或命令式句柄,不能笼统说“只有类组件能用 ref”。不同 React 版本传递 ref 的方式请查项目版本文档。

jsx
import { useRef } from 'react';

export default function App() {
  const input = useRef(null);
  return <><input ref={input} /><button onClick={() => input.current?.focus()}>聚焦</button></>;
}

需要保存字符串状态并在刷新后恢复时,可用下面明确约束的 Hook;这是浏览器端、固定 key、组件内同步示例,不提供跨标签页同步或 SSR 支持。

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

export function useStoredText(key, fallback = '') {
  const [text, setText] = useState(() => {
    try { return window.localStorage.getItem(key) ?? fallback; }
    catch { return fallback; }
  });
  useEffect(() => {
    try { window.localStorage.setItem(key, text); }
    catch (error) { console.error('无法保存到本地存储', error); }
  }, [key, text]);
  return [text, setText];
}

调用示例为 const [draft, setDraft] = useStoredText('draft')。key 在挂载期间应固定,切换 key 不会自动重新读取;需设计重新挂载或状态重载流程。存储内容可被同源脚本读取,不用来存放秘密。

维护旧类组件 ​

类组件仍可阅读和维护;新增组件通常按项目约定采用函数组件。类的状态更新与 Hooks 的整体替换语义不同,setState 合并对象的顶层字段。

jsx
import { Component } from 'react';

export default class Counter extends Component {
  state = { count: 0 };
  increment = () => this.setState((state) => ({ count: state.count + 1 }));
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

render 保持纯净;componentDidMount 设置订阅,componentDidUpdate 按条件同步变化,componentWillUnmount 清理。不要在 render 中更新状态,也不要在 componentDidUpdate 中无条件 setState。

分阶段练习 ​

  1. JSX:渲染带稳定 id 的评论列表,覆盖空列表;不把对象直接渲染到页面。
  2. 状态与通信:添加、删除评论,把单条评论拆成子组件;确认其他条目的输入状态不会因删除而错位。
  3. Effect:反复挂载与卸载滚动组件,确认没有重复监听;切换查询 URL,确认旧响应不会覆盖新状态。
  4. Ref 与存储:按钮聚焦输入框;刷新页面确认固定 key 的草稿可恢复,并考虑存储不可用的情况。
参考核对

评论数据使用{id,text},列表把key={comment.id}放在map直接生成的组件上;添加创建新数组,删除按id筛选,不能按过滤后下标删除。卸载滚动组件后清理函数应移除原函数引用,快速切URL时只允许当前Effect发布结果。聚焦通过input.current?.focus();固定draft键在新挂载时读取,读取失败使用fallback,写入失败按Hook示例记录错误。持久化失败不保证刷新可恢复;这些操作需在读者项目实际验证。

补充练习可参考历史课程的 评论组件 与 todoMVC Hooks。这些旧项目所需的 Node 版本、分支与 mock 服务按对应仓库说明恢复,不作为当前项目的初始化模板。

深入课程已接续:外部Store、Context与生命周期。基础内容与旧锚点保留,按当前路线进入完整机制和实践。

继续阅读