当前位置: 首页 > news >正文

组件复用的 3 种高级方案:抽离逻辑 vs 封装组件 vs 自定义 hooks

组件复用的 3 种高级方案:抽离逻辑 vs 封装组件 vs 自定义 hooks

组件复用不仅是“把代码复用起来”,更重要的是选择合适的抽象边界,让未来的修改成本更低、协作更顺畅。本文从三种常见且互补的路径展开:抽离逻辑(跨 UI 复用)、封装组件(统一视觉与交互)、自定义 hooks(复用有状态逻辑),并给出可直接拿来用的示例与选型建议。

快速地图

  • 抽离逻辑:将算法/业务规则独立为纯函数或服务模块,跨页面与框架复用。
  • 封装组件:把稳定 UI + 交互协议封装成组件,对外暴露安全的 Props 接口。
  • 自定义 hooks:抽离状态与副作用的“可组合逻辑”,在 React 组件中重用。

方案一:抽离逻辑(跨 UI 的通用复用)

适用场景

  • 业务规则稳定但展示形式多样(表单、列表、图表均需同样的计算/校验/权限判断)。
  • 希望被多端(Web、Node、React Native)或不同框架共同复用。

实践要点

  • 纯函数优先:输入→输出明确,避免隐藏副作用。
  • 明确契约:单位、范围、边界、错误抛出策略要清楚。
  • 可组合:提供小而稳的工具函数,复杂逻辑通过组合完成。

示例:分页与排序的通用工具

// logic/paginate.ts
export function paginate<T>(list: T[], pageSize: number, page: number): T[] {if (pageSize <= 0 || page < 1) return [];const start = (page - 1) * pageSize;return list.slice(start, start + pageSize);
}// logic/sortBy.ts
export function sortBy<T>(list: T[], key: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {const factor = order === 'asc' ? 1 : -1;return [...list].sort((a, b) => {const av = a[key] as unknown as number | string;const bv = b[key] as unknown as number | string;if (av === bv) return 0;return (av > bv ? 1 : -1) * factor;});
}

在组件中调用

import { paginate } from './logic/paginate';
import { sortBy } from './logic/sortBy';export function UserTable({ data }: { data: Array<{ name: string; age: number }> }) {const sorted = sortBy(data, 'age', 'asc');const page1 = paginate(sorted, 10, 1);return (<ul>{page1.map((u) => (<li key={u.name}>{u.name} - {u.age}</li>))}</ul>);
}

方案二:封装组件(统一视觉与交互协议)

适用场景

  • UI 结构与交互较为稳定,需在多个页面一致呈现(如 Modal、Select、Table)。
  • 团队希望通过“基座组件”统一规范与可访问性(a11y)。

实践要点

  • 受控/非受控双模式:同时支持 open + onOpenChangedefaultOpen
  • 细粒度插槽:通过 children 或 render props 暴露可插入区域。
  • 可扩展:合理的 props 设计(classNamestyleas)与 forwardRef

示例:可受控的 Modal 组件

import { useState, useEffect, forwardRef } from 'react';type ModalProps = {open?: boolean;defaultOpen?: boolean;onOpenChange?: (next: boolean) => void;title?: string;children?: React.ReactNode;footer?: React.ReactNode;className?: string;
};export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal({ open, defaultOpen, onOpenChange, title, children, footer, className },ref
) {const [innerOpen, setInnerOpen] = useState(Boolean(defaultOpen));const isControlled = typeof open === 'boolean';const visible = isControlled ? open! : innerOpen;useEffect(() => {if (!isControlled && typeof open === 'boolean') setInnerOpen(open);}, [open, isControlled]);const setOpen = (next: boolean) => {if (!isControlled) setInnerOpen(next);onOpenChange?.(next);};if (!visible) return null;return (<div ref={ref} role="dialog" aria-modal="true" className={className}>{title && <div>{title}</div>}<div>{children}</div><div>{footer ?? <button onClick={() => setOpen(false)}>关闭</button>}</div></div>);
});

使用示例

// 受控模式
function Page() {const [open, setOpen] = useState(false);return (<><button onClick={() => setOpen(true)}>打开</button><Modal open={open} onOpenChange={setOpen} title="标题">内容……</Modal></>);
}// 非受控模式
function PageUncontrolled() {return (<><Modal defaultOpen title="欢迎">初次打开显示</Modal></>);
}

方案三:自定义 hooks(复用有状态逻辑)

适用场景

  • 多个组件共享同样的状态管理与副作用(请求、滚动、节流/防抖、订阅)。
  • 逻辑以“组合”方式更灵活复用,而不强制 UI 结构。

实践要点

  • 输入输出明确:参数、返回值(state、函数)清晰。
  • 稳定引用:返回的函数使用 useCallbackuseRef 保持稳定。
  • 清理副作用:订阅、计时器、网络请求要在卸载时清理或取消。

示例 A:useDebouncedValue(防抖值)

import { useEffect, useState } from 'react';export function useDebouncedValue<T>(value: T, delay = 300): T {const [debounced, setDebounced] = useState(value);useEffect(() => {const t = setTimeout(() => setDebounced(value), delay);return () => clearTimeout(t);}, [value, delay]);return debounced;
}

示例 B:useRequest(带取消与重试)

import { useEffect, useRef, useState, useCallback } from 'react';type RequestState<T> = { loading: boolean; data?: T; error?: unknown };export function useRequest<T>(fn: (signal: AbortSignal) => Promise<T>,options?: { retries?: number; baseDelay?: number; immediate?: boolean }
) {const [state, setState] = useState<RequestState<T>>({ loading: false });const controllerRef = useRef<AbortController | null>(null);const retries = options?.retries ?? 2;const base = options?.baseDelay ?? 200;const run = useCallback(async () => {controllerRef.current?.abort();controllerRef.current = new AbortController();setState({ loading: true });for (let i = 0; i <= retries; i++) {try {const data = await fn(controllerRef.current.signal);setState({ loading: false, data });return;} catch (e) {if (controllerRef.current.signal.aborted) return;if (i === retries) {setState({ loading: false, error: e });return;}const jitter = Math.random() * 0.2 + 0.9;const delay = Math.round(base * Math.pow(2, i) * jitter);await new Promise((r) => setTimeout(r, delay));}}}, [fn, retries, base]);useEffect(() => {if (options?.immediate) run();return () => controllerRef.current?.abort();}, [options?.immediate, run]);return { ...state, run, cancel: () => controllerRef.current?.abort() };
}

使用示例

function SearchBox() {const [q, setQ] = useState('');const dq = useDebouncedValue(q, 300);const { data, loading, error } = useRequest<string[]>(async (signal) => {const res = await fetch(`/api/search?q=${encodeURIComponent(dq)}`, { signal });if (!res.ok) throw new Error('failed');return res.json();},{ immediate: true });return (<div><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="搜索…" />{loading && <span>加载中…</span>}{error && <span>出错</span>}<ul>{data?.map((x) => <li key={x}>{x}</li>)}</ul></div>);
}

如何选:简单决策树

  • 只复用“计算/规则/格式化”,且与 UI 无关 → 抽离逻辑。
  • 需要统一 UI 与交互协议,页面直接复用整体 → 封装组件。
  • 需要复用“有状态逻辑 + 副作用”,但保留 UI 灵活性 → 自定义 hooks。

进一步判断

  • 复用面广、跨技术栈 → 抽离逻辑优先。
  • 设计系统/风格统一要求强 → 封装组件优先。
  • 多种组件组合同一数据流 → hooks 优先。

常见误区与规避

  • 过度封装:把不稳定的需求封成“大组件”,导致改动牵一发而动全身。
  • hooks 泄漏业务:把具体接口路径、环境变量硬编码在 hooks 内,降低可移植性。
  • 边界不清:未定义受控/非受控、错误处理、取消策略,使用者难以正确集成。
  • 不可测试:逻辑混在 UI 中,单测无法覆盖关键路径。

工程实践建议

  • 分层结构:logic(纯函数) → hooks(状态逻辑) → components(UI)
  • 类型友好:通过 TypeScript 泛型与可选参数增强复用性。
  • 文档与示例:为封装组件与 hooks 保留最小示例(Story/MDX),降低接入成本。
  • 监控与可视化:对 hooks 的请求与重试打点,便于定位性能与稳定性问题。

小结

抽离逻辑、封装组件、自定义 hooks 是互补的三条路。正确的复用,不是把代码“堆在一起”,而是在合适的层级建立可持续的抽象边界。先把“复用的东西是什么”说清楚,再决定用哪种方案实现,才能让未来的你与同事受益。

http://www.dtcms.com/a/603197.html

相关文章:

  • 宣传的网站开发需要多少钱怎样把一个网站建设的更好
  • 廊坊建设网站的公司公司名称大全简单
  • 丽江网站建设公司玉林网站优化
  • LIN总线基础讲解之四—LIN总线的网络休眠唤醒机制
  • 如何在线实现手机版网站建设企业网站怎样优化
  • 哪里建网站好品牌建设成效
  • 域名交易网站建设外发加工网订货会
  • 编译语言的回溯 | 探索回溯机制在编译器设计中的应用和重要性
  • 网络服务网站建设做网站建设公司怎么样
  • 网站建设与 宣传关系做公司官方网站
  • 正邦的网站建设金华建设局网站
  • 天河建网站上海网站推广费用
  • 把树莓派镜像安装到新的内存卡内,以及怎么扩展内存 ?
  • Api创建云主机文档
  • 小门户网站模板景德镇做网站哪家好
  • 网站域名空间合同wordpress 缩略图优化
  • app介绍网站模板免费下载泰安网站建设价格
  • MinGW-w64安装下载实现c、c++的编译
  • 实操解决Navicat连接postgresql时出现‘datlastsysoid does not exist‘报错的问题
  • 湖北专业的网站制作代理商seo推广优化
  • 博客网站开发背景及意义网站建设课设总结
  • 企业建设网站流程wordpress搬家404
  • 网站主机方案网站设计的企业
  • GCC编译C语言:理解其工作原理与优化技巧
  • 建站极速通国家建设规范网站
  • 【2025最新】05 Spring Security 构建 RESTful Web 注册服务 - 1
  • 收到网站打入0.1元怎么做分录素材库网站
  • 岳阳市规划局建设工程公示网站wordpress指定页面连接数据库连接
  • 输入输出的本质——cin、cout背后的流缓冲机制
  • 为什么要给大夫做网站网站内容管理系统(cms)