QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

2024 年,你应该知道的 5 种 React 设计模式

作者:Lakindu Hewawasam

  • 2023-12-31
    北京
  • 本文字数:6222 字

    阅读完需:约 20 分钟

大小:1.58M时长:09:13
2024年,你应该知道的5种React设计模式

如果你在开发工作中使用的是 React 框架,那么首当其冲要学习的就是思考如何设计组件。组件设计并非简单地将多个组件合成一个集合,而是需要思考如何设计更小、复用性更强的组件。例如,思考下面这张组件图:

 

 

简化的组件图

 

图中有三个组件,分别是:

 

  1. Typography 组件

  2. Footer 组件

  3. Sizeable Box 组件

 

如图所示,Typography 组件同时被 Footer 和 Sizeable Box 组件使用。通常我们以为这样,就能构建一个简单、易维护和易排除错误的应用了。但其实只是这样思考组件的设计是远远不够的。

 

如果你知道如何从组件的视角思考问题,就可以通过在 React 组件中使用设计模式来提高代码的整体模块性、可扩展性和可维护性。

 

因此,下面这五种设计模式,是你在使用 React 时必须要掌握的。

 

模式一:基础组件


首先,在使用 React 时候,请尝试为应用设计基础组件。

 

基础UI组件,就是一个具备默认行为且支持定制化的组件。

 

例如,每个应用都会通过基础的样式、按钮设计或者基础的排版,来实现应用在视觉和交互上的一致性。这些组件的设计特点是:

 

  1. 组件会应用一组默认的配置。因此,使用者无需进行任何额外的配置,就可以快速基于默认配置使用组件。

  2. 组件可以支持定制化,使用者通过定制化可以覆盖组件的默认行为,从而为组件提供自定义的整体视觉和交互效果。

 

通过一个 Button 组件就能很好地说明基础组件模式的实现。示例如下:

 

  1. Button 组件可能会有诸如空心、实心等不同形态。

  2. Button 组件可能会有默认的文本。

 

现在你就可以利用基础组件模式进行设计,使组件的使用者可以改变其行为。请参考我基于基础组件模式完成的Button组件,示例代码如下:

 

import React, { ButtonHTMLAttributes } from 'react';

// 按钮组件的形态:实心或者空心type ButtonVariant = 'filled' | 'outlined';

export type ButtonProps = { /** * the variant of the button to use * @default 'outlined' */ variant?: ButtonVariant;} & ButtonHTMLAttributes<HTMLButtonElement>;;

const ButtonStyles: { [key in ButtonVariant]: React.CSSProperties } = { filled: { backgroundColor: 'blue', // Change this to your filled button color color: 'white', }, outlined: { border: '2px solid blue', // Change this to your outlined button color backgroundColor: 'transparent', color: 'blue', },};

export function Button({ variant = 'outlined', children, style, ...rest }: ButtonProps) { return ( <button type='button' style={{ ...ButtonStyles[variant], padding: '10px 20px', borderRadius: '5px', cursor: 'pointer', ...style }} {...rest}> {children} </button> );}
复制代码

 

仔细观察代码会发现,这里 Button 组件的 props 类型合并了原生 HTML 中 button 标签属性的全部类型。这意味着,使用者除了可以为 Button 组件设置默认配置外,还可以设置诸如 onClick、aria-label 等自定义配置。这些自定义配置会通过扩展运算符传递给 Button 组件内部的 button 标签。

 

通过不同的上下文设置,可以看到不同的 Button 组件的形态,效果截图如下图。

 

这个可以查看具体设置:

https://bit.cloud/lakinduhewa/react-design-patterns/base/button/~compositions

 

 

基础组件在不同上下文中的使用效果

 

通过不同的上下文,你可以设定组件的行为。这可以让组件成为更大组件的基础。

 

模式二:组合组件

 

在成功创建了基础组件后,你可能会希望基于基础组件创建一些新的组件。

 

例如,你可以使用之前创建的 Button 组件来实现一个标准的 DeleteButton 组件。通过在应用中使用该 DeleteButton,可以让应用中所有删除操作在颜色、形态以及字体上保持一致。

 

不过,如果出现重复组合一组组件来实现相同效果的现象,那么你可以考虑将它们封装到一个组件中。

 

下面,让我们来看看其中一种实现方案:

https://bit.cloud/lakinduhewa/react-design-patterns/composition/delete-button

 

 

使用组合模式创建组件

 

如上面的组件依赖图所示,DeleteButton 组件使用基础的 Button 组件为所有与删除相关的操作提供标准的实现。下面是基本代码实现:

 

// 这里引入了,基础按钮组件和其propsimport { Button, ButtonProps } from '@lakinduhewa/react-design-patterns.base.button';import React from 'react';

export type DeleteButtonProps = {} & ButtonProps;

export function DeleteButton({ ...rest }: DeleteButtonProps) { return ( <Button variant='filled' style={{ background: 'red', color: 'white' }} {...rest} > DELETE </Button> );}
复制代码

 

我们使用基于模式一创建的 Button 组件来实现的 DeleteButton 组件的效果如下:

 

 

现在我们可以在应用中使用统一的删除按钮。此外,如果你使用类似 Bit 的构建系统进行组件的设计和构建,那么当 Button 组件发生改变时,可以让CI服务自动将此改变传递到DeleteButton组件上,就像下面这样(当 Button 组件从 0.0.3 升级到了 0.0.4,那么 CI 服务会自动触发,将 DeleteButton 组件从 0.0.1 升级到 0.0.2):

 

Bit 上的一个 CI 构建


模式三:使用 Hooks


React Hooks 是React v16就推出来的特性,它不依赖类组件实现状态管理、负效应等概念。简而言之,就是你可以通过利用 Hooks API 摆脱对类组件的使用需求。useSate 和 useEffect 是最广为人知的两个 Hooks API,但本文不打算讨论它们,我想重点讨论如何利用 Hooks 来提高组件的整体可维护性。

 

例如,请考虑下面这个场景:

 

  1. 有一个 BlogList 组件。

  2. BlogList 组件会通过调用一个简单的 API,获取博客文章列表数据,同时将其渲染在组件上。

 

基于上面的案例,你可能会像下面这样将 API 逻辑直接写在函数组件中:

 

import React, { useState, useEffect } from 'react';import axios from 'axios';const BlogList = () => {    const [blogs, setBlogs] = useState([]);    const [isLoading, setIsLoading] = useState(true);    const [error, setError] = useState(null);    useEffect(() => {        axios.get('https://api.example.com/blogs')            .then(response => {                setBlogs(response.data);                setIsLoading(false);            })            .catch(error => {                setError(error);                setIsLoading(false);            });    }, []);    if (isLoading) return <div>Loading...</div>;    if (error) return <div>Error: {error.message}</div>;    return (        <div>            <h2>Blog List</h2>            <ul>                {blogs.map(blog => (                    <li key={blog.id}>{blog.title}</li>                ))}            </ul>        </div>    );};export default BlogList;
复制代码

 

这样写,组件也能正常工作。它将会获取博客文章列表并且渲染在 UI 上。但是,这里将 UI 逻辑和 API 逻辑混在一起了。

 

理想情况下,React 组件应该不需要关系如何获取数据。而只需要关心接收一个数据数组,然后将其呈现在 DOM 上。

 

因此,实现这一目标的最佳方法是将 API 逻辑抽象到 React Hook 中,以便在组件内部进行调用。这样做就可以打破 API 调用与组件之间的耦合。通过这种方式,就可以在不影响组件的情况下,修改底层的数据获取逻辑。

 

其中一种实现方式如下。

 

1.useBlog hook


import { useEffect, useState } from 'react';import { Blog } from './blog.type';import { Blogs } from './blog.mock';

export function useBlog() { const [blogs, setBlogs] = useState<Blog[]>([]); const [loading, setLoading] = useState<boolean>(false);

useEffect(() => { setLoading(true); // 注意:这里的setTimeout非实际需要,只是为了模拟API调用 setTimeout(() => { setBlogs(Blogs); setLoading(false); }, 3000); }, []);

return { blogs, loading }}
复制代码

 

如上代码所示,useBlog hook 获取博客列表数据,然后赋值给状态变量,最后通过导出变量给到消费者(BlogList 组件)使用:

 

 

Hook 效果

 

2.BlogList 组件


import React from 'react';// 引入上面封装的 useBlog hookimport { useBlog } from '@lakinduhewa/react-design-patterns.hooks.use-blog';export function BlogList() {  const { blogs, loading } = useBlog();  if (loading) {    return (      <p>We are loading the blogs...</p>    )  }  return (    <ul>      {blogs.map((blog) => <ol        key={blog.id}      >        {blog.title}      </ol>)}    </ul>  );}
复制代码

 


BlogList 组件效果

 

通过调用 useBlog 和使用其导出的状态变量,我们在 BlogList 组件中使用了 Hooks。如此,相对于之前,我们可以减少大量代码,并以最少的代码和精力维护两个组件。

 

此外,当你使用类似 Bit 这样的构建系统时(就像我一样),只需将 useBlog 组件导入本地开发环境,然后再修改完成之后重新推送回 Bit Cloud。Bit Cloud 的构建服务器可以依托依赖树将此修改传递给整个应用。因此如果只执行一些简单修改,甚至不需要访问整个应用。

 

模式四:React Providers


此模式的核心是解决组件状态共享。我们都曾是 props 下钻式传递的受害者。但如果你还没有经历过,那这里简单解释下:“props 下钻式传递”就是当你在组件树中进行 props 传递时,这些 props 只会在最底层组件中被使用,而中间层的组件都不会使用该 props。例如,看看下面这张图:

props 下钻式传递


从 BlogListComponent 一直向下传递一个 isLoading 的 props 到 Loader。但是,isLoading 只在 Loader 组件中使用。因此,在这种情况下,组件不但会引入不必要的 props,还会有性能开销。因为当 isLoading 发生变化时,即使组件没有使用它,React 依然会重新渲染你的组件树。

 

因此,解决方案之一就是通过利用 React Context 来使用 React Context Provider 模式。React Context 是一组组件的状态管理器,通过它,你可以为一组组件创建特定的上下文。通过这种方式,你可以在上下文中定义和管理状态,让不同层级的组件都可以直接访问上下文,并按需使用 props。这样就可以避免 props 下钻式传递了。

 

主题组件就是该模式的一个常见场景。例如,你需要在应用程序中全局访问主题。但将主题传递到应用中的每个组件并不现实。你可以创建一个包含主题信息的 Context,然后通过 Context 来设置主题。看一下我是如何通过React Context实现主题的,以便更好地理解这一点:

https://bit.cloud/lakinduhewa/react-design-patterns/contexts/consumer-component

 

import { useContext, createContext } from 'react';export type SampleContextContextType = {  /**   * primary color of theme.   */  color?: string;};export const SampleContextContext = createContext<SampleContextContextType>({  color: 'aqua'});export const useSampleContext = () => useContext(SampleContextContext);
复制代码

 

在 Context 中定义了一种主题颜色,它将在所有实现中使用该颜色来设置字体颜色。接下来,我还导出了一个 hook——useSampleContext,该 hook 让消费者可以直接使用 Context。

 

只是这样还不行,我们还需要定义一个 Provider。Provider 是回答 "我应该与哪些组件共享状态?"问题的组件。Provider的实现示例如下:

 

import React, { ReactNode } from 'react';import { SampleContextContext } from './sample-context-context';export type SampleContextProviderProps = {  /**   * primary color of theme.   */  color?: string,  /**   * children to be rendered within this theme.   */  children: ReactNode};export function SampleContextProvider({ color, children }: SampleContextProviderProps) {  return <SampleContextContext.Provider value={{ color }}>{children}</SampleContextContext.Provider>}
复制代码

 

Provider 在管理初始状态和设置 Context 可访问状态的组件方面起着至关重要的作用。

 

接下来,你可以创建一个消费者组件来使用状态:

 

 

消费者组件

 

模式五:条件渲染


最后一个想和大家分享的是条件渲染模式。今天,人人都知道 React 中的条件渲染。它通过条件判断来选择组件进行渲染。

 

但在实际使用中我们的用法常常是错误的:

 

// ComponentA.jsconst ComponentA = () => {    return <div>This is Component A</div>;};// ComponentB.jsconst ComponentB = () => {    return <div>This is Component B</div>;};// ConditionalComponent.jsimport React, { useState } from 'react';import ComponentA from './ComponentA';import ComponentB from './ComponentB';const ConditionalComponent = () => {    const [toggle, setToggle] = useState(true);    return (        <div>            <button onClick={() => setToggle(!toggle)}>Toggle Component</button>            {toggle ? <ComponentA /> : <ComponentB />}        </div>    );};export default ConditionalComponent;
复制代码

 

你是否注意到,这里我们将基于条件的逻辑耦合到了 JSX 代码片段中。通常,你不应该在 JSX 代码中中添加任何与计算相关的逻辑,而只将与 UI 渲染相关的内容放在其中。

 

解决这个问题的方法之一是使用条件渲染组件模式。创建一个可重用的 React 组件,该组件可以根据条件渲染两个不同的组件。它的实现过程如下

 

import React, { ReactNode } from 'react';export type ConditionalProps = {  /**   * the condition to test against   */  condition: boolean  /**   * the component to render when condition is true   */  whenTrue: ReactNode  /**   * the component to render when condition is false   */  whenFalse: ReactNode};export function Conditional({ condition, whenFalse, whenTrue }: ConditionalProps) {  return condition ? whenTrue : whenFalse;}
复制代码

 

我们创建了一个可以按条件渲染两个组件的组件。当我们将其集成到其他组件中时,会使代码更简洁,因为无需在 React 组件中加入复杂的渲染逻辑。你可以像下面这样使用它:

 

export const ConditionalTrue = () => {  return (    <Conditional      condition      whenFalse="You're False"      whenTrue="You're True"    />  );}export const ConditionalFalse = () => {  return (    <Conditional      condition={false}      whenFalse="You're False"      whenTrue="You're True"    />  );}
复制代码

 

实际的输入如下:

 

 

总结

 

掌握这五种设计模式,为 2024 年做好充分准备,构建出可扩展和可维护的应用吧。

 

如果你想详细深入本文中讨论的模式,请随时查看我在Bit Cloud的空间

https://bit.cloud/lakinduhewa/react-design-patterns

 

感谢你的阅读!

 

原文链接:

https://blog.bitsrc.io/react-design-patterns-for-2024-5f2696868222


相关阅读:


React JS 广受业界认可,高级开发者年薪百万

从新 React 文档看未来 Web 的开发趋势

我被 React 劫持了,很痛苦又离不开

React 开发者们的 Solid.js 快速入门教程

2023-12-31 08:009362

评论

发布
暂无评论
发现更多内容

LeetCode-119. 杨辉三角II(java)

bug菌

Leet Code 7月月更

数据也能进超市

天翼云开发者社区

云计算 大数据 云平台

大数据ZooKeeper(一):基本知识和集群搭建

Lansonli

大数据 zookeeper 7月月更

SVN 修订版本关键字

攻城狮杰森

svn 关键字 7月月更

易观分析加入智能投研技术联盟,共促行业数智化发展

易观分析

易观新闻

关于微软 Edge 浏览器的 Tracking Prevention 特性在 Angular 应用中的影响

汪子熙

JavaScript typescript Web web开发 7月月更

数据库审计部署方式有哪些?哪种比较好?

行云管家

数据库 数据库审计 数据库审计部署

揭露数据不一致的利器 —— 实时核对系统

Shopee技术团队

数据分析 后端

天翼云携手华为,强强联合,共创数据存储新生态

天翼云开发者社区

存储 数字化

Python|分析QQ群聊信息,记录词频并制作词云

AXYZdong

Python 7月月更

APISIX 如何与 Hydra 集成,搭建集中认证网关助力企业安全

API7.ai 技术团队

云原生 网关 身份验证 APISIX 网关

告别缺电焦虑!充电桩装上“智慧大脑”

天翼云开发者社区

云主机 云平台

大数据培训Spark数据倾斜问题的解决方法

@零度

spark 大数据开发

百家号基于AE的视频渲染技术探索

百度Geek说

视频 视频渲染

内行,阿里大牛离职带出内部“高并发系统设计”学习手册

程序知音

Java 阿里巴巴 程序员 后端 高并发

leetcode 455. Assign Cookies 分发饼干(简)

okokabcd

LeetCode 数据结构与算法 贪心算法

2022数十位Java架构师汇总产出,最新25个技术栈“Java面经”

程序知音

Java 程序员 面试 后端 八股文

拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

程序员小毕

程序员 程序人生 计算机网络 java面试 TCP协议

【容器篇】Docker实现资源隔离的秘籍

技术小生

Docker 7月月更

【愚公系列】2022年7月 Go教学课程 010-数据类型之布尔型和字符类型

愚公搬代码

7月月更

阿里云第四届全球数据库大赛火热开赛,40万奖金广纳英才

科技热闻

小程序表单组件-1

小恺

7月月更

为安全而生!云安全漫谈开讲啦

云安全 云计算运维

数据库审计和日志审计的三大区别分析

行云管家

数据库 日志 日志审计 数据库审计

java零基础入门-多态

喵手

Java 7月月更

火眼金睛,天翼云助力打造城市视觉中枢

天翼云开发者社区

大数据 云平台

Python 入门指南之标准库概览

海拥(haiyong.site)

7月月更

项目进度管理和风险管理记录

老猎人

一招,让停车管理不再难

天翼云开发者社区

数字化 云平台

大数据环境搭建:​​​​​​​​​​​​​​​​​​​​​Hadoop编译和分布式环境搭建

Lansonli

大数据 hadoop 环境搭建 7月月更

面试官:Linux操作系统里一个进程最多可以创建多少个线程?

Java全栈架构师

程序员 多线程 操作系统 计算机 java面试

2024年,你应该知道的5种React设计模式_架构/框架_InfoQ精选文章