组件设计模式与通信

一、组件通信方式

1. 父 → 子:Props

jsx 复制代码
function Parent() {
  return <Child name="小明" age={18} />;
}

function Child({ name, age }) {
  return <p>{name},{age}岁</p>;
}

2. 子 → 父:回调函数

jsx 复制代码
function Parent() {
  const handleMsg = (msg) => console.log('收到:', msg);
  return <Child onSend={handleMsg} />;
}

function Child({ onSend }) {
  return <button onClick={() => onSend('你好')}>发送</button>;
}

3. 兄弟组件:状态提升

jsx 复制代码
// 把共享状态提升到共同的父组件
function Parent() {
  const [value, setValue] = useState('');
  return (
    <>
      <InputBox onChange={setValue} />
      <Display text={value} />
    </>
  );
}

4. 跨层级:Context

jsx 复制代码
const ThemeCtx = createContext('light');

function App() {
  return (
    <ThemeCtx.Provider value="dark">
      <Page />  {/* 中间隔了很多层 */}
    </ThemeCtx.Provider>
  );
}

function DeepChild() {
  const theme = useContext(ThemeCtx);  // 直接拿到,不用层层传递
  return <div className={theme}>内容</div>;
}

5. 全局状态:Redux / Zustand

适合大型应用,多个不相关组件需要共享状态时使用(详见 06.md)。

通信方式速查

场景 推荐方式
父 → 子 Props
子 → 父 回调函数
兄弟 状态提升
跨多层 Context
全局复杂状态 Redux / Zustand

二、高阶组件(HOC)

一句话:接收一个组件,返回一个增强后的新组件。本质是函数,不是组件。

jsx 复制代码
// HOC:给任意组件添加"加载中"功能
function withLoading(WrappedComponent) {
  return function EnhancedComponent({ isLoading, ...props }) {
    if (isLoading) return <div>加载中...</div>;
    return <WrappedComponent {...props} />;
  };
}

// 使用
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading isLoading={true} users={[]} />

常见 HOC

  • React.memo() --- 性能优化
  • connect() --- Redux 连接(旧写法)
  • withRouter() --- React Router(旧写法)

HOC 的问题

  • 嵌套多了形成"包装地狱"
  • props 来源不清晰
  • 现在基本被 Hooks 替代了

三、Render Props

一句话:通过 props 传一个渲染函数,让组件把数据"交出来",由外部决定怎么渲染。

jsx 复制代码
// 鼠标位置追踪组件
function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });

  const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });

  return <div onMouseMove={handleMove}>{render(pos)}</div>;
}

// 使用:同一个逻辑,不同的渲染方式
<MouseTracker render={({ x, y }) => <p>鼠标在 ({x}, {y})</p>} />
<MouseTracker render={({ x, y }) => <div style={{ left: x, top: y }} className="cursor" />} />

现在也基本被自定义 Hook 替代了(useMousePosition)。

四、组合 vs 继承

React 推荐组合,不推荐继承。

jsx 复制代码
// ✅ 组合:通过 children 或 props 组合
function Card({ title, children }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

<Card title="用户信息">
  <p>姓名:小明</p>
  <p>年龄:18</p>
</Card>

// ✅ 插槽模式:多个内容区域
function Layout({ header, sidebar, content }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{content}</main>
    </div>
  );
}

<Layout
  header={<NavBar />}
  sidebar={<Menu />}
  content={<Page />}
/>

五、错误边界(Error Boundary)

一句话:捕获子组件树的 JS 错误,显示降级 UI,防止整个页面白屏。

jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };  // 触发降级 UI
  }

  componentDidCatch(error, info) {
    console.error('组件错误:', error, info);  // 上报错误
  }

  render() {
    if (this.state.hasError) {
      return <h2>出错了,请刷新页面</h2>;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary>
  <App />  {/* App 内部报错不会白屏,而是显示降级 UI */}
</ErrorBoundary>

注意:错误边界捕获不了这些错误:

  • 事件处理函数中的错误(用 try/catch)
  • 异步代码(setTimeout、Promise)
  • 服务端渲染
  • 错误边界自身的错误

目前只能用类组件写错误边界,函数组件暂不支持。可以用 react-error-boundary 库简化。

六、Portals

一句话:把组件渲染到 DOM 树的其他位置,而不是父组件内部。

jsx 复制代码
import { createPortal } from 'react-dom';

function Modal({ children, onClose }) {
  // 虽然 Modal 在组件树中是 App 的子组件
  // 但实际 DOM 渲染到了 document.body 下
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}

// 使用
function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <button onClick={() => setShow(true)}>打开弹窗</button>
      {show && <Modal onClose={() => setShow(false)}>弹窗内容</Modal>}
    </div>
  );
}

典型场景:弹窗(Modal)、提示框(Tooltip)、全局通知(Toast)。

Portal 虽然 DOM 位置变了,但 React 事件冒泡仍然按组件树走,不按 DOM 树走。

七、高频面试题

Q1:React 组件通信有哪些方式?

父→子用 Props,子→父用回调函数,兄弟用状态提升,跨层级用 Context,全局状态用 Redux/Zustand。

Q2:HOC 和自定义 Hook 怎么选?

优先用自定义 Hook。HOC 是类组件时代的方案,存在嵌套地狱和 props 来源不清的问题。Hook 更直观、更灵活、更容易组合。

Q3:为什么 React 推荐组合不推荐继承?

  • 组合更灵活:通过 props 和 children 可以任意组合
  • 继承耦合太强:子类依赖父类实现,改父类可能影响所有子类
  • React 官方明确说:在 Facebook 数千个组件中,没有找到需要继承的场景

Q4:错误边界能捕获所有错误吗?

不能。只能捕获渲染过程、生命周期、构造函数中的错误。事件处理、异步代码、服务端渲染中的错误捕获不了,需要用 try/catch 或 window.onerror。

Q5:Portal 的事件冒泡怎么走?

组件树冒泡,不按 DOM 树。即使 Modal 渲染到了 body 下,点击 Modal 内部的事件仍然会冒泡到 React 组件树中的父组件。

复制代码
相关推荐
im_AMBER2 小时前
前端性能优化之首屏提速
前端·学习·性能优化
lxh01132 小时前
计算右侧小于当前元素的个数 题解
javascript·数据结构·算法
天天向上10242 小时前
vue 大屏适配的一种实现思路
前端·javascript·vue.js
SuperEugene2 小时前
Vue/Vite 多环境配置实战:dev、test、prod 差异区分与避坑指南|Vue 工程化篇
前端·javascript·vue.js
结网的兔子2 小时前
前端学习笔记(实战准备篇)——用vite构建一个项目【吐血整理】
前端·学习·elementui·npm·node.js·vue
kyriewen2 小时前
盒模型:CSS 世界的物理法则,margin 塌陷与 padding 的恩怨情仇
前端·css·html
lichenyang4532 小时前
React 性能优化组件设计模式与通信
前端·javascript·设计模式
小成C2 小时前
别再把 Claude Code 用乱了:CLAUDE.md、Rules、Skills、Hooks 到底怎么分工?
前端·人工智能·面试
Kel2 小时前
这就是编程:Pi Monorepo 源码深度--解析一个工业级 AI Agent 框架的设计哲学
人工智能·设计模式·架构