写点什么

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:008959

评论

发布
暂无评论

FliFlik Watermark Remover for Mac(去水印工具) v6.0.0激活版

Mac相关知识分享

独“数”一帜 双证加冕!TeleDB亮相可信数据库发展大会

天翼云开发者社区

数据库

支持75对矿井、23 类、1100余个系统工业数据采集,云鼎科技煤矿安全监测的经验分享

TDengine

实时检出率仅19%,SIEM还是网络威胁处理的“瑞士军刀”吗?

我再BUG界嘎嘎乱杀

网络安全 安全 信息安全 网安 SIEM

用二维码进行活动报名,收集汇总信息更高效

草料二维码

草料二维码 报名二维码 活动报名二维码

C#中常用集合类型

不在线第一只蜗牛

C# 集合 windows

DNS在架构中的使用

EquatorCoco

架构 DNS

AI+奥运:2024巴黎奥运时刻,怎么用AI技术给网友亿点震撼?

爱AI的猫猫头

人工智能 海报 AI绘画 Prompt AI视频

如何选择最佳需求跟踪工具?8大优质系统盘点

易成管理学

需求管理 需求管理工具 需求跟踪

金融行业中API的挑战与未来趋势

蛙人族

API接口

海外直播APP源码技术配置说明 个性化定制海外直播平台

山东布谷科技胡月

国际版语音直播APP 社交直播APP开发 海外直播App开发 海外直播APP源码 聊天交友源码

迈向更高级的财务规划,助推企业蓬勃发展

智达方通

数字化转型 企业管理 全面预算管理 财务管理 财务规划

微软:云服务大规模宕机因DDoS“防卫过当”

我再BUG界嘎嘎乱杀

黑客 网络安全 信息安全 DDoS

怎么解决做海外直播的网络问题?

Ogcloud

海外直播专线 海外直播 tiktok直播专线 海外直播网络 tiktok直播网络

折叠想象,「天池AI IP形象征集大赛」火热进行中!

阿里云天池

阿里云 AI 图像生成

010 Editor for Mac(文本和十六进制编辑器) v15.0激活版

Mac相关知识分享

体验教程:通义灵码陪你备战求职季

阿里巴巴云原生

阿里云 云原生 通义灵码

【网络安全】Web Hacking网络黑客手册,GitHub星标3.7K!

我再BUG界嘎嘎乱杀

黑客 网络安全 安全 WEB安全 网安

Sonoma Cache Cleaner for Mac(MacOS系统清理优化工具)注册激活版

Mac相关知识分享

淘宝商品数据接口实战:自动化监控与竞品分析

tbapi

淘宝API接口 淘宝商品列表数据接口 淘宝商品列表数据采集

Next.js 中为什么 App Router 可能是未来,但 Pages Router 仍然重要?

Immerse

router 路由 路由框架 next.js app route

荣誉再加码!2024可信云大会,天翼云载誉而归!

天翼云开发者社区

云计算 可信云大会

SiteSucker Pro for Mac(网站下载工具SiteSucker专业版)v5.4.3中/英

Mac相关知识分享

智算引领,数耀鹭岛!天翼云与厦门电信共筑智算时代新底座!

天翼云开发者社区

云计算 智算中心 天翼云

Java 后端已经过时的技术,也是我逝去的青春

源字节1号

开源

汽车虚拟仿真交互体验更真实,实时云渲染来助力!

3DCAT实时渲染

实时云渲染 云仿真 云交互 实时渲染云 汽车虚拟仿真

碳课堂|什么是碳盘查、碳核查?

AMT企源

碳管理 碳核算

速卖通商品列表数据接口(标题|主图|销量|价格|店铺)

tbapi

速卖通API接口 速卖通商品列表数据接口 速卖通API

TikTok海外直播专线是什么?有什么用?

Ogcloud

TikTok tiktok运营 tiktok直播 tiktok直播专线 tiktok直播网络

体验教程:通义灵码陪你备战求职季

阿里云云效

阿里云 云原生 通义灵码

SaaS 服务:满足个性化需求

可观测技术

SaaS

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