🧩 React 组件定义、组合与传值:课程设计

1. 课程概述

本课程旨在系统讲解 React 组件的定义方式组合模式 以及组件间的数据传递方法。组件是 React 应用的基石,理解如何创建、组合组件并管理它们之间的数据流,是构建复杂且可维护用户界面的关键。学生将学会使用函数组件和类组件,掌握 Props 的多种传递技巧,并理解单向数据流在实践中的应用。

2. 课程目标

目标类型 描述
知识目标 理解 函数组件类组件 的定义方式与区别。理解 Props 的概念、特性和使用方法(包括只读性)。掌握 组件组合 的思想与模式(包含关系、特殊化)。理解 单向数据流 和组件间通信的各种方式(父子、子父、兄弟、跨级)。
技能目标 能使用 函数组件类组件 定义组件。能通过 Props 进行父到子的数据传递和方法传递。能通过 回调函数 实现子到父的数据传递。能使用 状态提升 实现兄弟组件间的通信。能使用 Context 进行跨层级组件的数据传递。能根据场景选择合适的组件通信方式。

3. 核心知识点与实例代码

3.1 React 组件定义

React 组件是构建用户界面的独立、可复用的单元,它接收输入(称为 “props”)并返回描述页面该部分内容的 React 元素。

函数组件 (Function Components)

函数组件是使用 JavaScript 函数定义的组件,它接收一个 props 对象作为参数,并返回一个 React 元素。

  • 知识目标:理解函数组件的简洁性和无状态性(在 Hooks 之前)。
  • 技能目标:能编写基本的函数组件,并能正确接收和使用 props。
// 定义一个简单的函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// 使用箭头函数定义
const Welcome = (props) => {
  return <h1>Hello, {props.name}!</h1>;
};

// 使用解构赋值简化 props 获取
const Welcome = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
    </div>
  );
};

// 使用组件
function App() {
  return (
    <div>
      <Welcome name="Alice" age={25} />
      <Welcome name="Bob" age={30} />
    </div>
  );
}
类组件 (Class Components)

类组件是使用 ES6 class 定义的组件,它继承自 React.Component,并且必须包含一个 render() 方法。

  • 知识目标:理解类组件可以拥有本地状态(state)和生命周期方法。
  • 技能目标:能编写基本的类组件,能在 render() 方法中返回 JSX,并能通过 this.props 访问传入的属性。
// 定义一个类组件
class Welcome extends React.Component {
  render() {
    // 使用 this.props 访问传入的属性
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

// 使用组件(与函数组件相同)
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
    </div>
  );
}
函数组件 vs. 类组件

为了更清晰地理解两者的区别和选择,请看下表:

特性 函数组件 类组件
语法 JavaScript 函数 ES6 Class
状态管理 需使用 Hooks (如 useState) 使用 this.statesetState
生命周期 需使用 Hooks (如 useEffect) 使用生命周期方法 (如 componentDidMount)
代码简洁性 更简洁,逻辑更集中 相对繁琐,需要绑定 this 等操作
学习曲线 较平缓,易于理解 较陡峭,需理解 Class 和 this
推荐使用 React 16.8+ 后的主流方式 旧项目维护,或需要 Error Boundaries 时

3.2 组件组合 (Component Composition)

组件组合是 React 的核心思想之一,它通过将多个小组件嵌套组合来构建复杂的 UI,而不是通过继承。

  • 知识目标:理解“组合优于继承”的设计原则,理解如何使用 children prop 和自定义 props 来实现组合。
  • 技能目标:能设计可组合的组件结构,能利用 children 实现内容的灵活嵌套。
// 1. 使用 children prop 实现包含关系
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">{children}</div> {/* 子内容在此渲染 */}
    </div>
  );
}

function App() {
  return (
    <Card title="用户信息">
      <p>姓名:张三</p> {/* 这些元素就是 children */}
      <p>年龄:25</p>
    </Card>
  );
}

// 2. 使用自定义 props 实现特殊化
function ContactCard({ contact }) {
  return (
    <Card title="联系方式">
      <p>电话:{contact.phone}</p>
      <p>邮箱:{contact.email}</p>
    </Card>
  );
}

3.3 组件传值 (Component Communication)

数据在 React 组件中沿着组件树自上而下单向流动。以下是常见的传值场景。

父组件向子组件传递数据 (Props Down)

父组件通过 props 将数据传递给子组件,这是 React 中最基本、最常用的通信方式。

  • 知识目标:理解 Props 是只读的,子组件不能直接修改接收到的 Props。
  • 技能目标:能熟练地在父组件中传递 props,在子组件中接收和使用 props。
// 父组件
function ParentComponent() {
  const user = { name: 'Alice', age: 25 };
  const greeting = "Hello from Parent!";

  return (
    <div>
      {/* 传递字符串、对象、数字等任何类型的数据 */}
      <ChildComponent 
        message={greeting} 
        userInfo={user} 
        score={100} 
      />
    </div>
  );
}

// 子组件
function ChildComponent(props) {
  // 可以通过 props 对象访问
  return (
    <div>
      <p>{props.message}</p>
      <p>Name: {props.userInfo.name}, Age: {props.userInfo.age}</p>
    </div>
  );
}

// 或者在函数参数中直接解构
function ChildComponent({ message, userInfo, score }) {
  return (
    <div>
      <p>{message}</p>
      <p>Name: {userInfo.name}, Age: {userInfo.age}</p>
      <p>Score: {score}</p>
    </div>
  );
}
子组件向父组件传递数据 (Callback Up)

子组件通过调用父组件传递下来的回调函数,将数据作为参数传递给父组件。

  • 知识目标:理解数据反向流动的模式,理解回调函数的作用。
  • 技能目标:能在父组件中定义回调函数并通过 props 传递给子组件,能在子组件中调用该回调并传递数据。
// 父组件
function ParentComponent() {
  const [dataFromChild, setDataFromChild] = useState('');

  // 定义回调函数,用于接收子组件数据
  const handleDataFromChild = (childData) => {
    setDataFromChild(childData);
    console.log('Received from child:', childData);
  };

  return (
    <div>
      <p>Data from child: {dataFromChild}</p>
      {/* 将回调函数传递给子组件 */}
      <ChildComponent onSendData={handleDataFromChild} />
    </div>
  );
}

// 子组件
function ChildComponent({ onSendData }) {
  const handleClick = () => {
    // 调用父组件传递下来的回调函数,并传递数据
    onSendData('Hello from Child Component!');
  };

  return (
    <div>
      <button onClick={handleClick}>Send Data to Parent</button>
    </div>
  );
}
兄弟组件间通信 (State Lifting Up)

兄弟组件之间不能直接传递数据,需要通过提升状态到它们共同的父组件来实现共享。

  • 知识目标:理解“状态提升”的概念和必要性。
  • 技能目标:能将需要共享的状态提升至最近的共同父组件,并通过 props 向下传递和回调函数向上传递来实现兄弟组件间的同步。
// 共同的父组件
function ParentComponent() {
  // 将状态提升到父组件
  const [sharedData, setSharedData] = useState('');

  // 用于更新状态的回调函数
  const handleDataChange = (newData) => {
    setSharedData(newData);
  };

  return (
    <div>
      {/* BrotherA 可以修改状态 */}
      <BrotherA onDataChange={handleDataChange} />
      {/* BrotherB 可以显示状态 */}
      <BrotherB sharedData={sharedData} />
    </div>
  );
}

// 兄弟组件A(负责发送数据)
function BrotherA({ onDataChange }) {
  const [inputValue, setInputValue] = useState('');

  const sendData = () => {
    onDataChange(inputValue);
  };

  return (
    <div>
      <input 
        value={inputValue} 
        onChange={(e) => setInputValue(e.target.value)} 
      />
      <button onClick={sendData}>Update Brother B</button>
    </div>
  );
}

// 兄弟组件B(负责接收并显示数据)
function BrotherB({ sharedData }) {
  return (
    <div>
      <p>Message from Brother A: {sharedData}</p>
    </div>
  );
}
跨层级组件通信 (Context API)

对于需要跨越多层组件传递数据的场景(如主题、用户信息等),使用 Context 可以避免“prop drilling”(逐层传递 props)的麻烦。

  • 知识目标:理解 Context 的应用场景(全局数据共享)。
  • 技能目标:能使用 createContext, Provider, useContext 来创建和消费 Context。
// 1. 创建一个 Context
const UserContext = React.createContext(); // 可以提供一个默认值

// 2. 在顶层组件提供数据(Provider)
function App() {
  const [currentUser, setCurrentUser] = useState({ name: 'John', id: 1 });

  return (
    // 使用 Provider 包裹组件树,并传递 value
    <UserContext.Provider value={{ currentUser, setCurrentUser }}>
      <Header />
      <Sidebar />
      <MainContent />
    </UserContext.Provider>
  );
}

// 3. 在底层任何组件消费数据(useContext)
function Header() {
  // 使用 useContext Hook 获取 Context 的值
  const { currentUser } = useContext(UserContext);

  return (
    <header>
      <p>Welcome, {currentUser.name}!</p>
    </header>
  );
}

// 另一个深层级的组件也可以直接获取
function SomeNestedComponent() {
  const { setCurrentUser } = useContext(UserContext);

  const handleLogout = () => {
    setCurrentUser(null);
  };

  return <button onClick={handleLogout}>Logout</button>;
}

4. 综合实战案例:用户评论列表

下面是一个融合了组件定义、组合和各种传值方式的综合示例:

import React, { useState, useContext } from 'react';

// 创建一个 Context 用于主题切换
const ThemeContext = React.createContext();

// 主应用组件
function App() {
  const [theme, setTheme] = useState('light');
  const [comments, setComments] = useState([
    { id: 1, text: 'Great post!', author: 'Alice' },
    { id: 2, text: 'Thanks for sharing.', author: 'Bob' }
  ]);

  // 添加新评论的函数(状态提升到App)
  const addComment = (commentText, author) => {
    const newComment = {
      id: Date.now(),
      text: commentText,
      author: author || 'Anonymous'
    };
    setComments([...comments, newComment]);
  };

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <div className={`app ${theme}`}>
        <Header />
        {/* CommentForm 接收一个回调函数 onAddComment */}
        <CommentForm onAddComment={addComment} />
        {/* CommentList 接收 comments 数组作为 prop */}
        <CommentList comments={comments} />
      </div>
    </ThemeContext.Provider>
  );
}

// Header 组件(消费 ThemeContext)
function Header() {
  const { theme, setTheme } = useContext(ThemeContext);

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <header>
      <h1>React Comment App</h1>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
      </button>
    </header>
  );
}

// 评论表单组件(子传父)
function CommentForm({ onAddComment }) {
  const [text, setText] = useState('');
  const [author, setAuthor] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAddComment(text, author); // 调用父组件传递的回调
      setText(''); // 清空输入框
      setAuthor('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          Your Name:
          <input 
            type="text" 
            value={author} 
            onChange={(e) => setAuthor(e.target.value)} 
            placeholder="Optional"
          />
        </label>
      </div>
      <div>
        <label>
          Comment:
          <textarea 
            value={text} 
            onChange={(e) => setText(e.target.value)} 
            required 
          />
        </label>
      </div>
      <button type="submit">Add Comment</button>
    </form>
  );
}

// 评论列表组件(父传子)
function CommentList({ comments }) {
  return (
    <div className="comment-list">
      <h2>Comments ({comments.length})</h2>
      {comments.length === 0 ? (
        <p>No comments yet.</p>
      ) : (
        comments.map(comment => (
          <CommentItem key={comment.id} comment={comment} />
        ))
      )}
    </div>
  );
}

// 评论项组件(接收单个 comment 对象作为 prop)
function CommentItem({ comment }) {
  return (
    <div className="comment-item">
      <strong>{comment.author}:</strong>
      <p>{comment.text}</p>
    </div>
  );
}

export default App;

案例技能点解析

  • 组件定义:使用了函数组件(App, Header, CommentForm, CommentList, CommentItem)。
  • 组件组合App 组件组合了多个子组件,CommentList 又组合了多个 CommentItem
  • 父传子 (Props Down)AppCommentList 传递 comments 数组;CommentList 向每个 CommentItem 传递 comment 对象。
  • 子传父 (Callback Up)AppCommentForm 传递 addComment 回调函数,CommentForm 在提交表单时调用它来向上传递新的评论数据。
  • ContextThemeContextApp 提供,并被 Header 组件消费,用于切换主题,避免了通过 props 层层传递 themesetTheme
  • 状态提升:评论数据 comments 和更新函数 setComments 被提升到共同的父组件 App 中管理,使得 CommentFormCommentList 可以共享和同步状态。

5. 课后练习与思考

  1. 基础练习

    • 创建一个 Button 组件,接收 text(按钮文字)和 onClick(点击回调)两个 props。
    • 创建一个 UserCard 组件,接收一个包含用户信息(name, avatar, bio)的对象作为 prop,并展示出来。
  2. 进阶挑战

    • 构建一个简单的“待办事项列表”(Todo List)。
      • App 组件管理一个 todos 数组状态。
      • TodoForm 组件通过回调函数向 App 添加新任务。
      • TodoList 组件接收 todos 数组和另一个回调函数 onToggleTodo,用于渲染列表和切换某项的完成状态。
    • 使用 Context 实现一个全局的主题切换功能,让应用中的多个组件都能根据主题改变样式。
  3. 思考题

    • 为什么 React 强调“组合优于继承”?组合模式带来了哪些好处?
    • 在什么情况下你会选择使用 Context API?它又能解决什么问题?
    • 函数组件和类组件的主要区别是什么?在现代 React 开发中,更推荐使用哪种?为什么?

更多推荐