Hooks
HeroUI Hooks 从 v2 到 v3 的迁移指南
完整的 API 参考请参阅 v3 组件文档。本指南重点介绍如何从 HeroUI v2 迁移 Hooks。
概述
HeroUI v3 移除了 v2 中存在的大多数组件 Hooks,转而使用复合组件,并新增了一个用于管理浮层状态的 Hook。本指南涵盖:
- 组件 Hooks 的移除(
useSwitch、useInput、useCheckbox等) useDisclosure→useOverlayState的迁移- 迁移策略与示例
组件 Hooks 的移除
HeroUI v2 提供了一系列组件 Hooks(例如 useSwitch、useInput、useCheckbox 等),它们返回一组 prop getter(getBaseProps、getWrapperProps、getThumbProps 等),让用户在无法直接修改内部子组件的情况下也能自定义组件结构。HeroUI v3 通过复合组件解决了这个问题,从而无需再依赖这些 Hooks。
为什么 v2 中存在这些 Hooks
在 v2 中,组件具有固定的内部结构。为了自定义这些结构,用户需要使用提供 prop getter 的 Hooks。例如,useSwitch 返回 getBaseProps()、getWrapperProps()、getThumbProps() 等,用户可以将其展开到自定义元素上,从而构建自己的 Switch 结构。
v3 的解决方案:复合组件
v3 采用了复合组件模式,让你可以直接访问组件的各个部分。你不再需要使用带有 prop getter 的 Hooks,而是可以直接通过 Switch.Control、Switch.Thumb、Checkbox.Control、Checkbox.Indicator 等子组件进行组合。
迁移策略
- 识别 Hook 用法:在你的代码库中搜索来自
@lenso/ui的导入,找出包含 Hook 名称(useSwitch、useInput、useCheckbox、useRadio等)的引用。 - 替换为复合组件:使用复合组件模式,替代带有 prop getter 的 Hooks。
- 保留原有结构:迁移时,尽量保持与原 Hook 实现一致的组件结构。例如:
- 如果你之前用
useSwitch创建了一个 不带 thumb 的 Switch,那么在 v3 中也不要添加Switch.Thumb - 如果你之前用
useCheckbox创建了一个 不带 indicator 的 Checkbox,那么在 v3 中也不要添加Checkbox.Indicator - 只引入原本基于 Hook 的实现中实际用到的子组件
- 如果你之前用
- 参考各组件指南:查看各个组件的迁移指南,获取具体示例。
主要差异
- v2:Hooks 提供 prop getter,用于自定义固定的组件结构
- v3:复合组件允许直接组合组件的各个部分
保留结构示例
v2:不带 thumb 的 Switch
import { useSwitch } from "@lenso/ui";
function CustomSwitch() { const { getBaseProps } = useSwitch(); return ( <div {...getBaseProps()}> {/* No thumb element */} </div> );}v3:等效结构
import { Switch } from "@lenso/ui";
function CustomSwitch() { return ( <Switch.Control> {/* No Switch.Thumb - preserving the original structure */} </Switch.Control> );}有关具体组件的详细迁移示例,请参阅各个组件的迁移指南。
useDisclosure → useOverlayState
v2 中的 useDisclosure 钩子在 v3 中已替换为 useOverlayState。该钩子用于管理 Modal、Popover 等浮层组件的打开/关闭状态。
v2:useDisclosure
API:
const {isOpen, onOpen, onClose, onOpenChange, isControlled, getButtonProps, getDisclosureProps} = useDisclosure({ isOpen?: boolean; defaultOpen?: boolean; onClose?(): void; onOpen?(): void; onChange?(isOpen: boolean | undefined): void; id?: string;});示例:
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure } from "@lenso/ui";
export default function App() { const {isOpen, onOpen, onOpenChange} = useDisclosure();
return ( <> <Button onPress={onOpen}>Open Modal</Button> <Modal isOpen={isOpen} onOpenChange={onOpenChange}> <ModalContent> <ModalHeader>Title</ModalHeader> <ModalBody>Content</ModalBody> <ModalFooter> <Button onPress={onOpenChange}>Close</Button> </ModalFooter> </ModalContent> </Modal> </> );}v3:useOverlayState
API:
const state = useOverlayState({ isOpen?: boolean; defaultOpen?: boolean; onOpenChange?: (isOpen: boolean) => void;});
// Returns:// {// isOpen: boolean;// open(): void;// close(): void;// toggle(): void;// setOpen(isOpen: boolean): void;// }示例:
import { Modal, Button, useOverlayState } from "@lenso/ui";
export default function App() { const state = useOverlayState();
return ( <Modal> <Button onPress={state.open}>Open Modal</Button> <Modal.Container isOpen={state.isOpen} onOpenChange={state.setOpen}> <Modal.Dialog> {({close}) => ( <> <Modal.Header> <Modal.Heading>Title</Modal.Heading> </Modal.Header> <Modal.Body>Content</Modal.Body> <Modal.Footer> <Button onPress={close}>Close</Button> </Modal.Footer> </> )} </Modal.Dialog> </Modal.Container> </Modal> );}迁移指南
基本迁移
v2:
const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure();v3:
const state = useOverlayState();// Use state.open(), state.close(), state.toggle(), state.setOpen(boolean)受控状态
v2:
const {isOpen, onOpenChange} = useDisclosure({ isOpen: controlledIsOpen, onChange: (isOpen) => setControlledIsOpen(isOpen)});v3:
const state = useOverlayState({ isOpen: controlledIsOpen, onOpenChange: setControlledIsOpen});非受控状态
v2:
const {isOpen, onOpen, onClose} = useDisclosure({ defaultOpen: false});v3:
const state = useOverlayState({ defaultOpen: false});// Use state.open(), state.close(), state.toggle()API 差异
| v2(useDisclosure) | v3(useOverlayState) | 说明 |
|---|---|---|
isOpen | isOpen | 相同 |
onOpen() | open() | 方法重命名 |
onClose() | close() | 方法重命名 |
onOpenChange() | toggle() | 新增的切换方法 |
onOpenChange(prop) | setOpen(boolean) | API 不同 |
isControlled | - | 已移除(由内部处理) |
getButtonProps() | - | 已移除(改用复合组件) |
getDisclosureProps() | - | 已移除(改用复合组件) |
useOverlayState 的优势
- 更简洁的 API:使用专用方法(
open()、close()、toggle()),而非回调 - 更简单的状态管理:在受控与非受控两种模式下都能无缝工作
- 更完善的 TypeScript 支持:改进了类型推断和自动补全
- 与 React Aria 保持一致:与 React Aria Components 的模式相符
替代方案:useState
对于简单的场景,你也可以直接使用 React 的 useState:
import { useState } from "react";import { Modal, Button } from "@lenso/ui";
export default function App() { const [isOpen, setIsOpen] = useState(false);
return ( <Modal> <Button onPress={() => setIsOpen(true)}>Open</Button> <Modal.Container isOpen={isOpen} onOpenChange={setIsOpen}> <Modal.Dialog> {/* content */} </Modal.Dialog> </Modal.Container> </Modal> );}不过,useOverlayState 为常见操作提供了更简洁的 API 和专用方法。
已移除的 Hooks
v2 中的以下 Hooks 在 v3 中已被移除:
- useDraggable:已移除
- useClipboard:已移除
- usePagination:已移除
- useToast:已移除
总结
- 组件 Hooks(
useSwitch、useInput等)→ 改用 复合组件 - useDisclosure → 改用 useOverlayState 来管理浮层状态
- useOverlayState 提供了更简洁的 API,包含
open()、close()、toggle()与setOpen()方法 - 已移除的 Hooks:
useDraggable、useClipboard、usePagination、useToast不再可用 - 对于简单的场景可以直接使用
useState,但useOverlayState在使用体验上更佳
有关具体组件的 Hooks 迁移示例,请参阅各个组件的迁移指南。