精读《正交的 React 组件》

1 引言

搭配了合适的设计模式的代码,才可拥有良好的可维护性,The Benefits of Orthogonal React Components 这篇文章就重点介绍了正交性原理。

所谓正交,即模块之间不会相互影响。想象一个音响的音量与换台按钮间如果不是正交关系,控制音量同时可能影响换台,这样的设备很难维护:

前端代码也一样,UI 与数据处理逻辑分离就是一种符合正交原则的设计,这样有利于长期代码质量维护。

2 概述

一个拥有良好正交性的 React App 会按照如下模块分离设计:

  1. UI 元素(展示型组件)。
  2. 取数逻辑(fetch library, REST or GraphQL)。
  3. 全局状态管理(redux)。
  4. 持久化(local storage, cookies)。

文中通过两个例子说明。

让组件与取数逻辑正交

比如一个展示雇员列表组件 <EmployeesPage>:

import React, { useState } from "react";
import axios from "axios";
import EmployeesList from "./EmployeesList";

function EmployeesPage() {
  const [isFetching, setFetching] = useState(false);
  const [employees, setEmployees] = useState([]);

  useEffect(function fetch() {
    (async function() {
      setFetching(true);
      const response = await axios.get("/employees");
      setEmployees(response.data);
      setFetching(false);
    })();
  }, []);

  if (isFetching) {
    return <div>Fetching employees....</div>;
  }
  return <EmployeesList employees={employees} />;
}

这样设计看上去没问题,但其实违背了正交原则,因为 EmployeesPage 既负责渲染 UI 又关心取数逻辑。正交的写法如下:

import React, { Suspense } from "react";
import EmployeesList from "./EmployeesList";

function EmployeesPage({ resource }) {
  return (
    <Suspense fallback={<h1>Fetching employees....</h1>}>
      <EmployeesFetch resource={resource} />
    </Suspense>
  );
}

function EmployeesFetch({ resource }) {
  const employees = resource.employees.read();
  return <EmployeesList employees={employees} />;
}

Suspense 将 loading 状态剥离到父级组件,因此子组件只需要关心如何用数据,不需关心如何取数据(以及 loading 态)。

让组件与滚动监听正交

比如一个滚动到一定距离就出现 “jump to top” 的组件 <ScrollToTop>,可能会这么实现:

import React, { useState, useEffect } from "react";

const DISTANCE = 500;

function ScrollToTop() {
  const [crossed, setCrossed] = useState(false);

  useEffect(function() {
    const handler = () => setCrossed(window.scrollY > DISTANCE);
    handler();
    window.addEventListener("scroll", handler);
    return () => window.removeEventListener("scroll", handler);
  }, []);

  function onClick() {
    window.scrollTo({
      top: 0,
      behavior: "smooth"
    });
  }

  if (!crossed) {
    return null;
  }
  return <button onClick={onClick}>Jump to top</button>;
}

可以看到,在这个组件中,按钮与滚动状态判断逻辑混合在了一起。如果我们将 “滚动到一定距离就渲染 UI” 抽象成通用组件 IfScrollCrossed 呢?

import { useState, useEffect } from "react";

function useScrollDistance(distance) {
  const [crossed, setCrossed] = useState(false);

  useEffect(
    function() {
      const handler = () => setCrossed(window.scrollY > distance);
      handler();
      window.addEventListener("scroll", handler);
      return () => window.removeEventListener("scroll", handler);
    },
    [distance]
  );

  return crossed;
}

function IfScrollCrossed({ children, distance }) {
  const isBottom = useScrollDistance(distance);
  return isBottom ? children : null;
}

有了 IfScrollCrossed,我们就能专注写 “点击按钮跳转到顶部” 这个 UI 组件了:

function onClick() {
  window.scrollTo({
    top: 0,
    behavior: "smooth"
  });
}

function JumpToTop() {
  return <button onClick={onClick}>Jump to top</button>;
}

最后将他们拼装在一起:

import React from "react";

// ...

const DISTANCE = 500;

function MyComponent() {
  // ...
  return (
    <IfScrollCrossed distance={DISTANCE}>
      <JumpToTop />
    </IfScrollCrossed>
  );
}

这么做,我们的 <JumpToTop><IfScrollCrossed> 组件就是正交关系,而且逻辑更清晰。不仅如此,这样的抽象使 <IfScrollCrossed> 可以被其他场景复用:

import React from "react";

// ...

const DISTANCE_NEWSLETTER = 300;

function OtherComponent() {
  // ...
  return (
    <IfScrollCrossed distance={DISTANCE_NEWSLETTER}>
      <SubscribeToNewsletterForm />
    </IfScrollCrossed>
  );
}

Main 组件

上面例子中,<MyComponent> 就是一个 Main 组件,Main 组件封装一些脏逻辑,即它要负责不同模块的组装,而这些模块之间不需要知道彼此的存在。

一个应用会存在多个 Main 组件,它们负责拼装各种作用域下的脏逻辑。

正交设计的好处

  • 容易维护: 正交组件逻辑相互隔离,不用担心连带影响,因此可以放心大胆的维护单个组件。
  • 易读: 由于逻辑分离导致了抽象,因此每个模块做的事情都相对单一,很容易猜测一个组件做的事情。
  • 可测试: 由于逻辑分离,可以采取逐个击破的思路进行单测。

权衡

如果不采用正交设计,因为模块之间的关联导致应用最终变得难以维护。但如果将正交设计应用到极致,可能会多处许多不必要的抽象,这些抽象的复用仅此一次,造成过度设计。

3 精读

正交设计一定程度可以理解为合理抽象,完全不抽象与过度抽象都是不可取的,因此列举了四块需要抽象的要点:UI 元素、取数逻辑、全局状态管理、持久化。

全局状态管理注入到组件,就是一种正交的抽象模式,即组件不用关心数据从哪来,而直接使用数据,而数据管理完全交由数据流层管理。

取数逻辑往往是可能被忽略的一环,无论是像原文中直接关心到 fetch 方法的 UI 组件,还是利用取数工具库关心了 loading 状态:

import useSWR from "swr";

function Profile() {
  const { data, error } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

虽然将取数生命周期封装到自定义 hook useSWR 中,但 error 信息对 UI 组件来说就是一个脏数据:这让这个 UI 组件不仅要渲染数据,还要担心取数是否会失败,或者是否在 loading 中。

好在 Suspense 模式解决了这个问题:

import { Suspense } from "react";
import useSWR from "swr";

function Profile() {
  const { data } = useSWR("/api/user", fetcher, { suspense: true });
  return <div>hello, {data.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile />
    </Suspense>
  );
}

这样 <Profile> 只要专注于做数据渲染,而不用担心 useSWR('/api/user', fetcher, { suspense: true }) 这个取数过程发生了什么、是否取数失败、是否在 loading 中。因为取数状态由 Suspense 管理,而取数是否意外失败由 ErrorBoundary 管理。

合理的抽象使组件逻辑变得更简单,从而组件嵌套使用使不用担心额外影响。尤其在大型项目中,不要担心正交抽象会使本来就很多的模块数量再次膨胀,因为相比于维护 100 个相互影响,内部逻辑复杂的模块,维护 200 个职责清晰,相互隔离的模块也许会更轻松。

4 总结

从正交设计角度来看,Hooks 解决了状态管理与 UI 分离的问题,Suspense 解决了取数状态与 UI 分离的问题,ErrorBoundary 解决了异常与 UI 分离的问题。

在你看来,React 还有哪些逻辑需要与 UI 分离?分别使用哪些方法呢?欢迎留言。

讨论地址是:精读《正交的 React 组件》 · Issue #221 · dt-fe/weekly

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/452553.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

10、Redis分布式系统之数据分区算法

Redis分布式系统之数据分区算法 1、什么是Redis分布式系统 ​ Redis分布式系统&#xff0c;官方称为Redis Cluster, Redis集群&#xff08;这个集群和前面的主从复制集群不同&#xff0c;这个集群可以理解为是多个主从复制集群所组成的集群&#xff09;&#xff0c;其实是Red…

保研复习数据结构记(4)--树(二叉树、线索树、哈夫曼树,并查集)

一.树的基本术语 1.树 什么是空树&#xff1f;结点数为0的树非空树的特性&#xff1f;有且仅有一个根结点&#xff0c;没有后继的结点称为“叶子结点”&#xff0c;有后继的结点称为“分支结点”&#xff0c;除了根结点外任何一个结点都有且仅有一个前驱&#xff0c;每个结点…

VS 调试Hololens 2工程报错 有未经处理的异常: Microsoft C++ 异常:

原因是unity 少安装了XR工具包 安装完后重新用unity打包&#xff0c;然后vs打开打包出来的工程&#xff0c;电脑和眼镜用usb连接&#xff0c;直接运行调试就可以了

力扣L6--- 两数之和(java版)--2024年3月12日

1.题目 2.知识点 注1&#xff1a;在Java中&#xff0c;数组的长度不是通过调用一个方法获得的&#xff0c;而是通过一个属性直接获得的。因此&#xff0c;正确的语法是nums.length而不是nums.length()。 所以应该使用int m nums.length; 注2&#xff1a;return new int[]{i,…

3DMAX的选择模型高亮显示方式

1、选择时会显示有个外框&#xff0c;J或ShiftJ。 2 首选项设置里有高亮设置 3 就像F4一样&#xff0c;选择时边面显示

Spring Boot Admin服务监控

目录 概述实践server端pom.xml类配置结果clientpom.xml配置 结束 概述 Spring Boot Admin 集权限、日志、异常通知。 实践 server端 pom.xml <!-- SpringBoot Admin --> <dependency><groupId>de.codecentric</groupId><artifactId>spring-…

【算法面试题】-07

小明找位置 题目描述 小朋友出操&#xff0c;按学号从小到大排成一列;小明来迟了&#xff0c;请你给小明出个主意&#xff0c;让他尽快找到他应该排的位置。 算法复杂度要求不高于nLog(n);学号为整数类型&#xff0c;队列规模<10000; 输入描述 1、第一行:输入已排成队列的…

STM32外设分类--学习笔记

简介: 本文在于根据自己的理解&#xff0c;将stm32f103外设按照功能分个类别&#xff0c;便于记忆。下面的几张图一定要熟悉&#xff0c;后期编写代码时能够快速找到想要的功能和对应的引脚。 我使用的工具链是&#xff1a;使用CubeMX完成keil5工程搭建和引脚初始化功能,然后用…

使用Maven打包时出现Please refer to D:路径 for the individual怎么解决?

遇到这种情况不要着急&#xff0c;直接按照下面步骤即可&#xff1a; 解决方法1 可能是你的测试用例里出现了bug&#xff0c;根据下面提示的路径可以找到bug&#xff0c;打开 txt 文件&#xff08;可以每个都打开&#xff0c;不一定是哪个出bug了&#xff09; 去项目中修改完…

LeetCode(力扣)算法题_1261_在受污染的二叉树中查找元素

今天是2024年3月12日&#xff0c;可能是因为今天是植树节的原因&#xff0c;今天的每日一题是二叉树&#x1f64f;&#x1f3fb; 在受污染的二叉树中查找元素 题目描述 给出一个满足下述规则的二叉树&#xff1a; root.val 0 如果 treeNode.val x 且 treeNode.left ! n…

基于pci多功能采集卡——pci9640

一、追逐潮流&#xff0c;应运而生 信息社会的高速发展&#xff0c;在很大程度上取决于信息与信号处理的先进性。数字信号处理技术的出现改变了信号与信号处理技术的整个面貌&#xff0c;而数据采集作为数字信号处理的必不可少的前期工作在整个数字系统中起到关键性乃至决定性的…

C# RAM Stable Diffusion 提示词反推 Onnx Demo

目录 介绍 效果 模型信息 项目 代码 下载 C# RAM Stable Diffusion 提示词反推 Onnx Demo 介绍 github地址&#xff1a;GitHub - xinyu1205/recognize-anything: Open-source and strong foundation image recognition models. Open-source and strong foundation ima…

【Android】源码中的建造者模式

本文是基于 Android 14 的源码解析 在 Android 源码中&#xff0c;最常用到的建造者模式就是 AlertDialog.Builder&#xff0c;使用该建造者来构建复杂的 AlertDialog 对象。在开发过程中&#xff0c;我们经常用到 AlertDialog&#xff0c;具体示例如下&#xff1a; private f…

SA3D:基于 NeRF 的三维场景分割方法

Paper: Cen J, Zhou Z, Fang J, et al. Segment anything in 3d with nerfs[J]. Advances in Neural Information Processing Systems, 2024, 36. Introduction: https://jumpat.github.io/SA3D/ Code: https://github.com/Jumpat/SegmentAnythingin3D SA3D 是一种用于 NeRF 表…

RabbitMQ 面试题及答案整理,最新面试题

RabbitMQ的核心组件有哪些&#xff1f; RabbitMQ的核心组件包括&#xff1a; 1、生产者&#xff08;Producer&#xff09;&#xff1a; 生产者是发送消息到RabbitMQ的应用程序。 2、消费者&#xff08;Consumer&#xff09;&#xff1a; 消费者是接收RabbitMQ消息的应用程序…

阿里云领盲盒活动

阿里云每次的活动都很给力&#xff0c;实打实地发东西。 这次是体验 通义灵码 的活动&#xff0c;这个是体验的推广链接 「通义灵码 体验 AI 编码&#xff0c;开 AI 盲盒」 我是在vscode安装的&#xff0c;体验还行&#xff0c;抽奖抽到了马克杯 这个是抽奖的具体步骤 https:…

mysql对索引的选择简述

概述 在业务中经常会优化一些mysql的慢查询&#xff0c;通常都是使用explain去查看分析&#xff0c;检查扫描行数和索引的命中情况&#xff1b; 但是在具体索引的选择上&#xff0c;explain结果中并没有直接展示出来&#xff1b; 此时可以开启mysql的追踪优化器Trace功能&…

【Greenhills】MULTI IDE网络版license统计授权使用情况

【更多软件使用问题请点击亿道电子官方网站查询】 1、 文档目标 用于购买了GHS网络版的客户&#xff0c;对于license的调用情况进行统计和管理 2、 问题场景 对于购买了GHS网络版license的客户&#xff0c;想要对于授权的使用情况进行分析和管理&#xff0c;但是&#xff0c;…

c++ 常用函数 集锦 整理中

c 常用函数集锦 目录 1、string和wstring之间转换 1、string和wstring之间转换 std::string convertWStringToString(std::wstring wstr) {std::string str;if (!wstr.empty()){std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;str converter.to_b…

【论文阅读】ACM MM 2023 PatchBackdoor:不修改模型的深度神经网络后门攻击

文章目录 一.论文信息二.论文内容1.摘要2.引言3.作者贡献4.主要图表5.结论 一.论文信息 论文题目&#xff1a; PatchBackdoor: Backdoor Attack against Deep Neural Networks without Model Modification&#xff08;PatchBackdoor:不修改模型的深度神经网络后门攻击&#xf…