Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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