本章定位上一章我们已经正式进入了 React 的交互开发阶段。你已经理解了这些非常关键的内容什么是state。事件如何触发状态变化。受控组件为什么能把表单和状态连接起来。也就是说到现在为止你已经不只是会写一个静态组件而是已经能做出点击后数量变化的计数器输入后实时预览内容的输入框可以自己管理表单值的小组件这一步非常重要。但如果你继续往下写很快会遇到三个新问题如果我有一组数据怎么把它们渲染成一组界面如果某块内容有时候要显示、有时候不要显示怎么处理如果页面拆成多个组件它们之间怎么配合这三个问题几乎会出现在所有稍微真实一点的 React 页面里。例如一个商品页面通常会有商品列表分类筛选空状态提示加载中提示弹窗显示与隐藏父组件和子组件之间的数据配合这些场景背后最常用到的就是列表渲染条件渲染组件通信所以你可以把这一章理解成从“单个组件会交互”走到“页面结构开始真正变复杂”。本章学习目标学完这一章后你应该能做到理解什么是列表渲染。学会用map把一组数据渲染成一组界面元素。理解key的作用以及什么样的key更合适。理解什么是条件渲染。掌握if、三元表达式、三种常见条件渲染方式。知道列表渲染和条件渲染为什么经常一起出现。理解什么是组件通信。复习父组件向子组件传值的基本方式。学会通过回调函数让子组件影响父组件状态。理解兄弟组件为什么常常通过共同父组件协作。建立“组件通信本质上是在协调数据流向”的意识。避开这一阶段最常见的几个坑。为下一章学习useEffect与数据请求做好准备。一、为什么学到这里必须处理“多条数据、多种界面、多组件配合”前面几章我们更多是在解决单个组件内部的问题。例如一个按钮点了以后会怎么样一个输入框的内容怎么同步到页面一个组件怎么接收props这些当然都很重要。但真实页面往往不会只停留在这种“单点交互”。更常见的情况是一组数据要渲染成一组界面不同状态要显示不同内容多个组件还要彼此协作。比如一个课程列表页面可能同时会遇到这些需求页面里有很多课程卡片。如果课程为空要显示“暂无课程”。点击分类按钮后要切换显示不同课程。分类按钮组件和课程列表组件需要一起工作。你会发现“很多卡片”对应的是列表渲染“暂无课程”对应的是条件渲染“按钮组件和列表组件一起工作”对应的是组件通信所以这一章并不是在学三个彼此割裂的小知识点而是在学React 页面如何真正组织动态界面。二、什么是列表渲染先给一个当前阶段最够用的理解列表渲染就是把一组数据转换成一组界面元素。这句话非常重要。因为 React 页面里大量内容本来就是“成组出现”的。例如商品列表文章列表评论列表待办事项列表菜单项列表学生信息列表这些内容的共同点是数据通常是一组界面也通常是一组。所以列表渲染的本质不是“写很多个li”而是根据一组数据生成一组对应的 JSX。三、为什么 React 里列表渲染经常配合map如果你回想 JavaScript 基础就会记得map很适合把一组数据转换成另一组结果。而 React 列表渲染做的事情本质上也很像把一组数据转换成一组界面结构。1. 先看一个最简单的思路假设我们有一组课程数据const lessonList [React, TypeScript, Next.js];如果你想把它们显示成列表项最自然的思路就是把数组里的每一项都变成一个li。2.map为什么特别适合这个场景因为它本来就在做这样的事遍历数组里的每一项针对每一项返回一个新结果在 React 里这个“新结果”经常就是一段 JSX所以map和列表渲染会经常一起出现。四、第一个列表渲染例子先看一个最基础的例子function LessonList() { const lessonList [React, TypeScript, Next.js]; return ( ul {lessonList.map(function (lessonName) { return li key{lessonName}{lessonName}/li; })} /ul ); }1. 这段代码在做什么它的逻辑其实很直白lessonList里有三条数据。map会一条条取出这些数据。每一条数据都返回一个li。最后这些li会一起出现在ul里。2. 当前阶段最值得你看懂什么你先把这条主线看清楚就够了数据不是先手写成很多个标签而是通过map渲染出来的。这就是 React 里列表渲染的基本感觉。五、如果列表项不只是一个名字怎么办真实项目里列表项通常不会只有一个字符串。更常见的是对象数组例如function LessonList() { const lessonList [ { id: 1, title: React 入门, level: 初级 }, { id: 2, title: TypeScript 基础, level: 初级 }, { id: 3, title: Next.js 上手, level: 中级 } ]; return ( ul {lessonList.map(function (lesson) { return ( li key{lesson.id} {lesson.title} - {lesson.level} /li ); })} /ul ); }1. 这个例子在说明什么它在说明列表渲染不是只能渲染“一个名字”而是可以把对象里的多个字段一起渲染到界面上。2. 为什么对象数组这么常见因为很多真实数据本来就是对象结构。例如商品标题、价格、库存文章标题、作者、发布日期学生姓名、班级、分数所以你要慢慢习惯React 列表渲染最常处理的往往是对象数组。六、为什么列表渲染要写key这是 React 列表渲染里非常重要的一点。也是很多初学者最容易“先照着写但没真正理解”的地方。你现在先记住一句非常关键的话key的作用不是为了让代码看起来完整而是为了帮助 React 识别列表项的变化。1. 什么叫“识别列表项的变化”假设页面上有三条待办事项学 React练 TypeScript写项目练习如果你删除了第二条或者把顺序调了一下React 需要知道哪一项还是原来的哪一项变了哪一项被删了。这时候key就像每一项的“稳定身份标识”。2.key不是给你看的而是给 React 识别用的这一点特别重要。你可以把它理解成React 渲染列表时需要靠key来更准确地追踪每一项。所以学习key的重点不是死记“列表必须写”而是理解它在帮助 React 识别元素变化。3. 当前阶段你最该抓住的结论当你在 JSX 里用map渲染一组元素时通常都要考虑我有没有给每一项提供一个稳定的key七、什么样的key更合适这一节很实用。因为很多人知道“要写key”但不知道“写什么更合适”。1. 最推荐的情况使用稳定的唯一标识例如key{lesson.id}这是非常常见、也通常最稳的写法。因为id往往具备两个特点唯一稳定这正是 React 最需要的。2. 为什么“稳定”很关键如果某个列表项今天是这个key明天又换成另一个React 就不容易准确判断这还是不是原来那一项。所以关键不只是“唯一”还要尽量“稳定”。3. 为什么不建议每次都临时生成随机值例如key{Math.random()}这通常是不推荐的。因为每次渲染时随机值都可能变。这样 React 看到的就像所有列表项都变成了全新的。这会让key失去它原本“帮助识别变化”的意义。八、数组下标能不能当key这是一个经常会被问到的问题。答案不是简单的“绝对不行”但当前阶段你可以先建立一个更稳的习惯只要列表可能新增、删除、排序变化就优先使用稳定的业务标识而不是数组下标。1. 为什么数组下标有风险假设你有三项数据下标 0下标 1下标 2如果你删除了中间那一项后面的下标就会变化。这时候 React 看到的标识关系就容易变得不够稳定。2. 当前阶段怎么记最实用你可以先记这个版本有稳定id优先用id列表会动态变化时不优先用下标真正静态、不会改顺序的小演示才可能临时用下标这样理解已经很够用了。九、列表渲染和状态为什么经常一起出现因为很多列表并不是固定不变的。它们常常会受到状态影响。例如当前选择了哪个分类当前搜索关键词是什么当前是否只显示未完成项目这就意味着列表渲染的结果往往不是写死的而是由当前状态决定的。先看一个很简单的筛选例子function CoursePanel() { const [currentLevel, setCurrentLevel] useState(all); const lessonList [ { id: 1, title: React 入门, level: 初级 }, { id: 2, title: TypeScript 基础, level: 初级 }, { id: 3, title: Next.js 上手, level: 中级 } ]; const filteredList currentLevel all ? lessonList : lessonList.filter(function (lesson) { return lesson.level currentLevel; }); return ( ul {filteredList.map(function (lesson) { return li key{lesson.id}{lesson.title}/li; })} /ul ); }1. 这个例子最值得你观察什么它说明了一件非常关键的事先有状态再根据状态决定当前要渲染哪一组数据。2. 为什么这很重要因为从这里开始列表渲染就不再只是“把数组打印出来”而是在开始进入根据交互结果渲染不同列表。这正是很多真实页面的基本工作方式。十、什么是条件渲染现在我们进入第二条主线条件渲染。先给一个当前阶段最够用的理解条件渲染就是根据不同条件决定界面应该显示什么。这句话一定要慢慢体会。因为你前面学原生 DOM 时很多显示隐藏逻辑可能更像是手动改类名手动改style.display手动删除或添加节点而在 React 里更常见的思路是根据当前条件直接决定返回哪一段 JSX。1. 条件渲染最常见会出现在什么地方例如正在加载时显示“加载中”没有数据时显示空状态登录后显示用户信息否则显示登录按钮弹窗打开时显示弹窗否则不显示2. 当前阶段最值得记住的理解React 的条件渲染本质上不是“我去手动隐藏 DOM”而是当前条件下这块界面该不该出现该出现什么样子。十一、第一种常见写法if与提前返回这是最直观、也最容易读的一种方式。尤其当你想表达某个条件成立时整个组件都应该显示另一套内容。先看一个例子function CourseList({ lessonList }) { if (lessonList.length 0) { return p当前还没有课程数据。/p; } return ( ul {lessonList.map(function (lesson) { return li key{lesson.id}{lesson.title}/li; })} /ul ); }1. 这种写法适合什么场景特别适合空状态加载中状态权限不满足时的替代内容整个组件分支差异很大时2. 为什么它很适合初学者因为它很直白。你看到代码时很容易直接读成如果没有数据就返回这段提示否则返回列表。这种可读性非常好。十二、第二种常见写法三元表达式当你有两个明确分支时三元表达式会很常见。例如function LoginStatus({ isLogin }) { return p{isLogin ? 欢迎回来 : 请先登录}/p; }1. 这类写法适合什么场景适合两种结果都很明确分支内容相对不长你希望直接在 JSX 里表达“二选一”2. 当前阶段要注意什么如果分支内容越来越长、越来越复杂三元表达式就会越来越难读。这时候你要敢于回到if提前返回或者把部分结构拆成更清楚的组件。十三、第三种常见写法这种写法很适合表达条件成立时显示条件不成立时什么都不显示。例如function CartSummary({ totalCount }) { return ( section h2购物车/h2 {totalCount 0 p你当前选中了 {totalCount} 件商品。/p} /section ); }1. 它和三元表达式有什么区别三元表达式更像有 A 就显示 A否则显示 B而更像只有满足条件时才显示其他时候就不显示这一块2. 当前阶段什么时候优先用它当你只是想补充一小块可选界面时往往很方便。例如角标提示错误提示成功提示统计信息十四、列表渲染和条件渲染为什么经常一起出现这一点非常重要。因为真实项目里列表几乎不会永远稳定存在。更常见的是有数据时显示列表没数据时显示空状态加载中时显示加载提示这就意味着列表渲染和条件渲染经常是配套使用的。先看一个简单例子function LessonList({ lessonList, isLoading }) { if (isLoading) { return p课程加载中.../p; } if (lessonList.length 0) { return p当前没有符合条件的课程。/p; } return ( ul {lessonList.map(function (lesson) { return li key{lesson.id}{lesson.title}/li; })} /ul ); }1. 这个例子在说明什么它说明 React 里一个常见的界面判断流程是先看是不是加载中再看是不是空数据最后才渲染正常列表2. 为什么这种结构很重要因为很多页面真正难的地方不是“正常状态怎么显示”而是不同状态下界面该怎么切换。而这正是条件渲染最核心的价值之一。十五、什么是组件通信现在进入第三条主线组件通信。你现在可以先把它理解成组件之间为了完成同一个页面功能彼此传递数据或协调行为。这个定义你一定要先记住。因为很多人学组件通信时容易只盯着“语法怎么写”忽略它真正要解决的问题。组件通信真正想解决的是页面拆开以后不同组件怎么继续合作。例如页头组件要接收用户名筛选按钮组件要告诉父组件“我被点了”列表组件要根据当前分类显示不同结果这本质上都和通信有关。十六、最基础的通信父组件向子组件传值这一层你其实已经学过了。它就是父组件通过props给子组件传数据。例如function UserCard({ userName }) { return h3{userName}/h3; } function App() { return UserCard userName小白 /; }1. 这里谁在通信App把数据给了UserCardUserCard收到后负责显示2. 为什么这属于组件通信因为它已经不是单个组件自己闷头工作了而是一个组件提供数据另一个组件消费数据。这就是最基础的一种组件通信。十七、更关键的一步子组件怎么影响父组件这一步非常关键。因为如果你只会“父传子”页面通常还不够灵活。真实项目里很常见的一种场景是子组件里有按钮或输入操作但真正要改的数据在父组件里。例如分类按钮在子组件里当前分类状态在父组件里子组件点击按钮后要通知父组件更新状态这时候最常见的做法是父组件把一个回调函数传给子组件子组件在合适时机调用它。十八、什么叫“回调传递”你现在可以先把回调传递理解成父组件把“以后你需要做什么”这个函数先传给子组件。然后子组件在按钮点击、输入变化等时机把这个函数调用起来。1. 先看一个简单例子function CategoryButtons({ onChangeCategory }) { return ( section button onClick{function () { onChangeCategory(react); }} React /button button onClick{function () { onChangeCategory(typescript); }} TypeScript /button /section ); }2. 这里的onChangeCategory是什么它不是子组件自己定义的数据结果而是父组件传进来的函数。子组件做的事情只是在点击时调用它并把需要的参数带过去。3. 为什么这种模式这么常见因为很多页面都需要这种配合子组件负责触发动作父组件负责管理真正的状态这就是 React 组件通信里非常核心的一种配合方式。十九、一个父子组件通信的完整小例子现在我们把父组件补出来import { useState } from react; function CategoryButtons({ onChangeCategory }) { return ( section button onClick{function () { onChangeCategory(all); }} 全部 /button button onClick{function () { onChangeCategory(react); }} React /button button onClick{function () { onChangeCategory(typescript); }} TypeScript /button /section ); } function App() { const [currentCategory, setCurrentCategory] useState(all); function handleChangeCategory(nextCategory) { // 子组件点击按钮后真正更新状态的还是父组件 setCurrentCategory(nextCategory); } return ( main p当前分类{currentCategory}/p CategoryButtons onChangeCategory{handleChangeCategory} / /main ); }1. 这个例子最值得你看懂什么请重点看这条数据流currentCategory状态在父组件App里父组件把handleChangeCategory传给子组件子组件点击按钮时调用这个函数父组件状态更新页面显示最新分类结果2. 这说明了什么说明所谓“子组件影响父组件”本质上并不是子组件直接去改父组件数据而是子组件通过父组件传来的回调去触发父组件自己的更新逻辑。这句话非常重要。二十、为什么说“子传父”本质上还是父在控制数据很多初学者第一次接触组件通信时容易听到一句简化说法子组件给父组件传值。这句话能帮助你快速入门但如果理解得太字面会有点偏。更准确一点的理解应该是子组件通过调用父组件传下来的函数通知父组件去更新自己的状态。也就是说真正的数据和状态通常还是由父组件掌握子组件更像是在发出“我这边发生了一个动作”的信号这一点越早理解越好。因为它会帮助你建立更清楚的数据流意识。二十一、兄弟组件怎么通信这一节也非常重要。因为真实页面里很常见的并不是只有父和子。很多时候你会遇到这种情况一个筛选组件一个列表组件它们是并列关系也就是兄弟组件。那它们怎么配合当前阶段最稳的理解是兄弟组件通常不直接通信而是通过共同父组件中转。1. 先把这件事说直白一点假设页面结构是这样App ├── CategoryButtons └── ProductList这里CategoryButtons负责切换分类ProductList负责显示商品列表它们彼此不是父子而是兄弟。2. 更常见的做法是什么由App来负责保存当前分类状态根据当前分类筛选数据把分类状态或回调传给按钮组件把筛选后的列表传给列表组件这就是非常典型的共同父组件协调兄弟组件。二十二、一个把列表渲染、条件渲染和组件通信串起来的小例子下面这个例子很重要。它会把这一章三条主线真正串到一起。import { useState } from react; function CategoryButtons({ currentCategory, onChangeCategory }) { return ( section button onClick{function () { onChangeCategory(all); }} 全部 /button button onClick{function () { onChangeCategory(react); }} React /button button onClick{function () { onChangeCategory(typescript); }} TypeScript /button p当前筛选{currentCategory}/p /section ); } function ProductList({ productList }) { if (productList.length 0) { return p当前分类下没有课程。/p; } return ( ul {productList.map(function (product) { return li key{product.id}{product.title}/li; })} /ul ); } function App() { const [currentCategory, setCurrentCategory] useState(all); const productList [ { id: 1, title: React 入门, category: react }, { id: 2, title: TypeScript 基础, category: typescript }, { id: 3, title: React 状态管理, category: react } ]; const filteredProductList currentCategory all ? productList : productList.filter(function (product) { return product.category currentCategory; }); return ( main CategoryButtons currentCategory{currentCategory} onChangeCategory{setCurrentCategory} / ProductList productList{filteredProductList} / /main ); }1. 这个例子里列表渲染体现在哪里在ProductList组件里productList.map(...)它把一组课程数据渲染成了一组li。2. 条件渲染体现在哪里还是在ProductList组件里if (productList.length 0) { return p当前分类下没有课程。/p; }这表示如果筛选后没有数据就不显示列表而显示空状态提示。3. 组件通信体现在哪里主要体现在两条线上父组件把currentCategory和onChangeCategory传给CategoryButtons父组件把filteredProductList传给ProductList这就说明父组件在协调数据流兄弟组件在各自完成自己的职责。二十三、把这个例子的“数据流”翻译成人话很多初学者看代码能勉强看懂但脑子里没有真正形成数据流。所以这一节我们把刚才那个例子翻译成更直白的话。1. 第一步父组件先保存当前分类状态也就是const [currentCategory, setCurrentCategory] useState(all);这表示当前页面记住“现在正在看哪个分类”。2. 第二步父组件根据状态算出当前该显示哪些数据也就是const filteredProductList ...这表示根据当前分类决定列表到底显示哪些课程。3. 第三步父组件把“当前分类”和“切换分类的方法”交给按钮组件按钮组件并不需要掌握整个页面逻辑它只需要知道当前是什么分类如果用户点了按钮我该调用哪个函数4. 第四步父组件把“筛选好的数据”交给列表组件列表组件不需要关心分类状态从哪来数据为什么会变成这样它只要专注一件事把拿到的数据渲染出来。这就是组件职责分工越来越清楚的开始。二十四、在 TypeScript 项目里这些模式怎么补简单类型你前面已经学过 TypeScript所以这里可以顺手做一点预习。例如刚才的列表项数据就很适合写类型interface ProductItem { id: number; title: string; category: string; } interface ProductListProps { productList: ProductItem[]; } function ProductList({ productList }: ProductListProps) { if (productList.length 0) { return p当前分类下没有课程。/p; } return ( ul {productList.map(function (product) { return li key{product.id}{product.title}/li; })} /ul ); }1. 当前阶段最值得先看什么先看两点列表项是什么结构组件接收的props是什么结构2. 为什么这一步有价值因为列表渲染和组件通信一多数据结构就更容易复杂起来。这时候类型会明显帮你把边界看清楚。二十五、初学者最容易踩的几个坑这一节建议你认真看。因为这一章的内容一旦混起来初学者很容易一团乱。1. 坑一列表渲染时忘记key这是最常见的问题之一。你也许代码能跑但 React 会提醒你。更重要的是如果你一直把key当成“可有可无”就很难真正理解列表项需要稳定标识React 才能更好识别变化。2. 坑二为了省事随手用随机值当key例如key{Math.random()}这通常不是个好习惯。因为它不稳定。3. 坑三条件渲染里套很多层三元表达式三元表达式不是不能用但如果你写成这样条件 A ? 结果 1 : 条件 B ? 结果 2 : 条件 C ? 结果 3 : 结果 4那代码很快就会变得非常难读。当前阶段更稳的做法是简单分支用三元表达式整块分支差异大时用if只想显示一小块时用4. 坑四子组件试图直接“管理父组件的数据”更清楚的方式应该是父组件管理状态子组件通过回调通知父组件发生了什么。这会让数据流更清楚。5. 坑五兄弟组件通信时不知道谁该负责如果你发现两个兄弟组件都在尝试彼此“够着改对方”通常说明该把协调逻辑放回共同父组件了。二十六、本章实践练习这一章的练习重点是把“成组数据、分支界面、组件协作”三件事真正练熟。1. 练习 1渲染一个课程列表请你准备一组课程数组数据至少包含idtitlelevel然后完成这些事情用map把它渲染成一组列表项为每一项补上合适的key这个练习的重点是把“数组数据 - JSX 列表”这条路径走顺。2. 练习 2给列表补一个空状态请在上面的列表组件基础上增加一个条件如果数组为空显示“当前没有课程”如果数组不为空正常渲染列表这个练习在训练你列表渲染和条件渲染的配合。3. 练习 3实现分类切换请你准备两到三个分类例如allreacttypescript然后完成这些事情用状态记录当前分类点击不同按钮切换分类根据当前分类筛选并渲染列表这个练习会帮你真正把状态列表渲染条件渲染串起来。4. 练习 4让子组件触发父组件列表更新请你把分类按钮单独拆成一个子组件。要求父组件保存当前分类状态父组件把回调传给子组件子组件点击按钮后通知父组件更新分类这个练习的重点是理解组件通信不是“谁改谁”而是“如何让数据流保持清楚”。二十七、学习重点提示这一章请你重点记住下面这些话列表渲染的本质是把一组数据转换成一组界面元素。React 里列表渲染常常配合map使用。key的核心作用是帮助 React 识别列表项的变化。合适的key应该尽量唯一且稳定。条件渲染的本质是根据当前条件决定界面应该显示什么。if、三元表达式、都是常见的条件渲染方式选择要看场景。列表渲染和条件渲染经常一起出现例如加载中、空状态、筛选结果等。组件通信的本质是在协调数据流向和组件职责分工。父组件给子组件传值通常通过props子组件影响父组件通常通过回调函数。兄弟组件更常通过共同父组件协作而不是彼此直接管理对方。如果你只记一句话请记住React 页面常见的组织方式是父组件掌握状态和数据流子组件各自负责触发动作或渲染结果。二十八、本章小结这一章我们正式把 React 从“单个组件内部的状态与交互”推进到了“页面结构之间的组织与协作”。你已经理解了什么是列表渲染为什么 React 里经常用map渲染列表key的作用和更稳的使用方式什么是条件渲染if、三元表达式、的常见场景为什么列表渲染和条件渲染经常一起出现什么是组件通信父子组件如何通过props和回调函数协作兄弟组件为什么常通过共同父组件中转更重要的是你开始建立一种更接近真实页面开发的 React 思路页面不是一个组件自己完成所有事情而是由多个组件围绕数据流共同协作。这一步非常关键。因为从这里开始你已经不只是在学“某个语法怎么写”而是在开始理解React 页面是怎样被组织起来的。二十九、课后思考题请你认真思考下面这些问题为什么说列表渲染的本质不是“手写很多个标签”而是“根据数据生成界面”key的核心作用到底是什么为什么说重点不是“为了消除警告”为什么数组下标通常不是动态列表里最稳的keyif、三元表达式、三种条件渲染方式各适合什么场景为什么说组件通信本质上是在协调数据流向为什么兄弟组件通常通过共同父组件协作而不是直接彼此管理在“分类按钮 列表展示”这个场景里为什么分类状态更适合放在父组件里建议你把这些问题用自己的话写下来。只要你能把这些问题讲清楚说明你已经真正开始进入 React 页面组织的主线了。三十、下一篇预告下一章我们会进入从零开始学前端 | 第三十一章useEffect 与数据请求到那时你会继续解决几个非常关键的问题组件加载后要做某些事情应该放在哪里请求接口数据后怎么把结果显示到页面上定时器、请求、副作用和清理逻辑该怎么组织也就是说下一章开始我们会从“页面内部的数据流和组件协作”继续走到React 中副作用与异步数据处理的基础。