如何设计更优雅的 React 组件?
liuian 2024-12-04 13:47 20 浏览
大家好,我是 Echa。
在日常开发中,团队中每个人组织代码的方式不尽相同。下面我们就从代码结构的角度来看看如何组织一个更加优雅的 React 组件!
1. 导入依赖项
我们通常会在组件文件顶部导入组件所需的依赖项。对于不同类别的依赖项,建议对它们进行分组,这有助于帮助我们更好地理解组件。可以将导入的依赖分为四类:
// 外部依赖
import React from "react";
import { useRouter } from "next/router";
// 内部依赖
import { Button } from "../src/components/button";
// 本地依赖
import { Tag } from "./tag";
import { Subscribe } from "./subscribe";
// 样式
import styles from "./article.module.scss";
- 外部依赖: 外部依赖主要是第三方依赖,这些依赖定义在package.json文件中并从node_modules 中导入;
- 内部依赖: 内部依赖主要是位于组件文件夹之外的可重用的组件或模块,这些导入都应该使用相对导入语法,以 ../ 开头。通常,大部分导入的依赖项都属于这一类。因此,如果需要的话,我们可以将这一类组件进一步分离,例如:UI组件、数据相关的导入、services等;
- 本地依赖: 本地依赖主要是与组件位于同一文件夹中的依赖项或子组件。这些依赖项的所有导入路径应以./开头。主要是比较大的组件会包含本地依赖项;
- 样式: 最后这一部分大多数情况下只包含一个导入——样式文件。如果导入了多个样式表,就需要考虑样式的拆分是否有问题。
对导入依赖项进行手动分组可能比较麻烦,Prettier 可以帮助我们自动格式化代码。可以使用 prettier-plugin-sort-imports 插件来自动格式化依赖项导入。需要在项目根目录创建prettier.config.js配置文件,并在里面配置规则:
module.exports = {
// 其他 Prettier 配置
importOrder: [
// 默认情况下,首先会放置外部依赖项
// 内部依赖
"^../(.*)",
// 本地依赖项,样式除外
"^./((?!scss).)*#34;,
// 其他
"^./(.*)",
],
importOrderSeparation: true,
};
下面是该插件官方给出的例子,输入如下:
import React, {
FC,
useEffect,
useRef,
ChangeEvent,
KeyboardEvent,
} from 'react';
import { logger } from '@core/logger';
import { reduce, debounce } from 'lodash';
import { Message } from '../Message';
import { createServer } from '@server/node';
import { Alert } from '@ui/Alert';
import { repeat, filter, add } from '../utils';
import { initializeApp } from '@core/app';
import { Popup } from '@ui/Popup';
import { createConnection } from '@server/database';
格式化之后的输出如下:
import { debounce, reduce } from 'lodash';
import React, {
ChangeEvent,
FC,
KeyboardEvent,
useEffect,
useRef,
} from 'react';
import { createConnection } from '@server/database';
import { createServer } from '@server/node';
import { initializeApp } from '@core/app';
import { logger } from '@core/logger';
import { Alert } from '@ui/Alert';
import { Popup } from '@ui/Popup';
import { Message } from '../Message';
import { add, filter, repeat } from '../utils';
prettier-plugin-sort-imports:https://github.com/trivago/prettier-plugin-sort-imports
2. 静态定义
在导入依赖项的下方,通常会放那些使用 TypeScript 或 Flow 等静态类型检查器定义的文件级常量和类型定义。
(1)常量定义
组件中的所有 magic 值,例如字符串或者数字,都应该放在文件的顶部,导入依赖项的下方。由于这些都是静态常量,这意味着它们的值不会改变。因此将它们放在组件中是没有意义的,因为放在组件中的话,它们会在每次重新渲染组件时重新创建。
const MAX_READING_TIME = 10;
const META_TITLE = "Hello World";
对于更复杂的静态数据结构,可以将其提取到一个单独的文件中,以保持组件代码整洁。
(2)类型定义
下面是使用 TypeScript 声明的组件 props 的类型:
interface Props {
id: number;
name: string;
title: string;
meta: Metadata;
}
如果这个 props 的类型不需要导出,可以使用 Props 作为接口名称,这样可以帮助我们立即识别组件 props 的类型定义,并将其与其他类型区分开。
只有当这个 Props 类型需要在多个组件中使用时,才需要添加组件名称,例如ButtonProps,因为它在导入另一个组件时,不应该与另一个组件的Props类型冲突。
3. 组件定义
定义函数组件的方式有两种:函数声明和箭头函数, 推荐使用函数声明的形式,因为这就是语法声明的内容:函数。官方文档的示例中也使用了这种方法:
function Article(props: Props) {
/**/
}
只会在必须使用 forwardRef 时才使用箭头函数:
const Article = React.forwardRef<HTMLArticleElement, Props>(
(props, ref) => {
/**/
}
);
通常会在组件最后默认导出组件:
export default Article;
4. 变量声明
接下来,我们就需要在组件里面进行变量的声明。注意,即使使用 const 声明,这里也称为变量,因为它们的值通常会在不同的渲染之间发生变化,只有在执行单个渲染过程时是恒定的。
const { id, name, title } = props;
const router = useRouter();
const initials = getInitials(name);
这里通常包含在组件级别使用的所有变量,使用 const 或 let 定义,具体取决于它们在渲染期间是否更改其值:
- 解构数据:通常来自 props、数据 stores 或组件的 state;
- Hooks:自定义hooks、框架内置 Hooks,例如 useState、useReducer、useRef、useCallback 或 useMemo;
- 在整个组件中使用的已处理数据,由函数计算得出;
一些较大的组件可能需要在组件中声明很多变量。这种情况下,建议根据它们的初始化方法或者用途对它们进行分组:
// 框架 hooks
const router = useRouter();
// 自定义 hooks
const user = useLoggedUser();
const theme = useTheme();
// 从 props 中解构的数据
const { id, title, meta, content, onSubscribe, tags } = props;
const { image, author, date } = meta;
// 组件状态
const [email, setEmail] = React.useState("");
const [showMenu, toggleMenu] = React.useState(false);
const [activeTag, dispatch] = React.useReducer(reducer, tags);
// 记忆数据
const subscribe = React.useCallback(onSubscribe, [id]);
const summary = React.useMemo(() => getSummary(content), [content]);
// refs
const sideMenuRef = useRef<HTMLDivElement>(null);
const subscribeRef = useRef<HTMLButtonElement>(null);
// 计算数据
const initials = getInitials(author);
const formattedDate = getDate(date);
变量分组的方法在不同组件之间可能会存在很大的差异,它取决于变量的数量和类型。关键是要将相关变量放在一起,在不同组之间添加一个空行来提高代码的可读性。
注:上面代码中的注释仅用于标注分组类型,在实际项目中不会写这些注释。
5. Effects
Effects 部分通常会写在变量声明之后,它们可能是React中最复杂的构造,但从语法的角度来看它们非常简单:
useEffect(() => {
setLogo(theme === "dark" ? "white" : "black");
}, [theme]);
任何包含在effect之内但是在其外部定义的变量,都应该包含在依赖项的数组中。
除此之外,还应该使用return来清理副作用:
useEffect(() => {
function onScroll() {
/*...*/
}
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
6. 渲染内容
组件的核心就是它的内容,React 组件的内容使用 JSX 语法定义并在浏览器中呈现为 HTML。所以,推荐将函数组件的 return 语句尽可能靠近文件的顶部。其他一切都只是细节,它们应该放在文件较下的位置。
function Article(props: Props) {
// 变量声明
// effects
// ? 自定义的函数不建议放在 return 部分的前面
function getInitials() {
/*...*/
}
return /* content */;
}
export default Article;
function Article(props: Props) {
// 变量声明
// effects
return /* content */;
// ? 自定义的函数建议放在 return 部分的后面
function getInitials() {
/*...*/
}
}
export default Article;
难道return不应该放在函数的最后吗?其实不然,对于常规函数,肯定是要将return放在最后的。然而,React组件并不是简单的函数,它们通常包含具有各种用途的嵌套函数,例如事件处理程序。最后的return语句以及前面的一堆其他函数,实际上阻碍了代码的阅读,使得很难找到组件渲染的内容:
- 很难搜索该return语句,因为可能有来自其他嵌套函数的多个 return 语句;
- 在文件末尾滚动查找 return 语句并不能很容易找到它,因为返回的 JSX 块可能非常大。
当然,可以根据个人喜好来决定函数定义的位置。如果将函数放在return的下方,那么如果想要使用箭头函数来自定义函数,那就只能使用var来定义,因为let和const不存在变量提升,不能在定义的箭头函数之前使用它。
7. 部分渲染
在处理大型 JSX 代码时,将某些内容块提取为单独的函数来渲染组件的一部分是很有帮助的,类似于将大型函数分解为多个较小的函数。
function Article(props: Props) {
// ...
return (
<article>
<h1>{props.title}</h1>
{renderBody()}
{renderFooter()}
</article>
);
function renderBody() {
return /* article body JSX */;
}
function renderFooter() {
return /* article footer JSX */;
}
}
export default Article;
- 可以给这些拆分出来的函数前面加上render前缀,以将它们与其他不返回 JSX 的函数区分开;
- 可以将这些函数放在return语句之后,以便将与内容相关的所有内容组合在一起;
- 无需向这些函数传递任何参数,因为它们可以访问props和组件定义的所有变量;
那为什么不将它们提取为组件呢?关于部分渲染函数其实是存在争议的,一种说法是要避免从组件内定义的任何函数中返回 JSX,另一种说法是将这些函数提取为单独的组件。
function Article(props: Props) {
// ...
return (
<article>
<h1>{props.title}</h1>
<ArticleBody {...props} />
<ArticleFooter {...props} />
</article>
);
}
export default Article;
function ArticleBody(props: Props) {}
function ArticleFooter(props: Props) {}
在这种情况下,就必须手动将子组件所需的局部变量通过props传递。在使用 TypeScript 时,我们还需要为组件的props定义额外的类型。最终代码就会变得臃肿,这就会导致代码变得难以阅读和理解:
function Article(props: Props) {
const [status, setStatus] = useState("");
return (
<article>
<h1>{props.title}</h1>
<ArticleBody {...props} status={status} />
<ArticleFooter {...props} setStatus={setStatus} />
</article>
);
}
export default Article;
interface BodyProps extends Props {
status: string;
}
interface FooterProps extends Props {
setStatus: Dispatch<SetStateAction<string>>;
}
function ArticleBody(props: BodyProps) {}
function ArticleFooter(props: FooterProps) {}
这些单独的组件不可以重复使用,它们仅被它们所属的组件使用,单独使用它们是没有意义的。因此,这种情况下,还是建议将部分 JSX 提取成渲染函数。
8. 局部函数
React 组件通常会包含事件处理函数,它们是嵌套函数,通常会更改组件的内部状态或调度操作以更新组件的状态。
另一类嵌套函数就是闭包,它们是读取组件状态或props的不纯函数,用于构建组件逻辑。
function Article(props: Props) {
const [email, setEmail] = useState("");
return (
<article>
{/* ... */}
<form onSubmit={subscribe}>
<input type="email" value={email} onChange={setEmail} />
<button type="submit">Subscribe</button>
</form>
</article>
);
// 事件处理
function subscribe(): void {
if (canSubscribe()) {
// 发送订阅请求
}
}
function canSubscribe(): boolean {
// 基于 props 和 state 的逻辑
}
}
export default Article;
- 通常会使用函数声明而不是函数表达式来声明函数,因为函数是存在提升的,这允许我们在使用它们之后定义它们。这样就可以将它们放在组件函数的末尾,return语句之后;
- 如果一个函数中嵌套了另外一个函数,那么建议将调用者放在被调用者之前;
- 将这些函数按使用顺序排列。
9. 纯函数
最后就是纯函数,我们可以将它们放在组件文件的底部,在 React 组件之外:
function Article(props: Props) {
// ...
// ? 纯函数不应该放在组件之中
function getInitials(str: string) {}
}
export default Article;
function Article(props: Props) {
// ...
}
// ? 纯函数应该放在组件之外
function getInitials(str: string) {}
export default Article;
首先,纯函数没有依赖项,如 props、状态或局部变量,它们接收所有依赖项作为参数。这意味着可以将它们放在任何地方。但是,将它们放在组件之外还有其他原因:
- 它向任何阅读代码的开发人员发出信号,表示它们是纯粹的;
- 它们很容易测试,只需要将要测试的函数导出并导入到测试文件中即可;
- 如果需要提取和重用它们,可以很容易将它们很移动到其他文件。
完整示例
下面是一个完整的典型 React 组件示例。由于重点是文件的结构,因此省略了实现细节。
// 1?? 导入依赖项
import React from "react";
import { Tag } from "./tag";
import styles from "./article.module.scss";
// 2?? 静态定义
const MAX_READING_TIME = 10;
interface Props {
id: number;
name: string;
title: string;
meta: Metadata;
}
// 3?? 组件定义
function Article(props: Props) {
// 4?? 变量定义
const router = useRouter();
const theme = useTheme();
const { id, title, content, onSubscribe } = props;
const { image, author, date } = meta;
const [email, setEmail] = React.useState("");
const [showMenu, toggleMenu] = React.useState(false);
const summary = React.useMemo(() => getSummary(content), [content]);
const initials = getInitials(author);
const formattedDate = getDate(date);
// 5?? effects
React.useEffect(() => {
// ...
}, []);
// 6?? 渲染内容
return (
<article>
<h1>{title}</h1>
{renderBody()}
<form onSubmit={subscribe}>
{renderSubscribe()}
</form>
</article>
);
// 7?? 部分渲染
function renderBody() { /*...*/ }
function renderSubscribe() { /*...*/ }
// 8?? 局部函数
function subscribe() { /*...*/ }
}
// 9?? 纯函数
function getInitials(str: string) { /*...*/ }
export default Article;
相关推荐
- Docker 47 个常见故障的原因和解决方法
-
【作者】曹如熙,具有超过十年的互联网运维及五年以上团队管理经验,多年容器云的运维,尤其在Docker和kubernetes领域非常精通。Docker是一种相对使用较简单的容器,我们可以通过以下几种方式...
- 电脑30个快问快答,解决常见电脑问题
-
1.强行关机/停电对电脑有影响吗?答:可能损坏硬盘(机械硬盘风险高)、未保存数据丢失,偶尔一次影响小,但频繁操作会缩短硬件寿命。2.C盘满影响速度吗?答:会!系统运行需C盘空间缓存临时数据,空间不...
- 使用Tcpdump包抓取分析数据包的详细用法
-
TcpDump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。tcpdump就是一种...
- 电脑启动不了(BootDevice Not Found Hard Disk-3F0)解决方案
-
HP品牌机,开机启动不了,黑屏,开机取下主板电池恢复BIOS后,开机显示找不到启动盘。一、按F2键进入BIOS,出现硬盘内存检测界面的话,直接退出。就会出现这个界面,光标键向下,选择BIOSSetu...
- 电脑开机黑屏别慌!快码住!起底维修老师傅不能说的秘密
-
按下开机键却只收获黑屏大礼包?那些神秘的英文提示、刺耳的蜂鸣声,其实是电脑在给你发送求救信号!从按下电源到进入桌面的12秒里,你的电脑经历了史诗级的硬件自检与系统加载,今天我们就破译这段“摩斯电码”。...
- 电脑启动故障为何总要先看BIOS?新手必读的关键知识解析
-
最近在帮朋友们解答电脑无法正常开机的问题时,发现大家经常收到一句高频建议:“先检查BIOS”。对不少普通用户而言,BIOS依然是个神秘的存在。那么,BIOS到底是什么?电脑出现哪些故障会与它相关呢?本...
- Windows 11 KB5053598更新:安全补丁还是系统噩梦?
-
2025年3月11日,微软发布了Windows1124H2的强制性更新KB5053598,作为“周二补丁日”(PatchTuesday)的一部分。然而,这款本应提升系统安全性的更新却引发了广泛的...
- 飞牛OS入门安装遇到问题,如何解决?
-
之前小编尝试了用旧电脑装飞牛OS安装之前特意查了一些硬件要求飞牛OS目前支持主流的x86架构硬件主机需能连网线飞牛OS暂时不支持只有无线网卡的安装貌似很多小伙伴在一开始安装就卡住了那今天咱们汇总分...
- 几种常见的电脑开机黑屏显示白色英文字母解决方法
-
当电脑开机出现黑屏并显示白色英文字母时,通常表示系统启动过程中遇到了错误。以下是几种常见原因及对应的解决方法,按照排查顺序整理:一、检查外接设备与硬件连接可能原因:外接U盘、移动硬盘等未拔出,或内部硬...
- 电脑启动出现问题,为什么都要先检查BIOS?
-
【ZOL中关村在线原创技巧应用】最近在回答问题的时候,总会发现很多朋友都在问“电脑无法正常开机怎么办?”这样类似的问题,而许多DIY大佬的回复总会出现一条高频建议“先检查BIOS”。但对于许多普通用户...
- 教你怎么用JavaScript检测当前浏览器是无头浏览器
-
什么是无头浏览器(headlessbrowser)?无头浏览器是指可以在图形界面情况下运行的浏览器。我可以通过编程来控制无头浏览器自动执行各种任务,比如做测试,给网页截屏等。为什么叫“无头”浏览器?...
- 12个高效的Python爬虫框架,你用过几个?
-
实现爬虫技术的编程环境有很多种,Java、Python、C++等都可以用来爬虫。但很多人选择Python来写爬虫,为什么呢?因为Python确实很适合做爬虫,丰富的第三方库十分强大,简单几行代码便可实...
- 运维的报表之路,用 node.js 轻松发送 grafana 报表
-
在运维过程中,无论是监控还是报表,都会有一些通过邮件发送图表的需求,由于开源的zabbix,grafana和kibana等并不完全具有“想发送哪儿就发送哪儿”的图片生成功能,在grafana...
- C#基于浏览器内核的高级爬虫(c#爬取网页内容)
-
基于C#.NET+PhantomJS+Sellenium的高级网络爬虫程序。可执行Javascript代码、触发各类事件、操纵页面Dom结构、甚至可以移除不喜欢的CSS样式。很多网站都用Ajax动态加...
- 如何优化一个秒杀项目?(秒杀实现思路)
-
问题1:使用jmeter性能压测,定位瓶颈代码步骤流程:线程组--->Http请求--->查看结果树--->聚合报告tips:host的文件--->优先调用映射,减少DNS的时...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
python使用fitz模块提取pdf中的图片
-
《人人译客》如何规划你的移动电商网站(2)
-
Jupyterhub安装教程 jupyter怎么安装包
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- table.render (33)
- uniapp textarea (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- react-admin (33)
- vscode切换git分支 (35)
- vscode美化代码 (33)
- python bytes转16进制 (35)