在这里插入图片描述

组件与Props

一、组件是什么?

1.1 5W1H分析

/**
 * 5W1H 分析:组件与Props
 * 
 * What: 组件是 React 的最小复用单元,Props 是传递给组件的数据
 * Why: 组件化开发提高代码复用性和可维护性
 * Who: 所有 React 开发者
 * When: 构建 UI 时,将界面拆分为独立、可复用的部分
 * Where: 函数组件、类组件(推荐函数组件)
 * How: 定义组件函数,通过参数接收 props,返回 JSX
 */

console.log("=" .repeat(60));
console.log("React 19 学习路线 - 第3篇:组件与Props");
console.log("=" .repeat(60));

// 组件示例
function Welcome(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// 使用组件
const element = <Welcome name="React Learner" />;

1.2 组件类型对比

/**
 * 函数组件 vs 类组件
 */

// 1. 函数组件(推荐 - React 16.8+)
function FunctionalComponent({ name, age }) {
  const [count, setCount] = useState(0);
  
  return (
    <div className="functional">
      <h2>函数组件</h2>
      <p>姓名: {name}, 年龄: {age}</p>
      <button onClick={() => setCount(c => c + 1)}>
        点击次数: {count}
      </button>
    </div>
  );
}

// 2. 类组件(传统方式)
class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  render() {
    return (
      <div className="class-component">
        <h2>类组件</h2>
        <p>姓名: {this.props.name}, 年龄: {this.props.age}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          点击次数: {this.state.count}
        </button>
      </div>
    );
  }
}

// 3. 函数组件 vs 类组件对比
const comparison = {
  函数组件: {
    语法: "更简洁",
    this绑定: "不需要",
    生命周期: "useEffect",
    状态管理: "useState",
    性能: "更优",
    学习曲线: "平缓"
  },
  类组件: {
    语法: "较复杂",
    this绑定: "需要",
    生命周期: "componentDidMount等",
    状态管理: "this.state",
    性能: "稍差",
    学习曲线: "陡峭"
  }
};

二、函数组件详解

2.1 基础函数组件

// 1. 基本定义
function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

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

// 3. 隐式返回(适用于简单组件)
const GreetingImplicit = (props) => <h1>Hello, {props.name}!</h1>;

// 4. 解构props
function GreetingDestructured({ name, age, city }) {
  return (
    <div>
      <h2>你好,{name}!</h2>
      <p>年龄: {age}岁</p>
      <p>城市: {city}</p>
    </div>
  );
}

// 5. 带默认值的解构
function GreetingWithDefaults({ name = "游客", age = 18, city = "未知" }) {
  return (
    <div>
      <h2>你好,{name}!</h2>
      <p>年龄: {age}岁</p>
      <p>城市: {city}</p>
    </div>
  );
}

// 6. 带children的组件
function Card({ title, children, footer }) {
  return (
    <div className="card">
      <div className="card-header">{title}</div>
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// 使用示例
function App() {
  return (
    <div>
      <Greeting name="React学习者" />
      <GreetingDestructured name="张三" age={25} city="北京" />
      <GreetingWithDefaults />
      
      <Card title="卡片标题">
        <p>这是卡片的内容区域</p>
        <button>操作按钮</button>
      </Card>
    </div>
  );
}

2.2 组件命名规范

/**
 * 组件命名规范
 * 1. 使用 PascalCase(大驼峰)命名
 * 2. 组件名应具有描述性
 * 3. 文件名与组件名保持一致
 */

// ✅ 正确:PascalCase
function UserProfile() { return <div>用户资料</div>; }
function ShoppingCart() { return <div>购物车</div>; }
function ButtonGroup() { return <div>按钮组</div>; }

// ❌ 错误:小驼峰或小写
// function userProfile() { ... }
// function shoppingcart() { ... }

// 目录结构建议
// src/
//   components/
//     common/
//       Button.jsx
//       Input.jsx
//       Card.jsx
//     user/
//       UserProfile.jsx
//       UserAvatar.jsx
//     product/
//       ProductList.jsx
//       ProductCard.jsx

// 默认导出 vs 命名导出
// 默认导出(推荐用于主要组件)
export default function UserProfile() { ... }

// 命名导出(推荐用于工具组件或需要多个导出的文件)
export const Button = () => { ... };
export const Input = () => { ... };

三、Props详解

3.1 Props传递

/**
 * Props 传递方式
 */

// 1. 基础传递
function UserCard({ user }) {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// 2. 多层传递(Props Drilling)
function GrandParent() {
  const user = { name: '张三', age: 25 };
  return <Parent user={user} />;
}

function Parent({ user }) {
  return <Child user={user} />;
}

function Child({ user }) {
  return <div>{user.name}</div>;
}

// 3. 展开运算符传递
function Button({ type, size, disabled, children, onClick }) {
  // 方式1:逐个传递
  return (
    <button
      type={type}
      className={`btn btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// 方式2:使用展开运算符
function ButtonSpread(props) {
  const { children, ...restProps } = props;
  return <button {...restProps}>{children}</button>;
}

// 使用示例
<ButtonSpread
  type="submit"
  className="btn-primary"
  disabled={false}
  onClick={() => console.log('clicked')}
>
  提交
</ButtonSpread>

// 4. 传递组件作为Props
function Layout({ header, sidebar, content, footer }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <div className="layout-main">
        <aside>{sidebar}</aside>
        <main>{content}</main>
      </div>
      <footer>{footer}</footer>
    </div>
  );
}

// 使用
<Layout
  header={<Header />}
  sidebar={<Sidebar />}
  content={<MainContent />}
  footer={<Footer />}
/>

3.2 Props类型检查

import PropTypes from 'prop-types';

/**
 * PropTypes 类型检查
 */

function UserProfile({ name, age, email, isActive, role, friends, settings }) {
  return (
    <div className="user-profile">
      <h3>{name}</h3>
      <p>年龄: {age}</p>
      <p>邮箱: {email}</p>
      <p>状态: {isActive ? '活跃' : '离线'}</p>
      <p>角色: {role}</p>
      <p>好友数: {friends.length}</p>
    </div>
  );
}

// 定义 Props 类型
UserProfile.propTypes = {
  // 基本类型
  name: PropTypes.string.isRequired,        // 必填字符串
  age: PropTypes.number,                    // 可选数字
  email: PropTypes.string,                  // 可选字符串
  isActive: PropTypes.bool,                 // 可选布尔值
  
  // 联合类型
  role: PropTypes.oneOf(['admin', 'user', 'guest']),  // 枚举值
  
  // 数组和对象
  friends: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string.isRequired
    })
  ),
  
  // 对象
  settings: PropTypes.shape({
    theme: PropTypes.string,
    notifications: PropTypes.bool
  }),
  
  // 自定义验证
  customProp: function(props, propName, componentName) {
    if (!/^[0-9]+$/.test(props[propName])) {
      return new Error(`Invalid prop ${propName} supplied to ${componentName}`);
    }
    return null;
  }
};

// 默认 Props 值
UserProfile.defaultProps = {
  age: 18,
  isActive: false,
  role: 'guest',
  friends: [],
  settings: { theme: 'light', notifications: true }
};

// TypeScript 版本(推荐)
interface UserProfileProps {
  name: string;
  age?: number;
  email: string;
  isActive?: boolean;
  role: 'admin' | 'user' | 'guest';
  friends: Array<{ id: number; name: string }>;
  settings?: {
    theme: string;
    notifications: boolean;
  };
}

const UserProfileTS: React.FC<UserProfileProps> = ({ 
  name, 
  age = 18, 
  email, 
  isActive = false,
  role,
  friends,
  settings = { theme: 'light', notifications: true }
}) => {
  return <div>...</div>;
};

3.3 Children属性

/**
 * children 属性详解
 */

// 1. 基础 children
function Container({ children }) {
  return <div className="container">{children}</div>;
}

// 2. 多个 children
function SplitPane({ left, right }) {
  return (
    <div className="split-pane">
      <div className="left-pane">{left}</div>
      <div className="right-pane">{right}</div>
    </div>
  );
}

// 3. 函数作为 children(Render Props)
function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [url]);
  
  return children({ data, loading, error });
}

// 使用 Render Props
<DataFetcher url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误: {error.message}</div>;
    return <UserList users={data} />;
  }}
</DataFetcher>

// 4. 验证 children 类型
function List({ children }) {
  // 确保只有一个子元素
  const child = React.Children.only(children);
  
  // 遍历 children
  const items = React.Children.map(children, (child, index) => {
    return React.cloneElement(child, { key: index });
  });
  
  // 计数 children
  const count = React.Children.count(children);
  
  // 转换为数组
  const array = React.Children.toArray(children);
  
  return <ul>{items}</ul>;
}

// 5. children 类型检查
List.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]).isRequired
};

四、组件组合模式

4.1 包含关系

/**
 * 组件组合模式
 */

// 1. 卡片组件(包含关系)
function Card({ title, children, actions }) {
  return (
    <div className="card">
      {title && <div className="card-header">{title}</div>}
      <div className="card-body">{children}</div>
      {actions && <div className="card-actions">{actions}</div>}
    </div>
  );
}

// 使用
<Card
  title={<h2>用户资料</h2>}
  actions={
    <>
      <button>编辑</button>
      <button>删除</button>
    </>
  }
>
  <p>姓名: 张三</p>
  <p>邮箱: zhangsan@example.com</p>
</Card>

// 2. 模态框组件
function Modal({ isOpen, onClose, title, children, footer }) {
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{title}</h3>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-footer">{footer}</div>}
      </div>
    </div>
  );
}

// 3. 选项卡组件
function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  
  const titles = React.Children.map(children, (child, index) => (
    <button
      key={index}
      className={`tab-title ${activeIndex === index ? 'active' : ''}`}
      onClick={() => setActiveIndex(index)}
    >
      {child.props.title}
    </button>
  ));
  
  const activeContent = React.Children.toArray(children)[activeIndex];
  
  return (
    <div className="tabs">
      <div className="tab-titles">{titles}</div>
      <div className="tab-content">{activeContent}</div>
    </div>
  );
}

function TabPane({ title, children }) {
  return <div className="tab-pane">{children}</div>;
}

// 使用
<Tabs>
  <TabPane title="标签1">
    <p>标签1的内容</p>
  </TabPane>
  <TabPane title="标签2">
    <p>标签2的内容</p>
  </TabPane>
  <TabPane title="标签3">
    <p>标签3的内容</p>
  </TabPane>
</Tabs>

4.2 特化关系

/**
 * 特化关系(特定配置的组件)
 */

// 1. 基础按钮组件
function Button({ variant, size, children, ...props }) {
  const className = `btn btn-${variant} btn-${size}`;
  return (
    <button className={className} {...props}>
      {children}
    </button>
  );
}

// 2. 特化组件
function PrimaryButton(props) {
  return <Button variant="primary" {...props} />;
}

function DangerButton(props) {
  return <Button variant="danger" {...props} />;
}

function LargeButton(props) {
  return <Button size="large" {...props} />;
}

function SmallButton(props) {
  return <Button size="small" {...props} />;
}

// 3. 图标按钮
function IconButton({ icon, children, ...props }) {
  return (
    <Button {...props}>
      <span className="icon">{icon}</span>
      {children}
    </Button>
  );
}

// 4. 确认按钮(带确认对话框)
function ConfirmButton({ onConfirm, message, children, ...props }) {
  const handleClick = () => {
    if (window.confirm(message || '确定要执行此操作吗?')) {
      onConfirm();
    }
  };
  
  return (
    <Button onClick={handleClick} {...props}>
      {children}
    </Button>
  );
}

五、高阶组件(HOC)

5.1 HOC基础

/**
 * 高阶组件(Higher-Order Component)
 * 是一个函数,接收组件作为参数,返回新组件
 */

// 1. 基础 HOC - 添加日志
function withLogging(WrappedComponent) {
  return function WithLogging(props) {
    useEffect(() => {
      console.log(`组件 ${WrappedComponent.name} 已挂载`);
      return () => console.log(`组件 ${WrappedComponent.name} 将卸载`);
    }, []);
    
    useEffect(() => {
      console.log(`组件 ${WrappedComponent.name} 已更新`, props);
    });
    
    return <WrappedComponent {...props} />;
  };
}

// 2. 条件渲染 HOC
function withAuthentication(WrappedComponent) {
  return function WithAuthentication(props) {
    const { isLoggedIn } = useAuth();
    
    if (!isLoggedIn) {
      return <LoginPrompt />;
    }
    
    return <WrappedComponent {...props} />;
  };
}

// 3. 数据获取 HOC
function withDataFetching(WrappedComponent, fetchUrl) {
  return function WithDataFetching(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
      fetch(fetchUrl)
        .then(res => res.json())
        .then(data => {
          setData(data);
          setLoading(false);
        })
        .catch(err => {
          setError(err);
          setLoading(false);
        });
    }, []);
    
    return (
      <WrappedComponent
        {...props}
        data={data}
        loading={loading}
        error={error}
      />
    );
  };
}

// 4. 样式注入 HOC
function withStyles(WrappedComponent, styles) {
  return function WithStyles(props) {
    return (
      <div style={styles}>
        <WrappedComponent {...props} />
      </div>
    );
  };
}

// 5. 组合多个 HOC
function withFeatures(WrappedComponent) {
  return compose(
    withLogging,
    withAuthentication,
    withDataFetching('/api/data')
  )(WrappedComponent);
}

// 使用 HOC
const EnhancedComponent = withLogging(MyComponent);
const ProtectedComponent = withAuthentication(Dashboard);
const DataComponent = withDataFetching(UserList, '/api/users');
const StyledComponent = withStyles(Button, { color: 'red' });

5.2 HOC注意事项

/**
 * HOC 注意事项和最佳实践
 */

// ✅ 正确:传递不相关的 props
function withSubscription(WrappedComponent, selectData) {
  return function WithSubscription(props) {
    const { forwardedRef, ...rest } = props;
    const data = selectData(DataSource, props);
    
    return (
      <WrappedComponent
        {...rest}
        data={data}
        ref={forwardedRef}
      />
    );
  };
}

// ✅ 正确:转发 ref
function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }
    
    render() {
      const { forwardedRef, ...rest } = this.props;
      return <WrappedComponent ref={forwardedRef} {...rest} />;
    }
  }
  
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

// ✅ 正确:保留显示名称
function withSubscription(WrappedComponent) {
  function WithSubscription(props) {
    // ...
  }
  
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

// ❌ 错误:在 render 方法中创建 HOC
function App() {
  // 每次渲染都会创建新组件,导致性能问题
  const EnhancedComponent = withLogging(MyComponent);
  return <EnhancedComponent />;
}

// ✅ 正确:在组件外部创建
const EnhancedComponent = withLogging(MyComponent);

function App() {
  return <EnhancedComponent />;
}

六、Render Props模式

6.1 Render Props基础

/**
 * Render Props - 使用函数作为 children 或 render 属性
 */

// 1. 使用 children 作为函数
class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };
  
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  };
  
  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.children(this.state)}
      </div>
    );
  }
}

// 使用
<MouseTracker>
  {({ x, y }) => (
    <p>鼠标位置: ({x}, {y})</p>
  )}
</MouseTracker>

// 2. 使用 render 属性
class DataProvider extends React.Component {
  state = { data: null, loading: true, error: null };
  
  componentDidMount() {
    this.fetchData();
  }
  
  fetchData = async () => {
    try {
      const response = await fetch(this.props.url);
      const data = await response.json();
      this.setState({ data, loading: false });
    } catch (error) {
      this.setState({ error, loading: false });
    }
  };
  
  render() {
    return this.props.render(this.state);
  }
}

// 使用
<DataProvider
  url="/api/users"
  render={({ data, loading, error }) => {
    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误: {error.message}</div>;
    return <UserList users={data} />;
  }}
/>

// 3. 组合多个 Render Props
function withMouse(Component) {
  return function(props) {
    return (
      <MouseTracker>
        {mouse => <Component {...props} mouse={mouse} />}
      </MouseTracker>
    );
  };
}

// 4. Render Props vs HOC
// 两者可以互相转换,选择取决于使用场景

七、组件通信

7.1 父子组件通信

/**
 * 父子组件通信模式
 */

// 1. 父 → 子:通过 props
function Parent() {
  const [message, setMessage] = useState('来自父组件的消息');
  
  return <Child message={message} />;
}

function Child({ message }) {
  return <div>{message}</div>;
}

// 2. 子 → 父:通过回调函数
function ParentWithCallback() {
  const [childData, setChildData] = useState(null);
  
  const handleChildData = (data) => {
    setChildData(data);
  };
  
  return (
    <div>
      <ChildWithCallback onSendData={handleChildData} />
      <p>子组件发送的数据: {childData}</p>
    </div>
  );
}

function ChildWithCallback({ onSendData }) {
  const sendData = () => {
    onSendData('这是子组件的数据');
  };
  
  return <button onClick={sendData}>发送数据给父组件</button>;
}

// 3. 父 → 子:通过 ref 调用子组件方法
const ChildWithRef = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    childMethod: () => {
      console.log('子组件方法被调用');
      return '子组件返回值';
    }
  }));
  
  return <div>子组件内容</div>;
});

function ParentWithRef() {
  const childRef = useRef();
  
  const callChildMethod = () => {
    const result = childRef.current.childMethod();
    console.log(result);
  };
  
  return (
    <div>
      <ChildWithRef ref={childRef} />
      <button onClick={callChildMethod}>调用子组件方法</button>
    </div>
  );
}

7.2 兄弟组件通信

/**
 * 兄弟组件通信(通过父组件)
 */

function SiblingsCommunication() {
  const [sharedData, setSharedData] = useState('');
  
  const handleDataChange = (data) => {
    setSharedData(data);
  };
  
  return (
    <div>
      <SiblingA onDataChange={handleDataChange} />
      <SiblingB data={sharedData} />
    </div>
  );
}

function SiblingA({ onDataChange }) {
  const sendData = () => {
    onDataChange('来自兄弟A的数据');
  };
  
  return <button onClick={sendData}>发送数据给兄弟B</button>;
}

function SiblingB({ data }) {
  return <div>收到数据: {data}</div>;
}

八、总结

8.1 知识点回顾

知识点 说明 重要程度
函数组件 现代React推荐方式 ⭐⭐⭐⭐⭐
Props传递 父子组件通信基础 ⭐⭐⭐⭐⭐
PropTypes 运行时类型检查 ⭐⭐⭐⭐
Children 组件组合模式 ⭐⭐⭐⭐
HOC 横切关注点复用 ⭐⭐⭐
Render Props 共享逻辑模式 ⭐⭐⭐

8.2 练习任务

// 练习1:创建一个可复用的表单字段组件
// 要求:支持标签、错误提示、验证

function FormField({ label, name, type, validation, ...props }) {
  // 实现代码
  return <div>...</div>;
}

// 练习2:创建一个数据表格组件
// 要求:支持列配置、排序、分页

function DataTable({ columns, data, pageSize }) {
  // 实现代码
  return 表...;
}

// 练习3:创建一个可拖拽的模态框组件
// 要求:支持拖拽移动、自定义大小

function DraggableModal({ title, children, onClose }) {
  // 实现代码
  return <div>...</div>;
}

8.3 下一节预告

下一篇将学习 State基础与useState,内容包括:

  • useState基础用法
  • 状态更新机制
  • 对象和数组状态
  • 状态提升
  • 状态管理最佳实践

更多推荐