React 20 useState管理组件状态 | 解构 | 将事件处理函数作为 props 传递 | 状态提升

在组件间共享状态 -- React 中文文档

1 useState****管理组件状态

javascript 复制代码
import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          显示
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>哈萨克斯坦,阿拉木图</h2>
      <Panel title="关于">
        阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
      </Panel>
      <Panel title="词源">
        这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
      </Panel>
    </>
  );
}

一、代码整体功能总结

这段代码定义了两个 React 函数组件:

  1. Panel 子组件:一个可切换显示 / 隐藏内容的面板,初始状态下只显示标题和 "显示" 按钮,点击按钮后会隐藏按钮并展示面板的内容。
  2. Accordion 父组件 :作为入口组件,渲染了标题和两个Panel面板(分别展示 "关于" 和 "词源" 的内容)。
  3. 整体效果是:页面上有两个面板,每个面板初始只显示标题和按钮,点击对应按钮就能展开看到面板里的详细内容。

二、代码逐部分详细解释

1. 导入核心 Hook

javascript 复制代码
import { useState } from 'react';

这行代码从 React 中导入了 useState Hook,useState是 React 函数组件中用于管理响应式状态的核心工具,能让组件拥有自己的状态并实现视图更新。

2. Panel 子组件定义
javascript 复制代码
function Panel({ title, children }) {
  // 1. 定义状态:isActive表示面板是否激活(展开),初始值为false(未展开)
  // setIsActive是修改isActive的方法,调用它会更新状态并重新渲染组件
  const [isActive, setIsActive] = useState(false);

  return (
    <section className="panel">
      {/* 2. 渲染面板标题:接收父组件传递的title属性 */}
      <h3>{title}</h3>
      {/* 3. 条件渲染:根据isActive的值决定显示内容还是按钮 */}
      {isActive ? (
        // 当isActive为true时,显示面板的子内容(children)
        <p>{children}</p>
      ) : (
        // 当isActive为false时,显示"显示"按钮,点击时调用setIsActive将状态改为true
        <button onClick={() => setIsActive(true)}>
          显示
        </button>
      )}
    </section>
  );
}

这里的关键知识点:

  • 组件传参Panel组件通过props 接收两个参数:title(面板标题)和children(面板的子内容,React 中组件标签内的内容会被自动传入children属性)。
  • 条件渲染 :使用三元运算符? :根据isActive的布尔值,切换渲染 "内容" 或 "按钮",这是 React 中实现条件渲染的常用方式。
  • 事件处理按钮的onClick事件绑定了一个箭头函数,点击时调用setIsActive(true)修改状态,触发组件重新渲染。
3. Accordion 父组件定义与导出
javascript 复制代码
export default function Accordion() {
  return (
    <>
      {/* 页面主标题 */}
      <h2>哈萨克斯坦,阿拉木图</h2>
      {/* 第一个Panel组件:传递title属性,子内容是"关于"的文本 */}
      <Panel title="关于">
        阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
      </Panel>
      {/* 第二个Panel组件:传递title属性,子内容包含特殊标签和文本 */}
      <Panel title="词源">
        这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
      </Panel>
    </>
  );
}

这里的关键知识点:

  • 默认导出export default表示将Accordion组件作为默认导出,其他文件导入时可以直接命名使用。
  • Fragment(片段)<>是 React 的空片段(等价于<React.Fragment>),用于包裹多个子元素,避免渲染多余的父 DOM 节点(比如不需要额外的<div>包裹)。
  • 组件复用 :两次使用Panel组件,分别传递不同的titlechildren,实现了组件的复用,这是 React 组件化的核心优势之一。

补充(使用子组件的方式):

<Panel title="关于">

阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。

</Panel>

这个是怎么用的,react文档里面有对应的用法吗,我不知道这个子组件可以这样调用。

你想知道<Panel title="关于">...</Panel>这种使用子组件的方式背后的原理,以及 React 文档中对应的知识点,其实这是 React 中组件的 props 传递children 属性的核心用法,是 React 的基础特性之一。

底层原理

你看到的这段代码:

javascript 复制代码
<Panel title="关于">
  阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
</Panel>

本质上是 React 的JSX 语法糖会被 Babel 编译成普通的 JavaScript 函数调用,具体拆解成两部分:

1. 标签上的属性(如title="关于")→ 传递给组件的 props 属性

title="关于"会被作为 props 的一个键值对传递给Panel组件,对应Panel组件定义中的{ title }解构。

2. 组件标签内部的内容 → 传递给组件的children props

<Panel>标签中间的文本内容,会被 React 自动封装成props.children属性 ,传递给Panel组件。

JSX 编译后的等价代码(帮你理解本质)

上面的 JSX 代码,会被编译成如下的 JavaScript 代码(简化版):

javascript 复制代码
React.createElement(
  Panel, // 要渲染的组件
  { title: "关于" }, // 传递的props(属性)
  // 组件内部的内容(children)
  "阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。"
);

React.createElement的第三个及以后的参数,都会被合并成props.children

这就是为什么Panel组件中可以通过{ children }拿到标签内部的内容 ------ 因为这些内容本质上就是props的一部分。

React 文档中的对应知识点

React 官方文档中明确讲解了这个特性,主要分布在以下章节:

1. 核心章节:「Props and Children」(属性和子元素)
2. 详细说明:「Children in JSX」(JSX 中的子元素)

在新版 React 文档中,children被归类在传递 Props 的章节里,并且列举了children的多种形式:

  • 文本子元素(比如你的例子中的纯文本)。
  • 元素子元素(比如<Panel><div>内容</div></Panel>)。
  • 多个子元素(比如<Panel><p>1</p><p>2</p></Panel>)。
  • 甚至可以是 JSX 表达式、组件、函数等(比如<Panel>{[1,2,3].map(item => <p key={item}>{item}</p>)}</Panel>)。
children的常见使用场景

除了你的例子中的纯文本,children还有很多实用场景,这也是 React 组件复用的重要方式:

场景 1:包裹任意元素(布局组件)

比如写一个通用的卡片组件:

javascript 复制代码
// 定义卡片组件
function Card({ children }) {
  return (
    <div style={{ border: '1px solid #ccc', padding: 20, borderRadius: 8 }}>
      {children} {/* 渲染外部传入的任意内容 */}
    </div>
  );
}

// 使用组件(可以传入任意内容)
function App() {
  return (
    <Card>
      <h3>卡片标题</h3>
      <p>卡片内容,可以是任意JSX元素</p>
      <button>按钮</button>
    </Card>
  );
}
场景 2:传入多个子元素
javascript 复制代码
<Panel title="关于">
  <p>第一行内容</p>
  <p>第二行内容</p>
  <img src="almaty.jpg" alt="阿拉木图" />
</Panel>

此时props.children是一个数组,包含<p><p><img>三个元素,Panel组件中直接{children}就能全部渲染。

场景 3:条件渲染 / 修改 children(高级用法)

甚至可以在组件内部对children进行处理后再渲染(比如添加样式、过滤内容):

javascript 复制代码
function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section>
      <h3>{title}</h3>
      {isActive ? (
        // 给children包裹一个样式标签
        <div style={{ color: '#333' }}>{children}</div>
      ) : (
        <button onClick={() => setIsActive(true)}>显示</button>
      )}
    </section>
  );
}
总结
  1. <Panel title="关于">内容</Panel>的用法,核心是React 的 props 传递title是 props 的普通属性,标签内的内容是props.children属性。
  2. 这个特性在 React 官方文档的**「传递 Props 到组件」**章节有详细讲解,是 React 的基础核心知识点。
  3. children属性支持文本、元素、组件、表达式等多种形式,是实现组件复用和灵活布局的关键。

简单来说,props.children就像是组件的 "插槽"(类似 Vue 中的 slot),让你可以在使用组件时,灵活地向组件内部插入任意内容。

三、代码的执行流程

  1. 页面加载时,Accordion组件被渲染,它会渲染两个Panel组件。
  2. 每个Panel组件初始化时,isActive的状态为false,因此页面上显示标题和 **"显示" 按钮 **。
  3. 当用户点击某个Panel的 "显示" 按钮时,该PanelsetIsActive(true)被调用,isActive变为true
  4. Panel组件重新渲染,此时会显示children内容,隐藏按钮。

可以优化的点(拓展)

目前这段代码的面板只能 "展开" 不能 "收起",如果需要实现收起功能,只需将按钮的点击逻辑改为切换状态

javascript 复制代码
// 将原来的onClick改为切换isActive的布尔值
<button onClick={() => setIsActive(!isActive)}>
  {/* 按钮文字也可以根据状态切换 */}
  {isActive ? '隐藏' : '显示'}
</button>

总结

  1. 这段代码的核心是 useState****管理组件状态+条件渲染,实现了面板的展开功能。
  2. 利用 React 的propschildren 属性实现了组件的传参和复用,体现了组件化的思想。
  3. 使用空片段<> 避免了多余的 DOM 节点,符合 React 的最佳实践。

2 解构

解构 - JavaScript | MDN

在之前的 React 代码中看到的{ title, children }这种写法,其实解构是 JavaScript(ES6+)里的一个语法糖 ,目的是快速从对象或数组中提取数据,简化代码书写

一、解构的基本定义

解构(Destructuring)就是将一个复杂的结构(对象、数组)拆解开来,把里面的元素 / 属性赋值给单独的变量

就像你有一个礼盒(对象 / 数组),里面装了不同的物品,解构就是直接把礼盒里的物品拿出来,不用每次都打开礼盒才能取东西。

二、两种最常见的解构:对象解构 & 数组解构

1. 对象解构(React 中最常用,比如 props 解构)

这是你在Panel组件中看到的写法,先看没有解构 的写法,再看解构后的写法,对比一下就懂了。

(1)没有解构的写法(繁琐)
javascript 复制代码
// 定义一个对象(比如React组件的props)
const props = {
  title: "关于",
  children: "阿拉木图的介绍内容"
};

// 提取属性到变量(需要逐个赋值)
const title = props.title;
const children = props.children;

console.log(title); // 输出:关于
console.log(children); // 输出:阿拉木图的介绍内容
(2)使用对象解构的写法(简洁)
javascript 复制代码
const props = {
  title: "关于",
  children: "阿拉木图的介绍内容"
};

// 一行代码提取title和children属性,赋值给同名变量
const { title, children } = props;

console.log(title); // 输出:关于
console.log(children); // 输出:阿拉木图的介绍内容
(3)React 组件中的对象解构(你看到的场景)

在组件的参数中直接解构,省去后续的变量赋值步骤:

javascript 复制代码
// 写法1:先接收props,再解构(新手易理解)
function Panel(props) {
  const { title, children } = props;
  // 后续使用title和children
}

// 写法2:参数中直接解构(更简洁,React中常用)
function Panel({ title, children }) {
  // 直接使用title和children
}
(4)对象解构的额外用法(拓展)
  • 给解构的变量重命名 (比如属性名不好听,或和现有变量冲突):

    javascript 复制代码
    const props = { title: "关于", c: "内容" };
    // 把c属性重命名为children
    const { title, c: children } = props;
    console.log(children); // 输出:内容
  • 设置默认值 (比如属性可能不存在时,避免 undefined):

    javascript 复制代码
    const props = { title: "关于" };
    // 如果props中没有children,默认值为"暂无内容"
    const { title, children = "暂无内容" } = props;
    console.log(children); // 输出:暂无内容

2. 数组解构(按位置提取数据)

数组解构和对象解构的区别是:对象解构按属性名匹配,数组解构按位置匹配

(1)没有解构的写法(繁琐)
javascript 复制代码
const arr = [10, 20, 30];
const a = arr[0];
const b = arr[1];
console.log(a, b); // 输出:10 20
(2)使用数组解构的写法(简洁)
javascript 复制代码
const arr = [10, 20, 30];
// 按位置提取第一个和第二个元素,赋值给a和b
const [a, b] = arr;
console.log(a, b); // 输出:10 20

// 跳过某个元素(比如跳过第二个,取第一个和第三个)
const [x, , y] = arr;
console.log(x, y); // 输出:10 30

三、解构在 React 中的常见使用场景

除了组件 props 解构,你还会在这些地方看到解构:

  1. 状态解构 :从useState的返回值中解构(数组解构)

    javascript 复制代码
    // useState返回的是一个数组:[状态值, 修改状态的方法]
    const [isActive, setIsActive] = useState(false);
  2. 状态对象解构 :从state中提取数据

    javascript 复制代码
    const [state, setState] = useState({
      list: [],
      total: 0
    });
    // 解构list和total
    const { list, total } = state;

我感觉像键值对,字典?还有python的元组。

你觉得 JavaScript 的对象解构更像 Python 里的键值对(字典),这个感受非常准确!

尤其是对象解构 ,本质就是按键(属性名) 来匹配数据,和 Python 字典的键值对逻辑高度契合;而之前提到的数组解构才是和 Python 元组 / 列表的按位置匹配更像。

我会帮你把这个对应关系理得更清楚。

一、先明确:JS 对象解构 ≈ Python 字典的键值对操作(按键匹配)

你之前看到的 JS 对象解构代码,核心就是按属性名(键)提取值,这和 Python 字典按 key 取值的逻辑完全一致,只是 JS 做了语法糖的简化。

1. JS 对象解构(按键匹配)
javascript 复制代码
// 定义一个JS对象(本质是键值对集合,类似Python字典)
const obj = {
  title: "关于",
  content: "阿拉木图的介绍"
};

// 对象解构:按键(title、content)提取值,赋值给同名变量
const { title, content } = obj;
// 等同于:const title = obj.title; const content = obj.content;
2. Python 字典的键值对操作(按 key 匹配)

Python 没有像 JS 那样的原生对象解构语法,但可以通过字典的 key 取值 实现相同的效果(Python 3.9 + 的结构化模式匹配更接近 JS 的解构):

python 复制代码
# 定义Python字典(键值对集合)
obj = {
    "title": "关于",
    "content": "阿拉木图的介绍"
}

# 方式1:传统按key取值(类似JS未解构的写法)
title = obj["title"]
content = obj["content"]
print(title, content)  # 输出:关于 阿拉木图的介绍

# 方式2:Python 3.9+ 结构化模式匹配(最接近JS的对象解构,按键匹配)
match obj:
    case {"title": title, "content": content}:
        print(title, content)  # 输出:关于 阿拉木图的介绍
3. 你贴的 "重命名" 写法:更体现键值对的对应关系

你之前提到的这段 JS 代码,键(原属性名)和值(新变量名) 的对应关系非常明显,完全就是键值对的逻辑:

javascript 复制代码
// JS对象解构:重命名(原键prop1 → 新变量x,本质是键值映射)
const obj = { prop1: 10, prop2: 20 };
const { prop1: x, prop2: y } = obj;
// 等同于:const x = obj.prop1; const y = obj.prop2;

对应 Python 的字典操作,就是把字典的 key(prop1)对应的值赋值给变量 x,这是纯粹的键值对映射逻辑。

二、再区分:JS 数组解构 ≈ Python 元组 / 列表解包(按位置匹配)

这是和 "键值对" 逻辑的核心区别,数组解构不看 "键",只看位置索引,和 Python 元组 / 列表的解包一致:

javascript 复制代码
// JS数组解构(按位置匹配,和键值对无关)
const arr = [10, 20, 30];
const [a, b] = arr; // a是第0位,b是第1位
python 复制代码
# Python元组/列表解包(按位置匹配)
tup = (10, 20, 30)
a, b = tup  # a是第0位,b是第1位

lst = [10, 20, 30]
a, b = lst  # 效果同上

三、一个形象的比喻:帮你记住两种解构的区别

  • JS 对象解构(键值对 / 字典逻辑) :就像你查字典,根据字(键 / 属性名) 找到对应的解释(值 / 变量),不管字在字典的第几页(位置),只看字本身。
  • JS 数组解构(位置 / 元组逻辑) :就像你排队领快递,根据排队的位置(索引) 领对应的包裹,不管包裹里是什么(值),只看位置。

四、React 中最常用的是 "键值对" 逻辑的对象解构

你在 React 代码中看到的解构,绝大多数是props 的对象解构,比如:

javascript 复制代码
// Panel组件的props是一个对象(键值对集合:title、children是键)
function Panel({ title, children }) {
  // 按键提取title和children的值
  return (
    <div>
      <h3>{title}</h3>
      <p>{children}</p>
    </div>
  );
}

// 使用组件时,props的键值对更明显:title是键,"关于"是值;children是键,文本是值
<Panel title="关于">
  阿拉木图的介绍内容
</Panel>

这里的 props 本质就是一个 JS 对象,所有属性传递都是键值对的形式,解构自然也遵循键值对逻辑,这也是你觉得像字典的核心原因。

总结

  1. JS 对象解构的核心是按「键(属性名)」匹配,和 Python 字典的键值对逻辑高度一致,这是你产生 "像字典" 感受的根本原因。
  2. JS 数组解构的核心是按「位置(索引)」匹配,和 Python 元组 / 列表的解包逻辑一致,和键值对无关。
  3. React 中最常用的是对象解构(props 解构),因此你会更多感受到 "键值对 / 字典" 的特性。
  4. 解构是 ES6 + 的语法糖 ,核心作用是快速从对象 / 数组中提取数据到变量,让代码更简洁。
  5. 对象解构按属性名匹配 (React 中 props 解构常用),数组解构按位置匹配(比如 useState 的返回值)。
  6. React 中最常见的解构场景是组件 props 解构useState 的数组解构,这是你后续写 React 代码会频繁用到的语法。

3 将事件处理函数作为 props 传递

https://zh-hans.react.dev/learn/responding-to-events#passing-event-handlers-as-props

javascript 复制代码
function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`正在播放 ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      播放 "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('正在上传!')}>
      上传图片
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="魔女宅急便" />
      <UploadButton />
    </div>
  );
}

一、先明确:为什么要把事件处理函数作为 props 传递?

在 React 中,props 是单向数据流 (父→子),子组件无法直接修改父组件的状态,也无法直接 "通知" 父组件发生了某个事件(比如点击)。而将事件处理函数作为 props 传递给子组件,就是子组件向父组件通信的唯一方式------ 子组件触发父组件传递过来的函数,从而让父组件执行对应的逻辑(比如修改状态、处理数据)。

简单来说:

  • 父组件:拥有数据(state)处理数据的函数
  • 子组件:只有UI 渲染用户交互(比如点击按钮),没有权限直接改父组件的数据。
  • 解决方案:父组件把处理函数通过 props 传给子组件,子组件触发这个函数,间接让父组件处理数据。

二、事件处理函数作为 props 传递的本质

你可以把这个过程理解为:父组件给子组件 "递了一个遥控器",子组件按下遥控器的按钮,父组件就会执行对应的操作

  • 函数本身是可传递的引用类型(和对象、数组一样),所以可以作为 props 的属性值传递。
  • 子组件接收到的不是函数的执行结果,而是函数的引用(相当于遥控器本身),只有当子组件触发(比如点击事件)时,函数才会执行。

三、结合你之前的代码,拆解传递的核心步骤

我们以你代码中的PlayButtonButton为例,再提炼一次核心流程,让你更聚焦 "函数传递" 这个点:

步骤 1:父组件(PlayButton)定义事件处理函数
javascript 复制代码
function PlayButton({ movieName }) {
  // 1. 定义处理函数(包含业务逻辑:弹出提示)
  const handlePlayClick = () => {
    alert(`正在播放 ${movieName}!`);
  };

  // 2. 将函数作为props(onClick)传递给子组件(Button)
  return (
    <Button onClick={handlePlayClick}>
      播放 "{movieName}"
    </Button>
  );
}

这里的关键:onClick={handlePlayClick}是传递函数引用 ,不是执行函数(如果写handlePlayClick(),组件渲染时就会立即执行,这是新手常见错误)。

步骤 2:子组件(Button)接收函数 props 并绑定到 DOM 事件
javascript 复制代码
function Button({ onClick, children }) {
  // 3. 接收父组件传递的onClick函数
  // 4. 将函数绑定到原生<button>的onClick事件(用户点击时触发)
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

这里的原生<button>onClickDOM 事件 ,当用户点击时,会调用绑定的onClick函数(即父组件的handlePlayClick)。

步骤 3:用户操作触发函数执行

用户点击按钮 → 原生onClick事件触发 → 执行父组件传递的handlePlayClick函数 → 执行业务逻辑(弹出提示)。

四、常见的两种函数传递形式(新手必知)

在 React 中,事件处理函数作为 props 传递有两种最常见的形式,你代码中都用到了:

形式 1:命名函数(推荐,逻辑复杂时用)
javascript 复制代码
// 父组件定义命名函数
function Parent() {
  const handleClick = () => {
    console.log("命名函数执行");
  };
  return <Child onClick={handleClick} />;
}

// 子组件接收并使用
function Child({ onClick }) {
  return <button onClick={onClick}>点击</button>;
}

优点:代码易读、易调试、可复用(同一个函数可以传给多个子组件)。

形式 2:匿名箭头函数(简洁,逻辑简单时用)
javascript 复制代码
// 父组件直接传递匿名箭头函数
function Parent() {
  return <Child onClick={() => console.log("匿名函数执行")} />;
}

// 子组件接收并使用(和命名函数完全一样)
function Child({ onClick }) {
  return <button onClick={onClick}>点击</button>;
}

优点 :写法简洁,适合一次性、简单的逻辑;缺点:每次渲染都会创建新的函数实例(性能影响极小,大部分场景可忽略)。

五、进阶:传递带参数的事件处理函数

很多时候,你需要给事件处理函数传递参数(比如列表项的 id),这时候需要注意写法,我们用一个例子说明:

javascript 复制代码
// 父组件:列表组件
function ItemList() {
  // 定义带参数的处理函数
  const handleItemClick = (itemId) => {
    console.log(`点击了列表项:${itemId}`);
  };

  const items = [1, 2, 3];

  return (
    <div>
      {items.map((itemId) => (
        // 传递带参数的函数给子组件
        <Item key={itemId} onItemClick={() => handleItemClick(itemId)} />
      ))}
    </div>
  );
}

// 子组件:列表项组件
function Item({ onItemClick }) {
  return <button onClick={onItemClick}>点击我</button>;
}

关键 :这里用() => handleItemClick(itemId)包裹,是为了延迟执行并传递参数 。如果直接写onItemClick={handleItemClick(itemId)},组件渲染时就会立即执行函数,这是错误的。

总结

  1. 核心目的 :将事件处理函数作为 props 传递,是 React 中子组件向父组件通信的唯一方式,解决了 props 单向数据流的限制。
  2. 传递本质 :传递的是函数的引用,而非函数的执行结果,只有子组件触发(如点击)时函数才会执行。
  3. 常见形式:命名函数(推荐复杂逻辑)和匿名箭头函数(适合简单逻辑),传递带参数的函数时需用箭头函数包裹延迟执行。

4 状态提升

javascript 复制代码
import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>哈萨克斯坦,阿拉木图</h2>
      <Panel
        title="关于"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
      </Panel>
      <Panel
        title="词源"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          显示
        </button>
      )}
    </section>
  );
}

这段代码是对之前面板组件的升级,实现了手风琴(Accordion)的核心逻辑------ 每次只能展开一个面板,点击不同面板的 "显示" 按钮时,会切换到对应面板展开,其他面板收起。

一、先总结这段代码的核心变化和功能

和之前的代码相比,核心变化是:

  1. 状态提升 :把原本每个Panel组件内部的isActive状态,提升到了父组件Accordion中,用activeIndex(活跃面板的索引)来统一管理所有面板的展开 / 收起状态。
  2. 父子组件通信 :父组件通过props给子组件传递是否激活的状态(isActive)点击按钮的回调函数(onShow),子组件触发回调时通知父组件更新状态。
  3. 实现互斥展开 :因为状态由父组件统一管理,所以每次只能有一个面板的isActivetrue,也就是只能展开一个面板。

二、逐部分拆解代码逻辑

1. 父组件Accordion:统一管理状态
javascript 复制代码
import { useState } from 'react';

export default function Accordion() {
  // 1. 定义状态:activeIndex表示当前活跃(展开)的面板索引,初始值为0(第一个面板默认展开)
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <>
      <h2>哈萨克斯坦,阿拉木图</h2>
      {/* 第一个Panel组件:索引0 */}
      <Panel
        title="关于"
        // 2. 传递isActive:判断当前面板是否是活跃面板(索引0是否等于activeIndex)
        isActive={activeIndex === 0}
        // 3. 传递onShow:点击按钮时,调用setActiveIndex将活跃索引设为0
        onShow={() => setActiveIndex(0)}
      >
        阿拉木图人口约200万...
      </Panel>
      {/* 第二个Panel组件:索引1 */}
      <Panel
        title="词源"
        // 2. 传递isActive:判断当前面板是否是活跃面板(索引1是否等于activeIndex)
        isActive={activeIndex === 1}
        // 3. 传递onShow:点击按钮时,调用setActiveIndex将活跃索引设为1
        onShow={() => setActiveIndex(1)}
      >
        这个名字来自于...
      </Panel>
    </>
  );
}

**关键知识点:状态提升(Lifting State Up)**这是 React 中的重要设计模式:当多个子组件需要共享状态或相互影响时,把状态从子组件移到父组件中,由父组件统一管理,再通过 props 把状态和修改状态的方法传递给子组件。这样就能实现子组件之间的协同工作。

2. 子组件Panel:接收 props 并渲染
javascript 复制代码
function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {/* 条件渲染:根据父组件传递的isActive决定显示内容还是按钮 */}
      {isActive ? (
        <p>{children}</p>
      ) : (
        // 点击按钮时,调用父组件传递的onShow回调函数
        <button onClick={onShow}>
          显示
        </button>
      )}
    </section>
  );
}

关键知识点:父子组件通信

  • 父组件→子组件:通过props传递数据(title/children/isActive)和方法(onShow)。
  • 子组件→父组件:子组件调用父组件传递的回调函数(onShow),通知父组件修改状态(这是 React 中子组件向父组件传值的唯一方式,因为 props 是单向的)。

三、代码的执行流程(一步一步看)

  1. 初始加载
    • Accordion组件的activeIndex初始值为0
    • 第一个PanelisActive0 === 0 → true,所以直接显示内容。
    • 第二个PanelisActive1 === 0 → false,所以显示 "显示" 按钮。
  2. 用户操作
    • 点击第二个Panel的 "显示" 按钮,触发onShow回调,调用setActiveIndex(1)
    • Accordion组件的activeIndex更新为1,组件重新渲染。
    • 第一个PanelisActive变为0 === 1 → false,切换为显示 "显示" 按钮。
    • 第二个PanelisActive变为1 === 1 → true,切换为显示内容。
  3. 再次操作 :点击第一个Panel的 "显示" 按钮,重复上述流程,切换回第一个面板展开。

四、拓展:优化代码(解决硬编码索引的问题)

当前代码中,面板的索引是硬编码的(0、1),如果新增面板需要手动加索引,不够灵活。可以把面板数据抽成数组,用map遍历渲染,更易维护:

javascript 复制代码
import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);

  // 抽成面板数据数组
  const panels = [
    {
      title: "关于",
      content: "阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。"
    },
    {
      title: "词源",
      content: "这个名字来自于 <span lang=\"kk-KZ\">алма</span>,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang=\"la\">Malus sieversii</i> 被认为是现今苹果的祖先。"
    }
  ];

  return (
    <>
      <h2>哈萨克斯坦,阿拉木图</h2>
      {/* 遍历数组渲染面板,索引由map自动生成 */}
      {panels.map((panel, index) => (
        <Panel
          key={index} // 注意:实际项目中最好用唯一id,这里用index仅作示例
          title={panel.title}
          isActive={activeIndex === index}
          onShow={() => setActiveIndex(index)}
        >
          {/* 注意:content包含HTML标签,需要用dangerouslySetInnerHTML,否则会被当作文本 */}
          <span dangerouslySetInnerHTML={{ __html: panel.content }} />
        </Panel>
      ))}
    </>
  );
}

// Panel组件保持不变
function Panel({ title, children, isActive, onShow }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>显示</button>
      )}
    </section>
  );
}

注意dangerouslySetInnerHTML用于渲染包含 HTML 标签的字符串,因为 React 默认会转义 HTML,直接写会显示成文本。使用时要确保内容是可信的,避免 XSS 攻击。

总结

  1. 这段代码的核心是状态提升:将多个子组件的状态统一放到父组件管理,实现面板的互斥展开。
  2. 父子组件通过props通信:父组件传状态(isActive)和回调(onShow),子组件触发回调更新父组件状态。
  3. 实现了手风琴效果的核心逻辑:每次只能有一个面板处于展开状态,点击切换面板。
相关推荐
江苏世纪龙科技2 小时前
汽车结构原理VR课:看得见、摸得着的机械世界
学习
小oo呆2 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Tools
前端·javascript·easyui
be or not to be2 小时前
HTML 与 CSS 基础入门笔记
css·笔记·html
亿元程序员2 小时前
你知道三国志战略版是怎么实现横竖屏动态切换的吗?
前端
BD_Marathon2 小时前
Vue3_双向绑定
前端·javascript·vue.js
AI视觉网奇2 小时前
ue 蓝图 学笔记
笔记·ue5
崇山峻岭之间2 小时前
Matlab学习记录09
开发语言·学习·matlab
小白学大数据2 小时前
Temu 商品历史价格趋势爬虫与分析
开发语言·javascript·爬虫·python
走在路上的菜鸟2 小时前
Android学Dart学习笔记第二十五节 类修饰符
android·笔记·学习·flutter