React组件通信方式

1. Props 传递

父组件向子组件传递数据

通过 props 将数据从父组件传递给子组件。介绍 React + TypeScript 中 Props 的类型定义

基础类型定义

ts 复制代码
interface Props {
  string: string;
  number: number;
  boolean: boolean;
  array: string[];
  numberArray: number[];
  object: { id: number; name: string };
  func: (name: string) => void;
  // 任意类型
  any: any;
  // 未知类型(比 any 更安全)
  unknown: unknown;
}

function BasicProps(props: Props) {
  return <div>{props.string}</div>;
}

可选属性(使用 ? 符号)

ts 复制代码
interface Props {
  required: string;
  optional?: string;  // 可选属性
  withDefault?: string;  // 带默认值的可选属性
}
// defaultProps 是一个对象,类型为 Partial<Props>,表示它只包含 Props 接口中的部分属性,Partial<Props> 是 TypeScript 的工具类型,表示 defaultProps 中的属性都是可选的
const defaultProps: Partial<Props> = {
    withDefault: '默认',
};

// 方式1:使用默认参数
function OptionalProps({ required, optional = "default" }: Props) {
  return <div>{optional}</div>;
}

// 方式2:使用 defaultProps
function OptionalProps(props: Props) {
	const { withDefault } = { ...defaultProps, ...props }
  return <div>{withDefault}</div>;
}

联合类型

ts 复制代码
interface Props {
  // 字符串字面量联合类型
  size: "small" | "medium" | "large";
  // 多类型联合
  id: string | number;
  // 可以是 null 或 undefined
  nullable: string | null;
  optional?: string | undefined;
}

泛型组件

tsx 复制代码
interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function GenericList<T>({ items, renderItem }: Props<T>) {
  return (
    <div>
      {items.map((item, index) => renderItem(item))}
    </div>
  );
}

// 使用示例
<GenericList 
  items={[1, 2, 3]} 
  renderItem={(item) => <div key={item}>{item}</div>}
/>

React 事件类型

ts 复制代码
interface Props {
  // 点击事件
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
  // 输入事件
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  // 表单提交
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
  // 键盘事件
  onKeyPress: (event: React.KeyboardEvent<HTMLInputElement>) => void;
}

子组件类型

ts 复制代码
interface Props {
  // 普通 children
  children: React.ReactNode;
  // 只接受特定元素类型
  child: React.ReactElement;
  // 字符串类型
  text: string;
  // 多个子元素
  elements: React.ReactElement[];
}

Enum 类型

ts 复制代码
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE'
}

interface Props {
  status: Status;
}

function StatusComponent({ status }: Props) {
  return <div>{status}</div>;
}

// 使用
<StatusComponent status={Status.Active} />

复杂对象类型

ts 复制代码
interface User {
  id: number;
  name: string;
  email?: string;
}

interface Props {
  user: User;
  users: User[];
  updateUser: (user: User) => void;
}

Record 类型

ts 复制代码
interface Props {
  // 键为字符串,值为数字的对象
  scores: Record<string, number>;
  // 键为特定字符串,值为任意类型的对象
  config: Record<'api' | 'timeout', any>;
}

使用建议:

  • 类型声明最佳实践
  • 使用 interface 而不是 type 来定义 props(除非需要使用联合类型或交叉类型)
  • 将 props 接口命名为 组件名Props
  • 尽可能使用具体类型,避免 any
jsx 复制代码
// Parent.jsx
function Parent() {
  return <Child message="Hello from parent" />;
}

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

子组件接受父组件传递的props

props是一个对象,会作为函数的第一个参数接受传过来的props值

注意:我们需要遵守单向数据流,子组件不能直接修改父组件的props

在React源码中会使用Object.freeze冻结props,限制props的修改。

Object.freeze() 静态方法可以使一个对象被冻结。冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定

子组件向父组件传递数据

通过传递回调函数给子组件,子组件调用该回调函数将数据传回父组件。

jsx 复制代码
// Parent.jsx
function Parent() {
  const handleChildData = (data) => {
    console.log('Data from child:', data);
  };

  return <Child onDataSend={handleChildData} />;
}

// Child.jsx
function Child(props) {
  return (
    <button onClick={() => props.onDataSend('Hello from child')}>
      Send to parent
    </button>
  );
}

2.Context(跨层级组件通信)

跨层级组件通信 :使用 React.createContext 创建上下文,通过 Provider 提供数据,子组件通过 ConsumeruseContext 钩子获取数据

首先创建一个主题切换的 Context组件ThemeContext:

tsx 复制代码
import React, { createContext, useState, useContext } from 'react';

// 定义主题类型
type Theme = 'light' | 'dark';

// 定义 Context 的类型
interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

// 创建 Context
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// 创建 Provider 组件
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');

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

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 创建自定义 Hook 来使用 Context
export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

然后在应用的根组件中使用 Provider:

tsx 复制代码
import { ThemeProvider } from './context/ThemeContext';
import Navbar from './components/Navbar';
import Content from './components/Content';

function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <Navbar />
        <Content />
      </div>
    </ThemeProvider>
  );
}

export default App;

创建导航栏组件,使用 Context:

tsx 复制代码
import { useTheme } from '../context/ThemeContext';

function Navbar() {
  const { theme, toggleTheme } = useTheme();

  return (
    <nav className={`navbar ${theme}`}>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>
        切换主题 (当前: {theme})
      </button>
    </nav>
  );
}

export default Navbar;

创建内容组件,同样使用 Context:

tsx 复制代码
import { useTheme } from '../context/ThemeContext';

function Content() {
  const { theme } = useTheme();

  return (
    <main className={`content ${theme}`}>
      <h2>内容区域</h2>
      <p>当前主题: {theme}</p>
    </main>
  );
}

export default Content;

3.状态管理库

全局状态管理 :使用状态管理库如 Redux、MobX、zustand 或 Recoil 来管理全局状态,组件通过订阅状态或派发动作来通信。

4.事件总线(Event Bus)

发布-订阅模式 :使用自定义事件总线或第三方库(如 mitt)来实现组件间的通信。

github.com/developit/m...

事件总线 (eventBus.ts)

ts 复制代码
// 定义事件类型
interface EventMap {
  'custom-event': { message: string };
  // 可以扩展其他事件
}

const eventBus = {
  // 订阅事件
  on<K extends keyof EventMap>(event: K, callback: (e: CustomEvent<EventMap[K]>) => void) {
    document.addEventListener(event, callback as EventListener);
  },

  // 触发事件
  emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
    document.dispatchEvent(new CustomEvent(event, { detail: data }));
  },

  // 取消订阅
  off<K extends keyof EventMap>(event: K, callback: (e: CustomEvent<EventMap[K]>) => void) {
    document.removeEventListener(event, callback as EventListener);
  },
};

export default eventBus;
  1. 类型安全

    • 使用泛型 K extends keyof EventMap 确保事件名称和数据类型匹配。
    • EventMap 定义了事件名称及其对应的数据类型。
  2. 可扩展性

    • 可以在 EventMap 中添加更多事件类型。
  3. 导出事件总线

    • 使用 export default 导出 eventBus,方便其他模块使用

组件 A (ComponentA.tsx)

tsx 复制代码
import React from 'react';
import eventBus from './eventBus';

const ComponentA: React.FC = () => {
  const sendData = () => {
    eventBus.emit('custom-event', { message: 'Hello from ComponentA' });
  };

  return <button onClick={sendData}>发送数据</button>;
};

export default ComponentA;
  1. 类型推断

    • 使用 React.FC 定义函数组件,自动推断 props 类型。
  2. 事件触发

    • 调用 eventBus.emit 时,TypeScript 会检查事件名称和数据类型的正确性。

组件 B (ComponentB.tsx)

tsx 复制代码
import React, { useEffect } from 'react';
import eventBus from './eventBus';

const ComponentB: React.FC = () => {
  useEffect(() => {
    const handler = (e: CustomEvent<{ message: string }>) => {
      console.log('Received data:', e.detail);
    };

    // 订阅事件
    eventBus.on('custom-event', handler);

    // 清理函数:取消订阅
    return () => {
      eventBus.off('custom-event', handler);
    };
  }, []);

  return <div>ComponentB is listening...</div>;
};

export default ComponentB;
  1. 类型安全

    • handler 函数的参数 e 明确为 CustomEvent<{ message: string }>,确保数据类型正确。
  2. 清理函数

    • useEffect 的清理函数中取消订阅事件,避免内存泄漏。
  3. 清晰的日志

    • 打印日志时增加描述性内容,便于调试。

使用示例 (App.tsx)

tsx 复制代码
import React from 'react';
import ComponentA from './ComponentA';
import ComponentB from './ComponentB';

const App: React.FC = () => {
  return (
    <div>
      <h1>Event Bus Example</h1>
      <ComponentA />
      <ComponentB />
    </div>
  );
};

export default App;
  1. 组件组合App 组件中同时使用 ComponentAComponentB,展示事件总线的通信效果。

  2. 用户点击 ComponentA 中的按钮。

  3. ComponentA 触发 'custom-event' 事件,并传递数据 { message: 'Hello from ComponentA' }

  4. ComponentB 监听到 'custom-event' 事件,并打印数据:

    复制

    css 复制代码
    Received data: { message: 'Hello from ComponentA' }
  5. ComponentB 卸载时,useEffect 的清理函数会取消订阅事件。

CustomEventEvent 的区别

CustomEventEvent 都是用于创建和触发事件的浏览器原生 API,但它们有一些关键区别。以下是它们的详细对比:React组件通信方式

特性 Event CustomEvent
数据传递 不支持 支持,通过 detail 属性
用途 创建普通事件(如 click 创建自定义事件,携带额外数据
兼容性 所有现代浏览器和 IE9+ 现代浏览器广泛支持,IE11 需要 polyfill
构造函数参数 type, optionsbubbles, cancelable type, optionsdetail, bubbles, cancelable
相关推荐
sen_shan2 分钟前
Vue3+Vite+TypeScript+Element Plus开发-02.Element Plus安装与配置
前端·javascript·typescript·vue3·element·element plus
疾风铸境14 分钟前
Qt5.14.2+mingw64编译OpenCV3.4.14一次成功记录
前端·webpack·node.js
晓风伴月18 分钟前
Css:overflow: hidden截断条件‌及如何避免截断
前端·css·overflow截断条件
最新资讯动态21 分钟前
使用“一次开发,多端部署”,实现Pura X阔折叠的全新设计
前端
爱泡脚的鸡腿36 分钟前
HTML CSS 第二次笔记
前端·css
灯火不休ᝰ1 小时前
前端处理pdf文件流,展示pdf
前端·pdf
智践行1 小时前
Trae开发实战之转盘小程序
前端·trae
最新资讯动态1 小时前
DialogHub上线OpenHarmony开源社区,高效开发鸿蒙应用弹窗
前端
lvbb661 小时前
框架修改思路
前端·javascript·vue.js
树上有只程序猿1 小时前
Java程序员需要掌握的技术
前端