立即注册找回密码

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

手机动态码快速登录

手机号快速注册登录

搜索

图文播报

查看: 489|回复: 5

[讨论] react hooks有必要分离 ui 和业务逻辑吗?

[复制链接]
发表于 2025-3-2 10:15 | 显示全部楼层 |阅读模式

登陆有奖并可浏览互动!

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录 手机动态码快速登录

×
react hooks 函数式组件写法虽然自由,但容易带来问题,比如组件容易过大,代码行数容易过大。业务逻辑容易堆积,不方便针对业务逻辑做单元测试,或针对ui做单测。也就是一个文件处理UI,一个文件处理业务逻辑

原文地址:https://www.zhihu.com/question/561700319
楼主热帖
回复

使用道具 举报

发表于 2025-3-2 10:16 | 显示全部楼层
先引用《架构整洁之道》里的一句话“UI 只是实现细节”。

举个例子,有一天上面要求你将业务的框架从 React 迁移至 Vue.js,或者让你从 DOM 转化为 Canvas 方案。
假如你的业务逻辑是和 React 强绑定的,那你会非常痛苦,几乎需要重写一遍。
但如果你的业务逻辑和 UI 是分离的,即 UI 层很薄,只负责将给定的 UI 状态绘制出来,那么你就可以直接复用以前的业务逻辑,业务逻辑不做任何改动,只需要重写消费 UI 的部分就可以了。

正如大家所熟知的 UI = f(state) ,React 只是 f 罢了,f 如何更换和我 state 的计算没有什么关系。

再举一个例子,你用 React 写了一个计算器,合理情况下,即使要把这个计算器改为能在 Shell 中运行,你的业务逻辑(1 + 1 = 2)也应该是不变的。

综上,这也是我为什么不太喜欢 jotai 的原因

==============================

更新:在日常开发中,怎样确认某个功能或者状态是属于 UI 还是业务逻辑呢?
只需要问自己,这个功能/状态在产品的 Shell 版 / Open API 版本中是否有必要存在就可以了。

例如说要实现“查找并高亮”这个功能,显然查找是底层的业务逻辑,因为在各个平台/版本都可以进行查找,而高亮是一个偏 UI 的事情。

所以,简单来说,这个功能从上到下应该这么划分:
UI 表现层

  • 渲染一个输入框,允许用户输入内容,并进行查找
注意这里查找是调用 props 上的 onSearch,该组件并不负责查找相关的任何逻辑,哪怕是对内容的 trim 这样的逻辑。
组件里负责的顶多是 UI 状态相关的逻辑,例如用户点击确定时,如果输入框为空,则进行闪烁提示这种纯 UI 逻辑。

  • 内容区展示高亮
实现方式是外部传递进来一个 highlight 的 props,内容区负责将其展示出来
业务逻辑层
提供查找并高亮的能力
实现方式为将内容处理(trim 等)后,调用 Use Case 层的查找能力,然后将查找结果转换为高亮信息,并通知上面的 UI 表现层。
更高级一点,查找和高亮其实也是完全可以解耦的,可以提供一个高亮模块,任何业务只需要调用就可以在内容区进行高亮。
Use Case 层
提供纯粹的查找能力,并不关心是谁发起的查找,也不关心查找后要干嘛
Model 层
提供数据模型

所以我们看到,UI 层其实只是个工具人,负责将用户的输入发送给业务内部,并将业务内部返回的结果展示给用户,这也正是最纯粹的 View。
在这种设计下,你想把 React 换成 Vue,或者 DOM 换成 Canvas 简直轻而易举,只需要改动替换 UI 表现层,基本上就是重新写下组件就好了。

====================
再更新:
由上面的 Case 想到:果然,工具人是最容易被替换的部分。。。还是像查找这样的有核心能力的 Use Case 才不容易被替换呀。。。
回复 支持 反对

使用道具 举报

发表于 2025-3-2 10:17 | 显示全部楼层
不是要不要分离的问题,而是迟早要分离。
你组件销毁hooks里的state和effect也没了,谈何分离?
因为hooks的执行和组件的渲染都绑定在一个函数里,所以你只能分离代码,不能分离逻辑。
因为你的state和effect存在fiber上而父节点的变化可能会直接丢弃子节点的fiber。
react只能做UI,用react实现业务逻辑就是自讨苦吃。
react做点动画效果写点视图组件就可以了。
但凡看过整洁架构的,只要一句话就可以讲明白:
依赖方向应该是由不稳定指向稳定,而UI是最不稳定的,如果任何逻辑一定要先让组件渲染出来,那一定是错的,没有什么值得讨论的。
很多人做的事,就好像整天吹嘘花拳绣腿的高数解题技巧,但是却连最基本的1+1=2都看不见了。
永远不要用react提供的功能实现业务逻辑。
https://michel.codes/blogs/ui-as-an-afterthought/
回复 支持 反对

使用道具 举报

发表于 2025-3-2 10:18 | 显示全部楼层
先说结论,很有必要。

React 的定位原本就是用于构建用户界面的UI库,用UI库去实现业务逻辑本身就是不合理的。
我们从业务开发的角度和hooks自身问题的角度出发,分析一下各自的问题,以及为什么需要分离,分离带来的好处又有哪些?
业务带来的问题

我们知道,业务迭代往往排山倒海压来,一开始如果不做好全局规划,或者理清各个模块的关系,是很难把控好进度,进而出现赶工导致bug滋生。那么,目前的hooks 业务组件的写法有何问题呢?
基于hooks的纯业务组件写法没有做约束,ui与业务逻辑在一个函数内部维护,面条式代码滋生,容易使组件业务逻辑代码越写越长,久而久之难以维护。很容易出现一个函数内部耦合了types,constants,各类hooks(useState,useReducer,useCallback等),以及各种function,甚至是在dom层夹杂着非常多的逻辑处理。
慢慢地,复用性也会越来越差,可能需要经常重构,抽离代码 以达到复用的程度。但往往业务的排期已经没法抽开身去维护老代码,那怎么办呢?
hooks组件的分离

我们需要在团队内部达成共识,能够产出一种固定的开发范式,能够分离代码,做到职责清晰,例如:A模块专门处理View视图组件,B模块专门处理业务逻辑,C模块专门维护ts类型types,D模块专门维护各类常量constants,E模块专门维护公用hooks逻辑,F模块专门维护css modules等。
那么,在这前提之下,我们需要实现前端UI与业务逻辑分离,目前主流的有两种方式,一种是纯逻辑抽离出去,返回函数内部方法和state;形如:
const useApp = () => {
   const [name, setName] = useState('mike');
   const getName = () => {};
   const updateName = () => {};
   return {
      name,
      getName,
      updateName
   }
}

const AppView =() => {
const { name } = useApp();
return <div>{name}</div>
}
这种方式没什么太大问题,但这种代码不内聚,没法提供通用的逻辑处理,一旦业务发生变化,就会引发多处代码的维护危机。
其次如果有很多业务团队,那么就需要考虑如何规范化统一团队内部写法,如何支持更健壮的业务代码。UI与逻辑分离并不是最终的目的,最终的目的应该是形成一套易于维护,模块职责划分清晰,能够形成固定开发模式,易于扩展,能够规范化业务使用场景,且具备强壮生命力的方案。

如果这种方式可以实现的话,那么为何很少有人会这么干呢?原因可能在于大家的函数式组件的思维。
在hooks还没诞生之前,大家普遍对于函数式组件的认知就是没有state,所以当props是固定的,那么函数式组件每次渲染结果也都是一样的,也就是相同的输入总能得到相同的输出。但现在hooks出现了,函数组件内部可以维护state了,相同的输入并不一定能得到相同的输出了。
此外,这种方式与可复用的hooks的区别又在哪里,如果两种都使用hooks维护,又如何区分呢?
另外一种方式就是保留业务逻辑,但把UI组件抽离出去,这种方式更不推荐了。有点类似子组件,父子组件通信的既视感随之袭来。
接下来,我们再来看下纯hooks组件饱受大家诟病的一些问题:
纯hooks组件的问题

1、useState 写法难用,如果有很多state,需要一个个去维护,写法不够简洁;当业务逻辑越来越复杂,往往会出现一个模块几十个useState需要维护的尴尬局面。
2、useReducer + context的全局状态难用,仍然需要定义很多action type,还需要提供provider,使用useReducer跨组件共享状态很麻烦
3、useCallback 用法不够清晰,不知何时用何时不用,用法造成困惑
4、 生命周期需要引入useEffect,需要手动管理,且不够语义化
5、基于hooks的业务组件,内部方法依然难以做到复用,应抽离出去单独维护。
6、当使用useEffect模拟mounted事件时,处理异步请求函数时很麻烦。
7、当组件达到一定复杂度的时候,堆积到一起的代码会变得越来越难以维护
8、React Hook的闭包陷阱问题
9、useState 调用updater更新后,无法同步获取最新state值
10、useState updater无法实现细粒度更新对象的属性值,不得不浅拷贝一份数据再进行覆盖
hooks-view-model

想要写出健壮的,长期可持续维护的代码,就必须去理解这些在其他编程领域通用的设计模式、原则、范式。提高代码质量,除了依赖开发自测和相关流程规范化外,也应有相关工具或统一的开发范式做约束。
对于纯写业务的人来说,没有规范去强制约定,那么约等于没有人会这么处理业务逻辑与UI的关系,最终还是会写到一起。这是hooks这种弱约束的弊端。
基于上述问题,我开发了基于react hooks的UI与业务逻辑分离的方案,内部基于useState hooks的updater 实现。可实现在class内部setState,然后在View组件中相应更新。基本解决了上述react hooks的十个“老大难”问题
hooks-view-model是一种通过拆分UI视图与业务逻辑的解决方案,可做到无需useReducer,无需redux等技术方案实现全局状态更新而不会渲染无关组件。hooks-view-model是集状态管理,变量的存储管理和数据的持久化管理于一体的解决方案。
详情点击 :
https://github.com/hawx1993/hooks-view-modelhooks-view-model 主要用于分离UI与业务逻辑,可以解决 纯hooks组件的问题,对比一下hooks-view-model的优势:
hooks组件问题hooks-view-model
useState 写法难用,如果有很多state,需要一个个去维护,写法不够简洁可通过对象形式更新与解构数据,写法简洁
useReducer + context的全局状态难用,仍然需要定义很多action type,还需要提供provider,使用useReducer跨组件共享状态很麻烦全局状态更新只需使用useGlobalStatehooks,用法简单
生命周期需要引入useEffect,需要手动管理,且不够语义化提供mounted和unmounted 钩子函数,可自动执行,语义化友好
基于hooks的业务组件,内部方法依然难以做到复用,应抽离出去单独维护class 写法可通过继承 实现复用,还可以通过useVM引入其他viewModel进行复用,复用性高
当接收新的props,需要手动使用useEffect观察props变化,没有直接的钩子可以自动触发class 提供onPropsChanged 钩子函数,可自动触发执行
当组件达到一定复杂度的时候,堆积到一起的代码会变得越来越难以维护UI与逻辑做到了很好的分离,代码组织性强
React Hook的闭包陷阱问题由于方法都提到class中去维护了,所以不存在此问题
useState 调用updater更新后,无法同步获取最新state值可通过调用getCurrentState 同步获取最新值
调用updater无法实现细粒度更新对象属性值,需浅拷贝对象后覆盖可通过updateImmerState实现细粒度更新
写法如下:
// AppView.tsx
import { AppViewModel } from './AppViewModel'
import { useVM } from 'hooks-view-model'
import { usePrevious } from '@/hooks';

const AppView = () => {
  const { perviousAddress } = usePrevious();
  const { changeAddress, useCurrentState } = useVM(AppViewModel, {
    address: perviousAddress,
  })
  const { address = 'ZheJiang Province' } = useCurrentState()
  return (
    <div>
      <button onClick={changeAddress}>click to change address</button>
      <span>{address}</span>
    </div>
  )
}
数据逻辑处理:
// AppViewModel.ts
import  StoreViewModel from 'hooks-view-model'

class AppViewModel extends StoreViewModel {
  changeAddress = () => {
    this.updateCurrentState(this.props.address);// 相当于setState
  }
}
export { AppViewModel }
那么可能有很多人就疑惑了,明明react官方已经推崇函数式写法了,为什么还要用class?
基于class的viewModel写法与hooks有什么区别

诚然,hooks 可满足UI与逻辑分离的需求,但抽离无法被公用的业务逻辑到hooks中是否有必要?与可复用的hooks 是否容易造成混淆?hooks存在的useCallback,useReducer,以及对副作用的使用等容易造成使用困惑的,以及对useState 使用上的麻烦是否可以有其他方法简化?
其次,函数式组件的写法也并非函数式编程,相同的输入(props)并不会得到相同的输出(内部的state或全局的state都可能对结果产生影响)。
而业务逻辑抽离到class中,依然是函数式组件。class相比于function 天然的具有可组织性,可扩展性(extends),和可维护性。
首先,业务逻辑是比较复杂的,Class 具备继承能力,可实现viewModel与view都获得来自父类的能力;
其次,class 能够更好维护业务逻辑代码,在class中写业务逻辑,完全可以忽视react hooks自带的各种hooks,诸如useRef,useCallback,useReducer,useState等,写起业务逻辑来更加纯粹;
再者,hooks 也可以与viewModel共存,只需要在view中引入hooks,然后将返回值作为props,通过useVM传给viewModel即可,两者是共存的,并不是互斥的。
基于class的viewModel可以更好的维护业务逻辑代码,可以使用装饰器,public,private等关键字,显示提高代码可维护性和扩展能力。而可复用的hooks可以用来抽象业务逻辑实现副作用观察和逻辑复用,两者具有不同的心智模型。
配置生成项目模板文件

此外,我还在hooks-view-model内置了项目的模板文件,可一键生成所需模板文件和代码,这样便可以让各个业务线的前端团队始终保持一致的开发规范和风格。
可以真正做到成员B可以低成本介入项目A中,提高代码的可维护性,可阅读性。用法如下:
执行如下步骤,可一键生成模板文件
1、添加脚本命令
在package.json的scripts中添加如下脚本命令
scripts: {
  "generate": "plop --plopfile ./node_modules/hooks-view-model/generators/index.js"
}2、根目录创建template.config.js
指明模板需要生成的相对路径地址:
const dir_to_generate = './src/pages/';

module.exports = dir_to_generate;执行完后,便会在指定的目录下生成如下模板文件:



更好的debug能力支持

使用hooks,我们如果想知道当前的state值,我们需要一个个console出来,而基于hooks-view-model,我们只需要在控制台输入:globalStore,即可查看所有view对应的state,通过key区分。可大大提升debug能力。


结尾

俗话说:好的代码和差的代码都能运行,但我们会追求好的代码,获得更好的维护性和可读性。
没有架构的系统也能工作,但如果一个业务团队没有好的架构,整个团队将陷入混乱,最终难以支撑业务快速变化。
同样,好的解耦可以允许非常快速的 UI 迭代,也可以方便我们更自由地重构 UI,且在大多数应用程序中,UI 的开发速度远高于实际的业务逻辑。
无论何种方式,请大胆开始你的分离工作吧!
回复 支持 反对

使用道具 举报

发表于 2025-3-2 10:18 | 显示全部楼层
做好了「被喷」的准备,我来贡献一个「文不对题」的回答。我们不妨先把题干中的「React hooks」这个主语剥离,从「 ui」和「逻辑」这两个关键词切入分析,结合近几年前端流行的 headless ui,从更大的背景下,来重新思考这个问题。
<hr/>UI 和逻辑

就拿知乎-创作中心举例,我们看几个页面截图:
创作中心-潜力问题


创作中心-近期热点


创作中心-瓦力保镖


你有没有觉得这些页面都「长得很像」?
我们当然可以将 table / list 抽象,用类似 scheme 驱动的方式终结各个页面烟囱式实现的状况,最大化提升开发效率。但是,你再看看下面这个截图:
创作中心-热点问题模块


哎呦,长得好像不一样了。前端开发同学应该深有体会,阻碍表单 / 列表类统一的一个原因是各场景样式、ui 都有差异。在设计层面,可能由于业务需求,很难做到统一,不得已自带设计体系;另一方面,后端数据模型没有统一,因此对接数据前后涉及到的前端状态管理、数据流转也未能统一,复用性大打折扣。
而躲在这些需求的背后,本质都是是同一套数据模型的渲染。
现实情况可能却是,ui 层面离领域模型很远,上图中举例到的知乎创作中心场景,产品经理可能某天突然想展示某些数据,突然想加入 「海盐计划」 4.0,「灯塔计划」,「致知计划」等明细,从 ui 上很容易把数据模型拆解的七零八碎,后继开发者很难从页面看出其背后的领域和数据模型。前端从此和产品、业务渐行渐远。由于我们对于领域知识理解严重匮乏,逐步沦为切图和填充数据字段的工具人,而真正能够进行统一抽象的数据模型,状态管理机制,副作用处理没有得倒应有的沉淀;后端同学面对着分散各自领域的存储表,也很难有端到端渲染层面的数据模型概念。
<hr/>Headless to resuce

我们先对 headless ui 下一个定义:
Headless 组件即无 ui 组件,表示仅提供 UI 元素和交互的数据状态逻辑、状态、流转处理和API,但不提供标记、样式或预构建实现(或另外实现)。
事实上,构建复杂 ui 最难的部分通常围绕状态,事件,副作用,数据的计算和管理。通过将标记,样式和实现细节先剔除,我们的逻辑和组件可以更模块化和重复使用。这样带来的好处是业务有极大的 ui 自定义空间,而对框架来说,只考虑逻辑可以让自己更轻松的覆盖更多场景,满足更多开发者不同的诉求。
Headless ui 是如何解决问题的呢?简单说,headless ui 组件开发者需要关注逻辑节点而非 UI 节,重点工作包括设计并实现 combinators/primitives, data-processing, state-management, business logic。这样一来,开发者从日常的「实现 markup, styles and implementation details」工作中解放出来,最终目标是提供更加 modular and reusable 的组件。
分工和职能上,根据团队可能会分化为基础能力前端研发和 UI 侧前端研发来说,对应:

  • 基础能力前端研发只需要聚焦广义的 functional features,这里面包括了数据流设计等
  • ui 侧前端研发具备完全的组件 appearance(any style) 控制权,更贴合业务需求
这样两种类型的前端开发者(业务 ui 敏感 & 模型抽象敏感),都有各自的主战场和技能发挥舞台。
Headless ui 和 hooks

真正理解 React hooks 的同学,可能会联想到 React Hooks 是 Headless ui 在 React 世界中得以实现的基石。从面试八股文中,我们就知道 hooks 的好处是可以自定义 hooks,将业务逻辑封装在自定义的 hooks 里,然后将状态和控制器导出给 ui 使用,大大减少了 ui 里的模板代码,并且有效分离了 ui 定义和业务定义、层次分明。(from Headless UI 和 hooks 的一点思考
不败花丶:全新的 React 组件设计理念 Headless UI 文章中,作者认为「正是因为 React Hooks 的诞生,使 Headless ui 组件在技术上成为可能,这也是它为什么最近才开始流行的原因。」
我们不妨来看一个有趣的例子(headless-user-interface-components):


业务侧需求:在抛硬币实现猜正反的场景中,实现一个相关硬币组件(先不描述的太具体,这里只做一个例子)。

  • 实现原型
// 第一版实现
const CoinFlipV0 = () => Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;

  • 开发阶段实现样式 mockup
// 实现样式
const CoinFlipV1 = () => Math.random() < 0.5
  ? (<div><img src={HeadsCoin} alt="Heads" /></div>)
  : (<div><img src={TailsCoin} alt="Tails" /></div>);

  • 业务需求变更 / 扩大 1:一个新场景需要在页面上使用 label 显示抛硬币结果。对应实现上,组件代码需要加入 showLabels 作为组件参数
export const CoinFlipV2 = ({ showLabels = false }) => Math.random() < 0.5
  ? (<div><img src={HeadsCoin} alt="Heads" />
        {showLabels && <span>Heads</span>}
    </div>)
  : (<div><img src={TailsCoin} alt="Tails" />
        {showLabels && <span>Tail</span>}
    </div>);

  • 业务需求变更 / 扩大 2:一个新场景需要设计有一个按钮,来实现执行「重新抛硬币」的能力。对应组件实现上,我们只好加入一个 showButton 作为组件参数。注意,这时候已经是该组件的第三版迭代了
export const CoinFlipV3 = ({ showLabels = false, , showButton = false }) => {
  const [state, setState] = React.useState(flip());
  const handleClick = () => setState(flip());

  return (
    <>
      {showButton && <button onClick={handleClick}>Reflip</button>}
      {state.flipResults < 0.5 ? (
        <div>
          <img src={HeadsCoin} alt="Heads" />
          {showLabels && <span>Heads</span>}
        </div>
      ) : (   
        <div>
          <img src={TailsCoin} alt="Tails" />
          {showLabels && <span>Tails</span>}
        </div>
      )}
    </>
  );
}
在 headless 的设计理念下,我们将组件的逻辑 logic 和样式 & UI 部分接耦(这里还不涉及到数据交互,更多的工程场景还会涉及到的相关 API 数据处理等)。

  • 实现纯 logic 层组件,样式 & ui 部分通过渲染业务自定义的 children 的方式实现:
export const CoinFlipLogic = ({ children }) => {
  const [state, setState] = React.useState(flip());

  const handleClick = () => setState(flip());

  return children({
    rerun: handleClick,
    isHeads: state.flipResults < 0.5,
  });
};
注意看,我们的实现中只关注硬币正反面 state 以及相关重置状态按钮的响应事件,并通过 isHeads 和 rerun 两个「能力」提供给业务使用。
那么相关业务使用场景(严格来说这还不是最终的业务实现层,而是介于业务和基础实现的中间层,即 base 上层的 biz 能力)对应的代码如下:
export const CoinFlipV4 = ({ showLabels = false, showButton = false }) => (
  <CoinFlipLogic>
    {({ rerun, isHeads }) => (
      <>
        {showButton && <button onClick={rerun}>Reflip</button>}
        {isHeads ? (
          <div>
            <img src={HeadsCoin} alt="Heads" />
            {showLabels && <span>Heads</span>}
          </div>
        ) : (
          <div>
            <img src={TailsCoin} alt="Tails" />
            {showLabels && <span>Tails</span>}
          </div>
        )}
      </>
    )}
  </CoinFlipLogic>
);
代码似乎变多了,但是逻辑更清晰了,分工更彻底了,对应业务能力更强了。

  • 进一步优化,甚至我们可以将 isHeads 等逻辑拆的更干净,此时是一个 Probability 组件:
const run = () => ({
  random: Math.random(),
});

export const Probability = ({ children, threshold }) => {
  const [state, setState] = React.useState(run());

  const handleChange = () => setState(run());

  return children({
    rerun: handleChange,

    result: state.random < threshold,
  });
};
使用方式:
export const RollDiceV1 = () => (
  <Probability threshold={1 / 6}>
    {({ rerun, result }) => (
      <div>
        <span onMouseOver={rerun}>Roll the dice!</span>
        {result ? (
          <div>Big winner!</div>
        ) : (
          <div>You win some, you lose most.</div>
        )}
      </div>
    )}
  </Probability>
);
注意,这时候 interface 完全变更了(threshold, result),能够响应的事件也扩大了(这时候 rerun 的触发逻辑是鼠标划过事件)。
这个时候,(分离了 ui 和业务逻辑的)React hooks 该出场了,结合 React hooks,上面的这个例子可以得到非常天然的落地实现。如下:
export const useProbability = ({ threshold }) => {
  const [state, setState] = React.useState(run());

  const handleChange = () => setState(run());

  return {
    rerun: handleChange,
    result: state.random < threshold,
  };
};

export const RollDiceV2 = () => {
  const { rerun, result } = useProbability({threshold: 1 / 6});

  <div>
    <span onMouseOver={rerun}>Roll the dice!</span>
    {result ? <div>Big winner!</div> : <div>You win some, you lose most.</div>}
  </div>;
};
Headless ui 和 React hooks 背后的架构思想变革

如果你理解了上面场景想要表达的 headless 概念,那么你可能对 React 全面推进 hooks feature 革命的背后逻辑也会更加清楚。
React hooks,headless ui 理念轰轰烈烈推进背后是 ui library 向  -> logic utility 的转型或分化,都是我们更优雅地前端组件级 biz, base 合理分层的武器。
而反观像 antd 这种 ui/component library 大而全的武器,毫无疑问能帮助我们迅速开发,完成从零到一的建设,也能支撑迅速迭代的需求。但总有一天,随着业务的发展,逻辑的膨胀,前端在组件的架构上总会出现明显的需要升级的暗号,这些暗号就藏在我们上述开发场景的页面截图背后,藏在重复代码的背后。
除此之外,更多需要考虑 headless ui 的一些表象:系统难以维护;系统变得更加脆弱,开发人员不知道在进行变更的时候会破坏其他功能,或者需要承担较多的心智负担来理解消化;要从一个技术栈转移到另一个技术栈时不得不对整个系统进行修改;当多个团队在同一个代码库中进行交付时,会造成代码库的混乱,我们难以保持应用处于最新状态,难以升级版本;在没有团队负责维护和改进的情况下,有些代码会被遗弃,团队之间没有明确的责任;
纵观社区上的大部分现存巨型组件库,往往都不先在内部抽象数据和逻辑模型,同一个组件库中,不同 ui 组件实现却完全异构。我们错误地执迷于描述组件可以做什么,从大的块里取东西;而不是描述自己长什么样,如何从小的组成大的。Modular and reusable composable 成为永远飘着空中难以落地的口号。
我们发现,antd 中 #issues/5910:对于重型组件的基于功能粒度的拆分能力 引起了广泛热议,甚至 antd 组件 #5910 很难和一个时间处理的 moment utility 剥离
记得 @徐飞 讲个一个有趣的观察:
前端工程师当看待一个服务接口的时候,如果它提供了几十个参数,并且不分类,不分层,你大概率觉得那是一个很糟糕的东西,绝对不足以成为一个可复用体系的基础。但是当你看见一个几十个 props 也不分类不分组的前端组件,大部分却觉得组件化就是这样,它应该是很好的底层库。
headless ui 和 hooks 落地和开源社区方案

既然已经扯远这么多,那我们就继续聊一下 headless ui(或者在 React scope 下,hooks)的开源和落地情况吧。往大了说,headless ui 涉及到前端团队职能的分化,涉及到前端组件方案的颠覆。往小了说,落地上,我们可以找到一个垂直场景,借助开源社区已有能力的基础,实现 atomic headless ui(开源提供) -> biz headless ui(我们封装) 的设计。
目前社区 headless ui 开源方案主要是更 low level 组件的封装,工程上,我们可以基于良好封装的 low level headless ui 开源库,实现业务关联(往往是数据获取和处理)的 biz headless ui。这个过程,需要后端数据建模抽象,需要前端无缝对接。事实上,前后端一体的渲染层 <-适配层 <-模型层 <-存储层都需要默默提升。一个 LCDP(Low-Code Development Platform)的完成度,依赖每一层的进一步进展。更长远和更理想的这个话题我们姑且不表,先回到前端层面开源社区方案。

  • headlessui.com: Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
  • React Table Library: Build and design powerful datagrid experiences while retaining 100% control over markup and styles.

    • 典型的 lightweight table libraries(虽然他的作者认为是界于 heavyweight 和 lightweight 之间,但其实更偏向 lightweight)
    • 兼容更现代化的 composition over configuration, customization, extensibility, and SSR 理念。
    • 方便进行扩展,方便我们和后端模型相结合

  • tanstack.com/table: Headless, type-safe, powerful utilities for complex workflows like Data Management, Data Visualization, Charts, Tables, and UI Components

  • downshift-js.comPrimitives to build simple, flexible, WAI-ARI compliant React autocomplete/combobox or select dropdown components.
  • radix-ui.comUnstyled, accessible components for building high‑quality design systems and web apps in React.
  • react-spectrum.adobe.com/react-aria:A library of React Hooks that provides accessible UI primitives for your design system.
  • 抖音前端 semi.design

    • 国内大厂的一个实现,Semi 从未尝试约束用户,固化所谓的「统一规范」,而是在默认基础上,充分进行模块化解耦,并开放自定义能力,方便用户进行二次裁剪与定制,搭建适用于不同形态产品的前端资产

总结

React hooks 当然有必要分离 ui 和 logic,这是刻在 React hooks 基因里的要义,也是 headless ui 理念的落地基石。我们从一个问题中,以小见大,聊了前端分工,前端边界,前端趋势和发展,但这不是一篇「阿里味」的恢弘叙事长文,在开喷前,不妨静下心来读一下,尝试感受回答中的诚意。
最后,headless ui 不是孤立存在的:向前(卷设计、卷业务),我们可以通过 https://tailwindcss.com/  这样的平台协助解决样式管理方案、定制样式或设计体系,做到系统对接,体系建设(常见于中小公司);向后(卷后端),我们可以通过统一数据建模,对接渲染抽象能力,更高效地实现前后端一体的 LCDP(Low-Code Development Platform) 页面搭建等需求。卷前端卷我们自己,在 headless ui 基础上,把数据层面(数据驱动)、adapter 适配做到极致,找到前端 ui 和逻辑资产沉淀的空间,我们的想象力、我们的目光本可以更长远。
回复 支持 反对

使用道具 举报

发表于 2025-3-2 10:19 | 显示全部楼层
React Hooks 实际就是一堆函数,你该抽离就抽离,该限制副作用就限制副作用。
不要认为所有 Hooks 都只能写在组件函数里面,可以抽出去写的,然后你就会发现不会有什么过大、行数过多、逻辑堆积、不能做单元测试的问题了。
组件函数里应该尽可能只有 UI 逻辑,只写一下 JSX 和做一些生命周期的调用工作(仅仅是调用,不要把实现写在这里,除非只有三两行),具体的业务应该通过外界的函数来实现。
Hooks 也可以进行抽象,比如可以看看 ahooks,用不用无所谓,可以看看源码学习一下。
回复 支持 反对

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册 微信登录 手机动态码快速登录

本版积分规则

关闭

官方推荐 上一条 /3 下一条

快速回复 返回列表 客服中心 搜索 官方QQ群 洽谈合作
快速回复返回顶部 返回列表