React 组件通信、Context、懒加载与 Portal
本文目录
本文保留旧笔记的归档日期,主体按现代 React 整理。原文混在一起讨论了组件通信、Context、PropTypes、懒加载和 Portal;它们解决的是不同问题,先分清边界会更容易选对工具。
组件之间如何传数据
Props:父组件向下传递
Props 是父组件给子组件的输入。子组件不应直接修改 props;需要把变化反馈给父组件时,由父组件传入事件回调:
import { useState } from "react";
function Profile({ name, onRename }) {
return (
<button onClick={() => onRename("新名字")}>
当前用户:{name}
</button>
);
}
function Page() {
const [name, setName] = useState("Haoge");
return <Profile name={name} onRename={setName} />;
}
只用于包裹展示的组件可以用 children 做组合,避免组件必须认识每一种子内容。状态应尽量放在真正拥有它、且需要协调这些视图的最近共同父组件中。
Context:跨层级共享低频依赖
Context 适合主题、当前用户、语言等需要由远处组件读取的值,不是所有状态的默认容器。创建 Context 应放在组件外部;React 19 可直接用 Context 对象作为 Provider,较旧版本使用 Context.Provider:
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function ThemeButton() {
const theme = useContext(ThemeContext);
return <button className={"button-" + theme}>保存</button>;
}
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext value={theme}>
<ThemeButton />
</ThemeContext>
);
}
Context 的默认值只是树上没有匹配 Provider 时的静态兜底,不会随时间变化。Provider 的 value 改变时,读取该 Context 的组件会更新;把高频变化的大对象塞进一个全局 Context,可能让很多消费者一起重渲染。可以拆分 Context,或使用更适合的状态/store 方案。
Props 类型与运行时数据校验
React 19 移除了 React 对函数组件 propTypes 和 defaultProps 的运行时检查;旧写法可能不再按预期提示。新项目优先用 TypeScript 描述组件输入,默认值用 JavaScript 参数默认值。来自 URL、网络、文件或用户输入的数据仍需要运行时校验,因为静态类型不会在运行时替你验证外部数据。
PropTypes 不是身份、权限或业务数据校验。维护旧项目时,先确认其 React 版本和 prop-types 依赖,再决定兼容方案;不要为了组件类型提示把 PropTypes、TypeScript 和服务端校验混为一谈。
lazy 与 Suspense:按需加载组件代码
lazy 让组件代码在第一次需要渲染时再通过动态 import 加载。lazy 声明放在模块顶层,加载中的界面由最近的 Suspense 边界提供;加载 Promise 拒绝时,应由错误边界给出可恢复的失败状态:
import { lazy, Suspense } from "react";
const Reports = lazy(() => import("./Reports.js"));
function ReportsPage() {
return (
<Suspense fallback={<p role="status">正在加载报表…</p>}>
<Reports />
</Suspense>
);
}
默认写法要求被导入模块有 default export。若模块使用命名导出,可显式映射。加载 Promise 拒绝时,需要由 Suspense 外层最近的错误边界处理;错误边界应使用项目已有实现或专门的边界组件:
const Reports = lazy(() =>
import("./Reports.js").then(module => ({ default: module.Reports })),
);
Suspense 的 fallback 只负责被 Suspense 感知到的挂起操作;它不会自动把普通 useEffect 里的任意 Fetch 请求变成 Suspense 数据源。懒加载组件不要在父组件 render 内重新声明,否则组件身份可能变化并导致状态重置。
Portal:改变 DOM 落点,不改变 React 父子关系
createPortal 把一段 React 子树渲染到另一个已存在的 DOM 节点,常用于弹窗、提示层或受 overflow 裁切的浮层:
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
function Modal({ children }) {
const [modalRoot, setModalRoot] = useState(null);
useEffect(() => {
setModalRoot(document.getElementById("modal-root"));
}, []);
if (!modalRoot) return null;
return createPortal(
<div className="modal-layer">{children}</div>,
modalRoot,
);
}
Portal 只改变 DOM 中的物理位置。Context 仍沿 React 组件树传递,事件也按 React 树冒泡,而不是按 DOM 树冒泡;这可能让祖先的点击处理器收到 Portal 内事件。必要时在弹窗内部停止传播。
Portal 本身不会自动实现完整的对话框无障碍行为。真正的模态框还要处理可访问名称、焦点进入与恢复、Tab 焦点约束、背景内容不可交互、Escape 关闭策略和移动端滚动。
选型速查
| 问题 | 优先考虑 |
|---|---|
| 一个直接子组件需要父级数据 | Props |
| 多层组件共享稳定配置 | Context |
| 多个相关状态需要明确动作与转移 | useReducer 或状态管理方案 |
| 首屏不必加载的重组件 | lazy + Suspense + 错误边界 |
| 弹窗需要逃离父容器的裁切/层叠上下文 | createPortal,并补全对话框行为 |
| 组件输入需要编译期类型检查 | TypeScript |
| 读取接口、文件或用户输入等不可信值 | 服务端及运行时 schema 校验 |
官方资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。