百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

如何设计更优雅的 React 组件?

liuian 2024-12-04 13:47 16 浏览

大家好,我是 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);

这里通常包含在组件级别使用的所有变量,使用 constlet 定义,具体取决于它们在渲染期间是否更改其值:

  • 解构数据:通常来自 props、数据 stores 或组件的 state
  • Hooks:自定义hooks、框架内置 Hooks,例如 useStateuseReduceruseRefuseCallbackuseMemo
  • 在整个组件中使用的已处理数据,由函数计算得出;

一些较大的组件可能需要在组件中声明很多变量。这种情况下,建议根据它们的初始化方法或者用途对它们进行分组:

// 框架 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来定义,因为letconst不存在变量提升,不能在定义的箭头函数之前使用它。

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;

相关推荐

GANs为何引爆机器学习?这篇基于TensorFlow的实例教程为你解惑!

「机器人圈导览」:生成对抗网络无疑是机器学习领域近三年来最火爆的研究领域,相关论文层出不求,各种领域的应用层出不穷。那么,GAN到底如何实践?本文编译自Medium,该文作者以一朵玫瑰花为例,详细阐...

高丽大学等机构联合发布StarGAN:可自定义表情和面部特征

原文来源:arXiv、GitHub作者:YunjeyChoi、MinjeChoi、MunyoungKim、Jung-WooHa、SungKim、JaegulChoo「雷克世界」编译:嗯~...

TensorFlow和PyTorch相继发布最新版,有何变化

原文来源:GitHub「机器人圈」编译:嗯~阿童木呀、多啦A亮Tensorflow主要特征和改进在Tensorflow库中添加封装评估量。所添加的评估量列表如下:1.深度神经网络分类器(DNNCl...

「2022 年」崔庆才 Python3 爬虫教程 - 深度学习识别滑动验证码缺口

上一节我们使用OpenCV识别了图形验证码躯壳欧。这时候就有朋友可能会说了,现在深度学习不是对图像识别很准吗?那深度学习可以用在识别滑动验证码缺口位置吗?当然也是可以的,本节我们就来了解下使用深度...

20K star!搞定 LLM 微调的开源利器

LLM(大语言模型)微调一直都是老大难问题,不仅因为微调需要大量的计算资源,而且微调的方法也很多,要去尝试每种方法的效果,需要安装大量的第三方库和依赖,甚至要接入一些框架,可能在还没开始微调就已经因为...

大模型DeepSeek本地部署后如何进行自定义调整?

1.理解模型架构a)查看深度求索官方文档或提供的源代码文件,了解模型的结构、输入输出格式以及支持的功能。模型是否为预训练权重?如果是,可以在预训练的基础上进行微调(Fine-tuning)。是否需要...

因配置不当,约5000个AI模型与数据集在公网暴露

除了可访问机器学习模型外,暴露的数据还可能包括训练数据集、超参数,甚至是用于构建模型的原始数据。前情回顾·人工智能安全动态向ChatGPT植入恶意“长期记忆”,持续窃取用户输入数据多模态大语言模型的致...

基于pytorch的深度学习人员重识别

基于pytorch的深度学习人员重识别Torchreid是一个库。基于pytorch的深度学习人员重识别。特点:支持多GPU训练支持图像的人员重识别与视频的人员重识别端到端的训练与评估简单的re...

DeepSeek本地部署:轻松训练你的AI模型

引言:为什么选择本地部署?在AI技术飞速发展的今天,越来越多的企业和个人希望将AI技术应用于实际场景中。然而,对于一些对数据隐私和计算资源有特殊需求的用户来说,云端部署可能并不是最佳选择。此时,本地部...

谷歌今天又开源了,这次是Sketch-RNN

前不久,谷歌公布了一项最新技术,可以教机器画画。今天,谷歌开源了代码。在我们研究其代码之前,首先先按要求设置Magenta环境。(https://github.com/tensorflow/magen...

Tensorflow 使用预训练模型训练的完整流程

前面已经介绍了深度学习框架Tensorflow的图像的标注和训练数据的准备工作,本文介绍一下使用预训练模型完成训练并导出训练的模型。1.选择预训练模型1.1下载预训练模型首先需要在Tensorf...

30天大模型调优学习计划(30分钟训练大模型)

30天大模型调优学习计划,结合Unsloth和Lora进行大模型微调,掌握大模型基础知识和调优方法,熟练应用。第1周:基础入门目标:了解大模型基础并熟悉Unsloth等工具的基本使用。Day1:大模...

python爬取喜马拉雅音频,json参数解析

一.抓包分析json,获取加密方式1.抓包获取音频界面f12打开抓包工具,播放一个(非vip)视频,点击“媒体”单击打开可以复制URL,发现就是我们要的音频。复制“CKwRIJEEXn-cABa0Tg...

五、JSONPath使用(Python)(json数据python)

1.安装方法pipinstalljsonpath2.jsonpath与Xpath下面表格是jsonpath语法与Xpath的完整概述和比较。Xpathjsonpath概述/$根节点.@当前节点...

Python网络爬虫的时候json=就是让你少写个json.dumps()

大家好,我是皮皮。一、前言前几天在Python白银交流群【空翼】问了一个Python网络爬虫的问题,提问截图如下:登录请求地址是这个:二、实现过程这里【甯同学】给了一个提示,如下所示:估计很多小伙伴和...