React State
在 React 中,Props 是父组件向子组件传递数据的主要方式。我们提供的 ExerciseCard
组件就是一个很好的例子,它通过 props 接收 title
, description
, imageUrl
, link
, 和 tags
来展示练习信息。
// src/components/ExerciseCard.js (初始版本 - 仅依赖 Props)// (这是您提供的 ExerciseCard.js 的核心结构)import React from "react";
export default function ExerciseCard({ title, description, imageUrl, link, tags,}) { return ( <div className="bg-white rounded-lg shadow-lg overflow-hidden transform transition-all duration-300 hover:shadow-2xl hover:-translate-y-1"> <img className="w-full h-48 object-cover" src={imageUrl} alt={title || "Exercise Image"} /> <div className="p-6"> <h3 className="text-2xl font-semibold text-gray-800 mb-2"> {title || "练习标题"} </h3> <p className="text-gray-600 text-sm mb-4 leading-relaxed"> {description || "这里是练习的简要描述,介绍练习的主要内容和目标。"} </p> {/* ... 其他 props 相关的渲染 ... */} {link && ( <a href={link} target="_blank" rel="noopener noreferrer" className="inline-block bg-rose-600 text-white px-6 py-2 rounded-md font-medium transform transition-transform duration-200 hover:scale-105 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50" > 查看练习 </a> )} </div> </div> );}
Props 对于子组件来说是只读的。但如果我们希望 ExerciseCard
能够响应用户的特定操作并改变自身某些方面的显示呢?例如,我们想在卡片上添加一个“收藏”按钮,用户点击它可以将卡片标记为“已收藏”,再次点击则取消收藏。这种需要在组件内部进行管理并且会随用户交互而变化的数据,就需要使用 React 的 State。
State 是组件内部私有的、可以由组件自己管理和修改的数据。当 State 发生变化时,React 会自动重新渲染组件,使界面反映出最新的数据。
为 ExerciseCard
添加“收藏”State (Next.js App Router)
现在,我们将修改 ExerciseCard
组件,使其使用内部 State 来管理“收藏”状态。由于我们将使用 React Hooks (useState
) 和事件处理程序 (onClick
),在 Next.js App Router 环境下,我们需要将这个组件标记为客户端组件。
核心步骤:
- 添加
"use client";
指令:在文件顶部声明,将组件标记为客户端组件。 - 引入
useState
Hook:React 提供了useState
Hook 来在函数组件中添加和管理 State。 - 声明 State 变量:使用
useState
来声明一个表示收藏状态的 State 变量(例如isFavorited
)和一个用于更新它的函数(例如setIsFavorited
)。初始状态通常是未收藏 (false
)。 - 添加交互元素(收藏按钮):在 JSX 中添加一个按钮,用于触发收藏状态的切换。
- 创建事件处理函数:编写一个函数,当收藏按钮被点击时,它会调用
setIsFavorited
来切换isFavorited
的值。 - 根据 State 更新 UI:修改收藏按钮的文本或样式,使其根据内部的
isFavorited
State 来显示。
修改后的 ExerciseCard.js
(带收藏功能和 “use client”)
// src/components/ExerciseCard.js (添加 State 实现收藏功能)"use client"; // 1. 标记为客户端组件
import React, { useState } from 'react'; // 2. 引入 useState
export default function ExerciseCard({ title, description, imageUrl, link, tags,}) { // 3. 声明 State 变量来管理收藏状态 // 初始状态为未收藏 (false) const [isFavorited, setIsFavorited] = useState(false);
// 5. 创建事件处理函数来切换收藏状态 const handleToggleFavorite = () => { setIsFavorited(!isFavorited); // 将 isFavorited 的值取反 };
return ( <div className="bg-white rounded-lg shadow-lg overflow-hidden transform transition-all duration-300 hover:shadow-2xl hover:-translate-y-1"> <img className="w-full h-48 object-cover" src={imageUrl} alt={title || "Exercise Image"} /> <div className="p-6"> <h3 className="text-2xl font-semibold text-gray-800 mb-2"> {title || "练习标题"} </h3> <p className="text-gray-600 text-sm mb-4 leading-relaxed"> {description || "这里是练习的简要描述,介绍练习的主要内容和目标。"} </p>
{tags && tags.length > 0 && ( <div className="mb-4"> {tags.map((tag, index) => ( <span key={index} className="inline-block bg-sky-100 text-sky-700 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded-full" > {tag} </span> ))} </div> )}
<div className="flex items-center justify-between mt-4"> {/* 查看练习按钮 */} {link ? ( <a href={link} target="_blank" rel="noopener noreferrer" className="inline-block bg-rose-600 text-white px-6 py-2 rounded-md font-medium transform transition-transform duration-200 hover:scale-105 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-opacity-50" > 查看练习 </a> ) : ( <p className="text-sm text-gray-400">暂无在线链接</p> )}
{/* 4. & 6. 添加收藏按钮并根据 State 更新 UI */} <button onClick={handleToggleFavorite} className={`px-4 py-2 rounded-md font-medium text-sm transition-colors duration-200 ${ isFavorited ? "bg-amber-500 text-white hover:bg-amber-600" // 已收藏样式 : "bg-gray-200 text-gray-700 hover:bg-gray-300" // 未收藏样式 }`} > {isFavorited ? "已收藏 ★" : "收藏 ☆"} </button> </div> </div> </div> );}
代码讲解
-
"use client";
这是 Next.js App Router 中的一个重要指令。它必须是文件的第一行代码(在所有 imports 之前)。- 作用:它声明该文件及其导出的组件是“客户端组件”。默认情况下,App Router 中的组件是 React Server Components (RSC),它们在服务器上渲染,并且不能使用客户端特有的功能,如
useState
,useEffect
, 或浏览器事件处理器 (如onClick
)。 - 为什么需要:因为我们要使用
useState
来管理收藏状态,并且要响应用户的onClick
事件来切换这个状态,所以ExerciseCard
必须作为客户端组件来运行。客户端组件的代码会被发送到浏览器并在浏览器中执行和渲染,允许它们具有交互性。
- 作用:它声明该文件及其导出的组件是“客户端组件”。默认情况下,App Router 中的组件是 React Server Components (RSC),它们在服务器上渲染,并且不能使用客户端特有的功能,如
-
import React, { useState } from 'react';
我们从react
包中导入useState
。这是在函数组件中使用 State 的必要步骤。 -
const [isFavorited, setIsFavorited] = useState(false);
在ExerciseCard
函数组件内部,我们调用useState
。useState(false)
:我们将false
作为isFavorited
状态的初始值,表示卡片默认未被收藏。useState
返回一个包含两个元素的数组:isFavorited
:当前的 State 值(true
或false
)。React 会在组件的多次渲染之间“记住”这个值。setIsFavorited
:一个函数,用于更新isFavorited
这个 State 值。当你调用这个函数并传入新的状态值时,React 会安排一次组件的重新渲染。
-
添加收藏按钮 (JSX) 我们在卡片底部(与“查看练习”按钮并排)添加了一个新的
<button>
元素。onClick={handleToggleFavorite}
:将按钮的点击事件绑定到handleToggleFavorite
函数。- 按钮的文本和样式会根据
isFavorited
的状态动态改变:- 文本:如果
isFavorited
是true
,显示 “已收藏 ★“;否则显示 “收藏 ☆”。 - 样式 (
className
):如果isFavorited
是true
,应用表示“已收藏”的样式 (如bg-amber-500
);否则应用表示“未收藏”的样式 (如bg-gray-200
)。
- 文本:如果
-
const handleToggleFavorite = () => { setIsFavorited(!isFavorited); };
这是一个事件处理函数。当用户点击收藏按钮时,这个函数会被执行。setIsFavorited(!isFavorited)
:这行代码是核心。它调用状态更新函数setIsFavorited
,并传入当前isFavorited
值的反值(true
变false
,false
变true
)。- 调用
setIsFavorited
会告诉 React:“这个组件的isFavorited
状态已经改变了,请重新渲染它。”
-
根据 State 更新 UI (已在第4点中结合说明) 当
isFavorited
状态改变并触发重新渲染后,按钮的文本和背景色会因为依赖于isFavorited
而自动更新,从而向用户提供即时反馈。
效果演示
现在,每个 ExerciseCard
实例都拥有了自己独立的“收藏”状态。当用户与收藏按钮交互时:
- 点击按钮,
handleToggleFavorite
函数被调用。 setIsFavorited
更新组件内部的isFavorited
State。- React 检测到 State 变化,自动重新渲染该
ExerciseCard
组件 (在客户端)。 - 按钮的文本(“收藏 ☆” / “已收藏 ★“)和样式会根据新的
isFavorited
值进行更新,用户可以立刻看到卡片的收藏状态发生了变化。
这个 ExerciseCard
组件因此变得更加生动和具有交互性了!
总结
通过在文件顶部添加 "use client";
指令,并结合 useState
Hook,我们成功地为 ExerciseCard
组件赋予了管理内部“收藏”状态的能力,并使其能够在 Next.js App Router 环境下正确地实现客户端交互。这使得组件能够独立响应用户的交互(点击收藏按钮),并动态地更新其自身的外观和行为。这是构建复杂、用户友好的 React 应用中非常常见且重要的一种模式。
State 是组件的“记忆”,让它能够随着时间推移和用户输入而改变。