Use Cases (更新: 2026/6/7)

把 React 基础画成一张地图:组件边界、Hooks、状态放哪里不再纠结

state 放哪里、useEffect 何时用、组件在哪儿切——把 React 的判断轴汇成一张地图,配可运行代码和 Claude Code 的协作方式。

把 React 基础画成一张地图:组件边界、Hooks、状态放哪里不再纠结

「React 到底该把什么写在哪儿?」

这是我被问得最多的一个问题。教程看完了,useStateuseEffect 的名字都认识,可一开始动手做页面,手就停住了。这个 state 该放在这个组件里,还是往父组件上提?数据是用 useEffect 拉,还是不用?组件到底在哪里切一刀?

纠结到最后,干脆把所有东西塞进一个巨大的组件里。能跑,但三天后的自己读不懂。我最开始也是这么过来的。

在 React 里卡住的原因,九成不是语法,而是手里没有一套设计的判断轴。这篇文章就是要把那套判断轴画成一张地图。各个细节(Hooks 的深入用法、状态管理库怎么选、表格和表单怎么实现)我会在中途分流到对应专题,你先在这里把全局看清楚。

本文要点

  • React 的设计,只要定好「state 放在哪里」和「组件的边界在哪里」这两件事,九成的混乱就消失了。
  • useEffect 是用来「和外部世界同步」的。能从 props 和 state 算出来的值,不需要 Effect
  • 重渲染不是坏事。只在你真的感觉到卡的时候,才加 useMemomemo,别提前优化。
  • 让 Claude Code 帮忙时,先别说「帮我做」,而是把边界、状态、数据形状、验证方式这四点交给它,输出会稳得多。
  • 库(Zustand、TanStack Query、React Hook Form)要先用原生 React 把意图表达清楚,再去选,这样不容易翻车。

先用一句话说清 React 的思路

React 就是一套「状态(state)一变,就把画面重新画成跟那个状态匹配的样子」的机制。

这正是它和过去做法最不一样的地方。从前用 jQuery,你得手动下命令:「按钮被点了,就把这个元素的文字改掉」。React 反过来,你写的是「现在这个状态下,画面应该长这样」的结果。状态一变,React 自己算出差异,只把需要变的地方重画。

所以设计的主角不是外观(HTML),而是状态。先想「这个画面需要记住哪些值」「这些值放在哪里」。这是 React 开发的起点,这一步错位了,后面会全程难受。

设计的九成:state 放在哪里

state 的落点大致有四个选项。从上往下逐个考虑,上面不够用再往下挪,这是诀窍。

落点适合的值具体例子
不放(用计算得出)能从 props 或 state 推导的值过滤后的数组、合计金额、显示用标签
单个组件内部(局部)只有这个组件用的值展开/收起标志、输入框里正在敲的文字
提到父组件(状态提升)兄弟组件要共享的值当前选中的标签页、筛选条件
URL / 服务器端想共享、想能复现的状态搜索关键词、页码、API 返回的数据

最常犯的错,是直接跳过第一行。把「过滤后的用户列表」特意塞进 useState,再用 useEffect 重新算一遍——这完全不需要。只要有原始数组和筛选条件,每次渲染时算一下就行了。state 越多,需要操心「同不同步」的地方就越多,bug 也就越多。

反过来,要是「兄弟组件之间想看同一个值」,那就毫不犹豫地提到共同的父组件上。这跟 React 官方在 Thinking in React 里讲的思路一模一样:先找出状态的最小单位,再把它放在所有需要它的组件能共享的、最低的那个位置。就这么简单。

等状态跨页面变复杂了,才轮到库登场。不过像「先上 Redux 再说」这样扑上去,只会越用越重。按规模和种类来选的判断轴,我拆到状态管理库怎么选单独写了,等你觉得 useState 撑不住了再去读。

组件在哪里切一刀

另一个轴是边界。判断标准很简单:「能不能一句话说清这个组件的 props」。

比如 UserStatusBadge,你能说清它「收一个 status,输出一个带颜色的标签」。这是个好组件。可要是 UserTable 里塞进了搜索逻辑、API 调用、编辑弹窗的开关、保存处理,那就一句话说不完了。这就是该切的信号。

切的时候,把职责分成三类,思路会清晰很多。

  • 只管外观的组件(UI):收 props 然后展示,不持有状态。
  • 持有状态和逻辑的组件(容器):取数据,再分发给子组件。
  • 可复用的逻辑(自定义 Hook):把状态和副作用挪进函数里。

要当心的是,切得太碎同样是失败。把 UserNameTextUserEmailTextUserRoleText 这样一行一个地拆成组件,只是文件数变多,职责一点没理清。拆分只在「要复用」「想分开状态」「想更好测」这三件事上有效时才值得。把「用到第二处之前不做通用化」当口头禅,能挡掉很多过度设计。

useEffect 的正确位置

这里只在地图的范围内,把 Hooks 的事情理一下。

useState 是「画面要记住的值」;useEffect 是用来「和 React 外部的世界同步」的。分清这个区别,Hooks 九成的纠结都没了。

所谓「外部世界」,指的是 API 通信、localStorage、定时器、订阅浏览器事件这类——除了画画面以外、要碰外部的处理。换句话说,能从 props 和 state 算出来的值,不需要 useEffect。这是 React 里最常见的错,官方甚至专门做了一页 You Might Not Need an Effect

拿不准的时候,按这个顺序想一遍:

  1. 能从 props 或 state 算出来吗? → 渲染过程中直接算(不用 Effect)。
  2. 是用户操作触发的吗? → 写进事件处理函数(点击时 fetch 之类)。
  3. 是画面的显示本身需要跟外部同步吗? → 这时才用 useEffect

落到第三条的,比如「只在打开这个画面期间,去服务器取数据并显示」。下面这个自定义 Hook 就是这种情况的实例。

可直接复制运行:把取数逻辑挪进自定义 Hook

光看地图太枯燥,这里放一段能动手的。Vite + React + TypeScript 里直接能跑的取数自定义 Hook。useEffect 的「正确用法」和善后(清理)的写法,一次就看明白。

要点有两个。AbortController 中止旧请求;以及卸载之后别再碰 state。这两点一忘,在快速切换画面时就会冒出「旧结果覆盖新画面」这种不起眼的 bug。

import { useEffect, useState } from "react";

// 用一个值表示取数状态(避免把 loading / success / error 搞混)
type FetchState<T> =
  | { status: "loading"; data: null; error: null }
  | { status: "success"; data: T; error: null }
  | { status: "error"; data: null; error: string };

export function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    status: "loading",
    data: null,
    error: null,
  });

  useEffect(() => {
    // 用来中途打断旧请求的「中止按钮」
    const controller = new AbortController();
    setState({ status: "loading", data: null, error: null });

    async function run() {
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = (await res.json()) as T;
        setState({ status: "success", data: json, error: null });
      } catch (err) {
        // 中止不算「失败」,所以什么都不做,直接退出
        if (err instanceof DOMException && err.name === "AbortError") return;
        setState({
          status: "error",
          data: null,
          error: err instanceof Error ? err.message : "未知错误",
        });
      }
    }

    void run();

    // 在 url 改变前、画面消失前,把正在跑的通信中止掉
    return () => controller.abort();
  }, [url]);

  return state;
}

用的一方是这样。靠 status 分支,所以不会做出「明明有数据却还在 loading」这种自相矛盾的状态。

type User = { id: string; name: string };

export function UserList() {
  const result = useFetch<User[]>("/api/users");

  if (result.status === "loading") return <p>加载中…</p>;
  if (result.status === "error") return <p role="alert">失败了:{result.error}</p>;

  return (
    <ul>
      {result.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

这就是「Effect 正确用法」的范式。实战里你会想要缓存和重新取数,到那一步就换到 TanStack Query 的缓存设计。自己手写,只是为了把「把 UI 和通信分开」的感觉刻进身体里。

重渲染并不可怕

经常听到「React 重渲染太多所以慢」,先说在前面,重渲染本身大多数情况下几乎是免费的。React 只把差异反映到 DOM 上,函数被重新执行,并不等于画面会闪。

可要是提前到处贴 useMemoReact.memo,只会让代码更难读,而且通常并不会变快。顺序反了。正确的顺序是这样:

  1. 把 state 放到该放的地方(别把无关的组件也卷进来一起重画)。
  2. 列表的 key稳定的 ID(别拿 index 当 key)。
  3. 沉重的计算、巨大数组的处理,真的感到卡了之后再用 useMemo 包起来。
  4. 输入过程中会跑重处理,就加防抖(debounce,把高频调用稀释掉)。

连「为什么会发生重渲染」都说不清就加 memo,就像往不疼的地方贴膏药。先测量、定位瓶颈,再去修。这套思路我整理在了Web 性能优化的优先级里。

用 Claude Code 高效地做

把到这里的判断轴,交给 Claude Code,而不是甩给它,这是诀窍。只说「做个像样的后台页面」,出来的东西外观是齐整,但状态散得到处都是。我自己就栽过,出来的巨型组件最后几乎重写了一遍。

现在动手之前,我一定会用文字把下面这四点交出去。

要交的信息写法示例
组件的职责「把列表、搜索、编辑对话框拆成不同组件」
state 的落点「筛选条件放父组件,只有行的展开/收起放局部」
数据的形状User 类型是这个。API 响应的形状别改」
验证方式「用 Testing Library 写按 role 和 label 取元素的测试」

而个人觉得最管用的一招,是把**「新建」换成「让它审查」**。让它读现有的 diff,挑出「组件是不是太大」「有没有多余的 useEffect」「state 和服务器数据是不是混在了一起」。这样它不太会擅自新增文件,改动范围也能压得很小。

团队里做的话,把这四点连同禁止事项(过度通用化、只有 placeholder 的表单、拿 index 当 key 等)写进 CLAUDE.md,就不用每次重复同样的说明了。类型相关的地基看TypeScript 开发实战技巧,测试怎么铺开看测试策略的优先级,都是一脉相承的。

常见问题

Q. useState 和 useRef 怎么区分着用? A. 想反映到画面上的值用 useState,不用反映的值用 useRef。比如「输入框的 DOM 引用」「保存上一次的值」不需要重画,就用 useRef;值一改就希望画面跟着变,就用 useState。改 useRef 的值不会触发重渲染。

Q. props 超过几个就该拆组件? A. 看的不是数量,而是「说不说得清」。props 只有 3 个但职责各不相干,就太大了;有 6 个但全为同一个目的,就没问题。props 变多了,先怀疑「能不能合成一个对象」「是不是其实混进了两个组件」。

Q. useEffect 里取的数据,切换画面时会瞬间显示成旧的。 A. 是清理(善后)漏了。像上面的代码那样用 AbortController 中止上一次通信,务必写上 return () => controller.abort()。这样「旧结果覆盖新画面」的竞态就消失了。

Q. 状态管理库要不要一开始就上? A. 不用。先用 useState 加「提到父组件」推进,等状态跨页面缠成一团了再考虑也来得及。服务器数据本来就和 UI 状态是两回事,先到状态管理怎么选里把种类分开,再去选。

Q. Claude Code 老是做出过于「高大上」的通用组件。 A. 一开始就告诉它「只做当下需要的抽象」「用到第二处之前不通用化」。为了一个画面就被造出通用 Table、通用 Modal、通用 FilterEngine,将来一改就会一下子变得很重。

总结

React 的设计,归根结底就是回答两个问题:「这个值放在哪里?」和「这个组件的职责能不能一句话说清?」。state 的落点从上往下考虑,能用计算得出的值不碰 Effect,重渲染等到感觉卡了再动手。光这些,就能写出读得懂的代码。

而 Claude Code,你把这套判断轴交给它,它一下就成了靠得住的搭档;反过来,把判断轴也一起甩出去,回来的就是看着体面、运维起来脆弱的代码。先从一个小画面起步,把边界、状态、数据形状、验证方式这四点交出去,试试看。

想再往细处深挖的话:状态管理看状态管理怎么选,数据获取看 TanStack Query 的缓存设计,表单看 react-hook-form × zod 的校验,无障碍看 a11y 实现的优先级。想要动手的教材或可以请教的地方,也欢迎看看教材一览ClaudeCodeLab 培训与咨询

#React #组件设计 #Hooks #useEffect #Claude Code
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。