Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"dependencies": {
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.2.0",
"@rc-component/util": "^1.3.0",
"@rc-component/motion": "^1.1.3",
"classnames": "2.x",
"@rc-component/dropdown": "~1.0.0",
Expand Down
14 changes: 6 additions & 8 deletions src/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Accessibility https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role
import classNames from 'classnames';
import useMergedState from '@rc-component/util/lib/hooks/useMergedState';
import useControlledState from '@rc-component/util/lib/hooks/useControlledState';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

切换到 useControlledState:导入路径与语义检查

导入路径看起来与项目现有 util 用法一致(同为 /lib/ 前缀)。请确认该导入在 CJS/ESM 打包下都可用,且 useControlledState 的受控判断与 useMergedState 一致,避免受控/非受控行为改变。


🌐 Web query:

@rc-component/util/lib/hooks/useControlledState 是否为稳定入口;CJS/ESM 使用示例;与 useMergedState 行为差异

💡 Result:

简短结论

  • @rc-component/util/lib/hooks/useControlledState 不是对外稳定的「官方入口」。库里公开的 hook 名称是 useMergedState(源码/类型里可见),并且包的 CJS/ESM 导出策略历史上有不稳定/兼容问题,建议按包的对外导出方式来引用(不要硬拼 /lib 未声明的子路径)。[1][2][3]

为什么这么说(要点)

  • 源码/类型:仓库中 hook 源码文件名为 useMergedState.ts,但其 d.ts/实现里存在以 useControlledState 做默认导出/类型别名的情况 —— 说明内部实现/命名和对外暴露并不完全一致,容易被视为内部实现细节而非稳定 API。使用应以仓库/文档的对外名称为准。
  • 包导出策略:rc-util 的 package.json 将主入口指向 ./lib 和 ./es,并且社区已就 /es / /lib 的子路径导出与 ESM/CJS 兼容性提出 Issue(建议通过 package.json 的 exports 统一声明),说明直接引用 /lib/* 或 /es/* 可能因为 packageExports 策略或版本不同而断裂。

CJS / ESM 使用示例(推荐两种稳妥方式)

  1. 从包顶层导入(首选,依赖包的 exports/main/module 保证)
  • ESM:
    import { useMergedState } from 'rc-util'; // 或 import useMergedState from 'rc-util/es/hooks/useMergedState'(见下)
  • CJS:
    const { useMergedState } = require('rc-util');
  1. 明确从 es/lib 子路径(风险:若包未在 exports 声明,会随版本变化出问题)
  • ESM:
    import useMergedState from 'rc-util/es/hooks/useMergedState';
  • CJS:
    const useMergedState = require('rc-util/lib/hooks/useMergedState').default;

注意:避免直接依赖未承诺为 public API 的路径名(例如 /lib/hooks/useControlledState),因为 package 的 exports/esm 改动或重命名会导致断裂。

useControlledState vs useMergedState 行为差异(要点)

  • useMergedState(rc-util 中的实现)目的是处理受控/非受控两种模式的合并:支持 defaultValue、value、onChange、postState 等选项,返回 [state, setState],并在内部以传入的 value 优先(受控)否则使用内部 state(非受控)。实现细节还包含对 postState 的转换等。
  • “useControlledState” 作为通用命名在不同项目/实现中含义不完全一致:有实现会额外返回 isControlled、initialValue 或在受控/非受控切换时发出警告(见社区/项目的不同实现)。因此不要把某个仓库的 useControlledState 当成通用契约;在 rc-util 的上下文,关注 useMergedState 的行为和签名更可靠。

快速参考(阅读/引用)

  • useMergedState 源码(react-component/util 仓库)。
  • rc-util package.json(显示 main/module/files,说明有 lib/es 两套输出)。
  • 社区关于修复 ESM/exports 的 Issue(说明直接从 /lib 或 /es 子路径导入的兼容风险)。

需要我做的下一步?

  • 我可以为你检查当前你项目所安装的 rc-util 版本(package.json / node_modules)后,给出针对该版本的确切可用导入路径和示例。要我查就把版本或项目 package.json 发来。

使用官方导出 useMergedState 替换私有路径引用
直接引用 '@rc-component/util/lib/hooks/useControlledState' 属于未承诺的内部子路径,可能因 CJS/ESM 打包或后续版本变更而失效。请改为:

import { useMergedState } from '@rc-component/util';

(若必须使用子路径,可在 ESM 环境下谨慎使用:

import useMergedState from '@rc-component/util/es/hooks/useMergedState';

但仍存在兼容风险)
确保返回值和受控/非受控逻辑(支持 defaultValue、value、onChange、postState,以 value 优先)与原实现保持一致。

🤖 Prompt for AI Agents
In src/Tabs.tsx around line 3, replace the private subpath import of
useControlledState with the official export from '@rc-component/util' (import {
useMergedState } from '@rc-component/util') to avoid fragile internal path
usage; update any local references to useControlledState to useMergedState and
ensure the hook is used so its return shape and controlled/uncontrolled
semantics remain identical to the previous behavior: support defaultValue,
value, onChange and postState, and prefer value over defaultValue when present
so controlled mode wins.

import isMobile from '@rc-component/util/lib/isMobile';
import * as React from 'react';
import { useEffect, useState } from 'react';
Expand Down Expand Up @@ -123,10 +123,10 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
}, []);

// ====================== Active Key ======================
const [mergedActiveKey, setMergedActiveKey] = useMergedState<string>(() => tabs[0]?.key, {
value: activeKey,
defaultValue: defaultActiveKey,
});
const [mergedActiveKey, setMergedActiveKey] = useControlledState<string>(
defaultActiveKey ?? tabs[0]?.key,
activeKey,
);
const [activeIndex, setActiveIndex] = useState(() =>
tabs.findIndex(tab => tab.key === mergedActiveKey),
);
Expand All @@ -142,9 +142,7 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
}, [tabs.map(tab => tab.key).join('_'), mergedActiveKey, activeIndex]);

// ===================== Accessibility ====================
const [mergedId, setMergedId] = useMergedState(null, {
value: id,
});
const [mergedId, setMergedId] = useControlledState(null, id);

// Async generate id to avoid ssr mapping failed
useEffect(() => {
Expand Down