Skip to content

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 环境下,我们需要将这个组件标记为客户端组件。

核心步骤:

  1. 添加 "use client"; 指令:在文件顶部声明,将组件标记为客户端组件。
  2. 引入 useState Hook:React 提供了 useState Hook 来在函数组件中添加和管理 State。
  3. 声明 State 变量:使用 useState 来声明一个表示收藏状态的 State 变量(例如 isFavorited)和一个用于更新它的函数(例如 setIsFavorited)。初始状态通常是未收藏 (false)。
  4. 添加交互元素(收藏按钮):在 JSX 中添加一个按钮,用于触发收藏状态的切换。
  5. 创建事件处理函数:编写一个函数,当收藏按钮被点击时,它会调用 setIsFavorited 来切换 isFavorited 的值。
  6. 根据 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>
);
}

代码讲解

  1. "use client"; 这是 Next.js App Router 中的一个重要指令。它必须是文件的第一行代码(在所有 imports 之前)。

    • 作用:它声明该文件及其导出的组件是“客户端组件”。默认情况下,App Router 中的组件是 React Server Components (RSC),它们在服务器上渲染,并且不能使用客户端特有的功能,如 useState, useEffect, 或浏览器事件处理器 (如 onClick)。
    • 为什么需要:因为我们要使用 useState 来管理收藏状态,并且要响应用户的 onClick 事件来切换这个状态,所以 ExerciseCard 必须作为客户端组件来运行。客户端组件的代码会被发送到浏览器并在浏览器中执行和渲染,允许它们具有交互性。
  2. import React, { useState } from 'react'; 我们从 react 包中导入 useState。这是在函数组件中使用 State 的必要步骤。

  3. const [isFavorited, setIsFavorited] = useState(false);ExerciseCard 函数组件内部,我们调用 useState

    • useState(false):我们将 false 作为 isFavorited 状态的初始值,表示卡片默认未被收藏。
    • useState 返回一个包含两个元素的数组:
      • isFavorited:当前的 State 值(truefalse)。React 会在组件的多次渲染之间“记住”这个值。
      • setIsFavorited:一个函数,用于更新 isFavorited 这个 State 值。当你调用这个函数并传入新的状态值时,React 会安排一次组件的重新渲染。
  4. 添加收藏按钮 (JSX) 我们在卡片底部(与“查看练习”按钮并排)添加了一个新的 <button> 元素。

    • onClick={handleToggleFavorite}:将按钮的点击事件绑定到 handleToggleFavorite 函数。
    • 按钮的文本和样式会根据 isFavorited 的状态动态改变:
      • 文本:如果 isFavoritedtrue,显示 “已收藏 ★“;否则显示 “收藏 ☆”。
      • 样式 (className):如果 isFavoritedtrue,应用表示“已收藏”的样式 (如 bg-amber-500);否则应用表示“未收藏”的样式 (如 bg-gray-200)。
  5. const handleToggleFavorite = () => { setIsFavorited(!isFavorited); }; 这是一个事件处理函数。当用户点击收藏按钮时,这个函数会被执行。

    • setIsFavorited(!isFavorited):这行代码是核心。它调用状态更新函数 setIsFavorited,并传入当前 isFavorited 值的反值(truefalsefalsetrue)。
    • 调用 setIsFavorited 会告诉 React:“这个组件的 isFavorited 状态已经改变了,请重新渲染它。”
  6. 根据 State 更新 UI (已在第4点中结合说明)isFavorited 状态改变并触发重新渲染后,按钮的文本和背景色会因为依赖于 isFavorited 而自动更新,从而向用户提供即时反馈。

效果演示

现在,每个 ExerciseCard 实例都拥有了自己独立的“收藏”状态。当用户与收藏按钮交互时:

  1. 点击按钮,handleToggleFavorite 函数被调用。
  2. setIsFavorited 更新组件内部的 isFavorited State。
  3. React 检测到 State 变化,自动重新渲染该 ExerciseCard 组件 (在客户端)。
  4. 按钮的文本(“收藏 ☆” / “已收藏 ★“)和样式会根据新的 isFavorited 值进行更新,用户可以立刻看到卡片的收藏状态发生了变化。

这个 ExerciseCard 组件因此变得更加生动和具有交互性了!

总结

通过在文件顶部添加 "use client"; 指令,并结合 useState Hook,我们成功地为 ExerciseCard 组件赋予了管理内部“收藏”状态的能力,并使其能够在 Next.js App Router 环境下正确地实现客户端交互。这使得组件能够独立响应用户的交互(点击收藏按钮),并动态地更新其自身的外观和行为。这是构建复杂、用户友好的 React 应用中非常常见且重要的一种模式。

State 是组件的“记忆”,让它能够随着时间推移和用户输入而改变。