在日常开发中,团队中每个人组织代码的方式不尽相同。下面我们就从代码结构的角度来看看如何组织一个更加优雅的 React 组件!
我们通常会在组件文件顶部导入组件所需的依赖项。对于不同类别的依赖项,建议对它们进行分组,这有助于帮助我们更好的理解组件。可以将导入的依赖分为四类:
// 外部依赖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";
对导入依赖项进行手动分组可能比较麻烦,Prettier 可以帮助我们自动格式化代码。可以使用 prettier-plugin-sort-imports 插件来自动格式化依赖项导入。需要在项目根目录创建prettier.config.js配置文件,并在里面配置规则:
module.exports = { // 其他 Prettier 配置 importOrder: [ // 默认情况下,首先会放置外部依赖项 // 内部依赖 "^../(.*)", // 本地依赖项,样式除外 "^./((?!scss).)*$", // 其他 "^./(.*)", ], 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
在导入依赖项的下方,通常会放那些使用 TypeScript 或 Flow 等静态类型检查器定义的文件级常量和类型定义。
组件中的所有 magic 值,例如字符串或者数字,都应该放在文件的顶部,导入依赖项的下方。由于这些都是静态常量,这意味着它们的值不会改变。因此将它们放在组件中是没有意义的,因为放在组件中的话,它们会在每次重新渲染组件时重新创建。
const MAX_READING_TIME = 10;const META_TITLE = "Hello World";
对于更复杂的静态数据结构,可以将其提取到一个单独的文件中,以保持组件代码整洁。
下面是使用 TypeScript 声明的组件 props 的类型:
interface Props { id: number; name: string; title: string; meta: Metadata;}
如果这个 props 的类型不需要导出,可以使用 Props 作为接口名称,这样可以帮助我们立即识别组件 props 的类型定义,并将其与其他类型区分开。
只有当这个 Props 类型需要在多个组件中使用时,才需要添加组件名称,例如ButtonProps,因为它在导入另一个组件时,不应该与另一个组件的Props类型冲突。
定义函数组件的方式有两种:函数声明和箭头函数, 推荐使用函数声明的形式,因为这就是语法声明的内容:函数。官方文档的示例中也使用了这种方法:
function Article(props: Props) { /**/}
只会在必须使用 forwardRef 时才使用箭头函数:
const Article = React.forwardRef<HTMLArticleElement, Props>( (props, ref) => { /**/ });
通常会在组件最后默认导出组件:
export default Article;
接下来,我们就需要在组件里面进行变量的声明。注意,即使使用 const 声明,这里也称为变量,因为它们的值通常会在不同的渲染之间发生变化,只有在执行单个渲染过程时是恒定的。
const { id, name, title } = props;const router = useRouter();const initials = getInitials(name);
这里通常包含在组件级别使用的所有变量,使用 const 或 let 定义,具体取决于它们在渲染期间是否更改其值:
一些较大的组件可能需要在组件中声明很多变量。这种情况下,建议根据它们的初始化方法或者用途对它们进行分组:
// 框架 hooksconst router = useRouter();// 自定义 hooksconst 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]);// refsconst sideMenuRef = useRef<HTMLDivElement>(null);const subscribeRef = useRef<HTMLButtonElement>(null);// 计算数据const initials = getInitials(author);const formattedDate = getDate(date);
变量分组的方法在不同组件之间可能会存在很大的差异,它取决于变量的数量和类型。关键是要将相关变量放在一起,在不同组之间添加一个空行来提高代码的可读性。
注:上面代码中的注释仅用于标注分组类型,在实际项目中不会写这些注释。
Effects 部分通常会写在变量声明之后,它们可能是React中最复杂的构造,但从语法的角度来看它们非常简单:
useEffect(() => { setLogo(theme === "dark" ? "white" : "black");}, [theme]);
任何包含在effect之内但是在其外部定义的变量,都应该包含在依赖项的数组中。
除此之外,还应该使用return来清理副作用:
useEffect(() => { function onScroll() { /*...*/ } window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll);}, []);
组件的核心就是它的内容,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的下方,那么如果想要使用箭头函数来自定义函数,那就只能使用var来定义,因为let和const不存在变量提升,不能在定义的箭头函数之前使用它。
在处理大型 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;
那为什么不将它们提取为组件呢?关于部分渲染函数其实是存在争议的,一种说法是要避免从组件内定义的任何函数中返回 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 提取成渲染函数。
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;
最后就是纯函数,我们可以将它们放在组件文件的底部,在 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;
本文链接:http://www.28at.com/showinfo-26-51233-0.html如何设计更优雅的 React 组件?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: C语言中的柔性数组解析