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

评论

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

程序员入职国企,1周上班5小时,晒出薪资感叹,阿里P8架构师的Java大厂面试题总结

Java 程序员 后端

程序员面试时这样介绍自己的项目经验,成功率能达到98,华为od技术一面

Java 程序员 后端

程序员是如何看待薪资被高估的?内容过于真实,java语言程序设计与数据结构进阶版

Java 程序员 后端

究竟是什么样的奇葩需求?威胁到程序员的头发,java高级特性编程及实战第三章

Java 程序员 后端

算法基础之递归,java核心技术卷

Java 程序员 后端

碎片化时间学这些架构知识,月薪20K还不是轻轻松松(1),美团高级java面试题

Java 程序员 后端

碎片化时间学这些架构知识,月薪20K还不是轻轻松松,教你解决线上频出MySQL死锁问题

Java 程序员 后端

秀儿!用SSM框架实现了支付宝的支付功能,神操作啊,大型分布式系统架构图

Java 程序员 后端

秋招必备!阿里产出的高并发+JVM套餐,mybatis总结

Java 程序员 后端

程序员必知必会之——服务网格istio概念,springboot项目案例百度云

Java 程序员 后端

精通springcloud:服务发现,Eureka API,java技术上难以解决的问题

Java 程序员 后端

确定要面试问我JVM吗?我打算聊一个小时的!(1),linux驱动架构

Java 程序员 后端

神操:凭借“阿里Java脑图,mysql数据库教学视频教程

Java 程序员 后端

程序员就意味着高薪?解除35岁的忧虑,一条正确的职业生涯规划

Java 程序员 后端

程序员一定要会的软件项目管理评估方案,不做只会敲代码的码农!

Java 程序员 后端

算法在哈啰顺风车中的实践应用,netty实战pdf

Java 程序员 后端

立即可用的实战源码(springboot+redis+mybatis,java自学教程免费视频

Java 程序员 后端

类加载器深入剖析,2021最新华为Java校招面试题

Java 程序员 后端

精心备战30天,三天斩获阿里offer,揭秘面试流程及我的学习方向

Java 程序员 后端

神操:凭借“阿里Java脑图(1),神操作

Java 程序员 后端

秒懂 Java 的三种代理模式,任小龙java笔记百度云

Java 程序员 后端

程序员都应当知道的实用工具网站,Java400道面试题通关宝典助你进大厂

Java 程序员 后端

[架构实战营]模块二作业:微信朋友圈高性能复杂度架构

Geek_99eefd

架构实战营

精心整理全网最全Tomcat面试专题及答案(共19题,含答案解析

Java 程序员 后端

确定要面试问我JVM吗?我打算聊一个小时的!,目前最全的《Java面试题及解析》

Java 程序员 后端

程序员欣宸的文章分类汇总,javaee教程文档

Java 程序员 后端

算法基础之暴力递归到动态规划,java程序员面试算法宝典pdf猿媛之家

Java 程序员 后端

算法宝典最新分享:Alibaba+小米,redis笔记

Java 程序员 后端

秒懂数组拷贝,感知新境界,java编程思维百度云

Java 程序员 后端

程序员开发必备22个终端CLI工具也太香了(附下载地址!

Java 程序员 后端

算法入门 - 动态数组的实现(Java版本),分层架构图案例

Java 程序员 后端

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