Skip to content

refactor: 💡 resolve component path redundancy and public API encapsulation#798

Merged
punkbit merged 139 commits intomainfrom
refactor/component-path-redundancy
Mar 6, 2026
Merged

refactor: 💡 resolve component path redundancy and public API encapsulation#798
punkbit merged 139 commits intomainfrom
refactor/component-path-redundancy

Conversation

@punkbit
Copy link
Collaborator

@punkbit punkbit commented Feb 2, 2026

Why?

To resolve component path redundancy and allow public API encapsulation.

As work progressed on reducing import path verbosity, several deeper issues surfaced that were addressed as part of this PR. Component import statements previously required the component name twice, e.g. clickhouse/click-ui/components/EllipsisContent/EllipsisContent, which was unnecessary. Beyond that, the original version exposed internal implementation details, allowing consumers to directly access and depend on third-party APIs such as Radix UI components and types. This has led to applications incorrectly coupling themselves to these internals rather than the library's intended public API, a problem that now requires careful, incremental cleanup using @deprecated warnings.

While addressing the above, circular dependencies were discovered throughout the source code. These were not anticipated but were resolved as part of this PR, and new ESLint rules have been introduced to prevent them from reappearing as the library grows.

Finally, after #773 (distribute unbundled) was merged, which solved critical distribution size issues, could now confirm that tree-shaking works correctly under the revised conditions and both import strategies, e.g. top-package level and component-level.

API improvements

  1. Elegant import statements with zero performance cost 🥰 , e.g. gets rid of redundant component name on import, such as @clickhouse/click-ui/components/EllipsisContent/EllipsisContent 🤢
import { EllipsisContent } from '@clickhouse/click-ui/EllipsisContent';
  1. Decoupling consumers from the underlying implementation and improving the long-term maintainability of the library 👏, e.g. The original version exposes internal implementation details, allowing consumers to directly access and depend on third-party APIs such as Radix UI elements/types. This has led to applications incorrectly coupling themselves to these internals rather 🤢 than the library's intended public API, which now requires a lot of unwanted work as we have to rely on @deprecated warnings to remove them gradually! The PR addresses this by encapsulating these details, ensuring only the deliberate public API surface is accessible.

Build output size improvements

The original production version of the Click UI library had a critical bundling issue, producing a build output of 1,216.21 kB with chunks exceeding the 500 kB threshold after minification.
To benchmark the improvements, a baseline Vite app without Click UI was measured at 193.30 kB. After integrating the updated PR version of Click UI, the results were as follows:

Importing a component via the main barrel file / public API produced a build output of 223.70 kB, an overhead of just ~30 kB over the baseline. Importing directly from the component-specific export path (e.g. @clickhouse/click-ui/Button) brought this down marginally further to 223.09 kB.

Both approaches represent a dramatic reduction from the original, with the PR version adding less than 30 kB over a bare Vite app regardless of import strategy.

This is made possible by several changes to resolve component paths and, of course, by the introduction of #773, which makes the package distribution unbundled and moves optimisation responsibility to the consumer side. Before, the consumer always had an unscalable bundled/unoptimizable 😬 package of 1,216.21 kB.

👇 Read below for supporting evidence

❌ The original production version (build output size 1,216.21 kB, plus chunks are larger than 500 kB after minification)

demo-resolve-path-redundancy-public-api--current-original-production-version-0 250 0

🔎 Created a base to demonstrate the performance improvements, e.g. a Vite app without Click UI (build output size 193.30KB)

demo-resolve-path-redundancy-public-api--not-click-ui-dependency-output-size-pt1

The base, e.g. a Vite app shows a placeholder component (build output size 193.30KB)

demo-resolve-path-redundancy-public-api--not-click-ui-dependency-output-size-placeholder-component-pt2

👌 Vite app with Click UI PR Version imports a Click UI Component from main barrel file / Public API (build output size is 223.70 kB)

demo-resolve-path-redundancy-public-api--introduces-click-ui-component-via-main-barrel-file-pt1 demo-resolve-path-redundancy-public-api--introduces-click-ui-component-via-main-barrel-file-pt2

👌 Vite app with Click UI PR Version imports a Click UI Component directly from component path (build output size is 223.09 kB)

demo-resolve-path-redundancy-public-api--introduces-click-ui-component-import-component-directly-from-path-via-public-api-pt1

⚠️ WARNING: Depends on #773, which should be merged first
🤖 TODO: Once #773 is merged, switch base branch to main.

How?

  • Gets rid of some quick fix circular dependencies found in the source code, e.g. ideally this would be a separate PR, but the existence of circular dependencies was not expected, but as progress was made while removing the redundancies, these were found and resolved. ⚠️ There'll be a separa PR on circular dependencies
  • Introduce new ESLint rules to help prevent circular dependencies, e.g. import/no-cycle, and prevent import from barrel files
  • Safe Component-level module re-exports, e.g. while technically these are still "barrels", these are tightly scoped and include only the things the component needs
  • Normalise the distribution build output so that component entry files are named index rather than mirroring their parent directory name, e.g., Button/index.js instead of Button/Button.js
  • Reduced import statement verbosity
  • Make the Public API map directly to the component and type definitions file, e.g. so that consumers import directly from the source of truth rather than passing through intermediate files, making it much faster
  • Introduced benchmarks to address any confusion around the use of module re-exports, e.g. while these are technically barrel files, the benchmarks clearly show they carry zero cost in practice, on Text-editors, LSP, etc. It is also worth noting that these component-level barrel files do not appear in the build output at all.

Preview?

Distribution (to keep it short shows a small example for ESM Button), if you wante more see #773

dist/types
...
dist/cjs
...
dist/esm
├── components
│   ...
│   └── Button
│       ├── index.js
│       └── index.js.map
├── hooks
│   ├── useUpdateEffect.js
│   └── useUpdateEffect.js.map
├── index.js
├── index.js.map
├── lib
│   ├── EventEmitter.js
│   ├── EventEmitter.js.map
│   ├── getTextFromNodes.js
│   └── getTextFromNodes.js.map
├── theme
│   ├── ClickUIProvider
│   │   ├── index.js
│   │   └── index.js.map
│   ├── index.js
│   ├── index.js.map
│   ├── index2.js
│   ├── index2.js.map
│   └── tokens
│       ├── variables.dark.js
│       ├── variables.dark.js.map
│       ├── variables.light.js
│       └── variables.light.js.map
└── utils
    ├── date.js
    ├── date.js.map
    ├── file.js
    ├── file.js.map
    ├── mergeRefs.js
    └── mergeRefs.js.map

LSP direct to source or goto implementation is unaffected, e.g. you don't land in barrel but directly in the source code

Modal/terminal-based text editors

demo-lsp-editor-direct-source.mov
editor-goto-identifier.mov

VSCode-like text editors

demo-goto-source-vscode.mov

ESLint rules

Warns against importing from the main barrel within the library itself, guiding contributors towards importing directly from leaf modules instead. Also, component-level index files are reserved for cross-component imports. These rules help prevent circular dependencies from forming.

demo-lint-import-local-leaf

Benchmarks

To address any concerns around the use of module re-exports and show responsible and safe use of barrel files, benchmark results are available below:

==============================================================================
     Click UI - Deep Nested HMR (Hot Module Replacement) Benchmark
     Tests reload speed with 12-layer component dependency graph
     Target: Icon.tsx at depth ~12 (forces full graph traversal)
==============================================================================

[WARN] This benchmark will temporarily modify source files in src/components/
   Original content will be automatically restored after testing.

[CHECK] Root-level symlink exists: node_modules/@clickhouse/click-ui
[SETUP] Setting up deep nested HMR test playground...

[OK] Playground structure created

[INSTALL] Installing dependencies...
[OK] Dependencies installed


[BENCHMARK] Main Barrel (src/index.ts)
   Import from main barrel file
  [START] Starting dev server...
  [OK] Dev server ready
  [OK] Modules preloaded
  [TEST] HMR test 1/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 1)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 211ms
  [TEST] HMR test 2/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 2)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 188ms
  ...
  [INFO] File change detected by Vite...
  [OK] HMR update time: 103ms
  [TEST] HMR test 100/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 99)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 100ms
  [STOP] Stopping dev server...
  [MODIFY] src/components/Icon/Icon.tsx (marker: 100)

[BENCHMARK] Components Barrel (src/components/index.ts)
   Import from components barrel
  [START] Starting dev server...
  [OK] Dev server ready
  [OK] Modules preloaded
  [TEST] HMR test 1/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 1)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 129ms
  [TEST] HMR test 2/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 2)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 189ms
  ...
  [TEST] HMR test 100/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 100)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 201ms
  [STOP] Stopping dev server...

[BENCHMARK] Direct Source Imports
   Import directly from source files (simulating package exports with source maps)
  [START] Starting dev server...
  [OK] Dev server ready
  [OK] Modules preloaded
  [TEST] HMR test 1/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 1)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 144ms
  [TEST] HMR test 2/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 2)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 179ms
  ...
  [TEST] HMR test 99/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 99)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 202ms
  [TEST] HMR test 100/100
  [MODIFY] src/components/Icon/Icon.tsx (marker: 100)
  [INFO] File change detected by Vite...
  [OK] HMR update time: 201ms
  [STOP] Stopping dev server...

==========================================================================================
DEEP NESTED HMR (HOT MODULE REPLACEMENT) BENCHMARK RESULTS
   Measuring reload speed when modifying Icon.tsx (depth ~12)
   Component graph: App → Container → Flyout → Flyout.Content → Flyout.Body
                   → Container → Table → TableBodyRow → Cell → EllipsisContent
                   → Text → Icon (modified)
==========================================================================================

------------------------------------------------------------------------------------------
Configuration                       |      Avg |   Median |      Min |      Max |       Type
------------------------------------------------------------------------------------------
Main Barrel (src/index.ts)          |    110ms |    101ms |     85ms |    211ms | HMR update
Components Barrel (src/components/index.ts) |    201ms |    202ms |    129ms |    211ms | HMR update
Direct Source Imports               |    201ms |    202ms |    144ms |    211ms | HMR update
------------------------------------------------------------------------------------------

Summary:
  Fastest HMR: Main Barrel (src/index.ts) (110ms avg)
  Slowest HMR: Components Barrel (src/components/index.ts) (201ms avg)
  Difference: 91ms (82.7%)

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

[CLEANUP] Cleaning up...

[RESTORE] Restoring original source files...
  [OK] Restored: src/components/Icon/Icon.tsx
[OK] Cleanup complete

NOTE: Have omitted markers to keep it short. The benchmark file can be run in your local environment and modified to your liking. Find them at here

Copy link
Member

@hoorayimhelping hoorayimhelping left a comment

Choose a reason for hiding this comment

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

I'm still quite against barrel files and I don't think we should be using them, even in a library. IMO, the downsides far outweigh the benefits. https://tkdodo.eu/blog/please-stop-using-barrel-files

@punkbit punkbit changed the title refactor: 💡 resolve component path redundancy refactor: 💡 resolve component path redundancy and public API encapsulation Feb 23, 2026
@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Claude finished @punkbit's task in 2m 49s —— View job


Code Review

Great PR! The goals here are well-motivated — eliminating redundant import paths, fixing circular dependencies, and tightening the public API surface are exactly the right kind of library hygiene. The benchmarks and LSP demos are excellent supporting evidence. A few things worth flagging:


🚨 Must Fix

  • AutoComplete/index.ts line 1 exports SelectPopoverRoot — this is a styled(Root) Radix Popover component, which is precisely the kind of internal Radix detail the PR aims to encapsulate. If it's not needed by consumers, remove it. If it is needed, it should be documented. Fix this →

💡 Suggestions (non-blocking)

  • src/components/types.ts still re-exports raw Radix typesPopoverProps from @radix-ui/react-popover, HoverCardProps from @radix-ui/react-hover-card, and ContextMenuProps from @radix-ui/react-context-menu are all re-exported as-is. The PR description acknowledges this needs gradual cleanup via @deprecated. Worth adding those @deprecated markers in a follow-up if not already planned.

  • Accordion.tsx has two separate import lines from the same module (@/components/Icon) — they can be merged into one. Small style issue, not blocking.

    // current
    import { IconSize, IconName } from '@/components/Icon';
    import { Icon } from '@/components/Icon';
    
    // preferred
    import { Icon, IconSize, IconName } from '@/components/Icon';
  • no-restricted-imports pattern gap — the ESLint pattern ['**/index', '**/index.ts', '**/index.tsx'] only matches literal /index in the import string. Folder-resolution imports like @/components/Icon (which TypeScript resolves to Icon/index.ts) are not caught. This appears intentional — folder-level imports to component index files are the intended usage — but worth a comment in eslint.config.js to clarify why this is expected.

  • Changeset quality — the changeset is comprehensive and well written. One small addition that would help: a before/after example of the import path change in the changeset itself (not just in the PR description), since the changeset is what lands in CHANGELOG.md.


✅ What looks good

  • All new index.ts files correctly use export type for type-only re-exports
  • import/no-cycle at maxDepth: 10 with ignoreExternal: true is well-tuned
  • DateDetails.test.tsx test improvements (ISO date strings + regex matchers) are more robust and timezone-portable
  • package.json export map is clean and covers ESM, CJS, and types for every component path
  • Deleted Assets/{Icons,Flags,Logos,Payments}/system/index.ts — correct, those were intermediate re-export files no longer needed after the cycle fixes

@workflow-authentication-public
Copy link
Contributor

📚 Storybook Preview Deployed

✅ Preview URL: https://click-lxqz6w3qe-clickhouse.vercel.app

Built from commit: 5385aba6859ac6691e4f8dc119591d29f43b7d95

@punkbit punkbit merged commit 29c6bc5 into main Mar 6, 2026
8 checks passed
@punkbit punkbit deleted the refactor/component-path-redundancy branch March 6, 2026 10:33
punkbit added a commit that referenced this pull request Mar 9, 2026
…ation (#798)

* chore: 🤖 update required packages

* chore: 🤖 provide styled components types

* fix: 🐛 linter should ignore d.ts files

* fix: 🐛 use double quotes

* fix: 🐛 vite > v2 separates vitest config

* chore: 🤖 reorder package

* chore: 🤖 remove side property (deprecated)

* fix: 🐛 dropdown amends

* fix: 🐛 theme prop

* fix: 🐛 type

* chore: 🤖 TIAS build version supported on next v16 RSC

* chore: 🤖 WIP ongoing styled-component v6.1.11 (non experimental) support

* chore: 🤖 strict react version for dev

* fix: 🐛 missing ref on forwardRef, might have plenty of these

* chore: 🤖 update lockfile

* chore: 🤖 remove optional flag from ref (typo)

* chore: 🤖 add comment for future ref

* refactor: 💡 banner

* chore: 🤖 lint amends (double quotes)

* refactor: 💡 removes style prop as typed prop

* chore: 🤖 remove ajv

* chore: 🤖 format

* test: 💍 add aria pressed to ButtonGroup

* chore: 🤖 format

* test: 💍 use local getByText

* chore: 🤖 update lockfile

* chore: 🤖 add changeset

* chore: 🤖 small text amend to trigger vercel deploy

* chore: 🤖 prevent running on CI

* chore: 🤖 add HUSKY to preven husky runnig pre-commit hook

* fix: 🐛 conflict resolution

* chore: 🤖 bump rc number

* docs: 📝 build esm, how to use

* chore: 🤖 ESM vite builder (wip)

* fix: 🐛 remove .tsx extension from import statements

* fix: 🐛 remove .tsx extension from import statements

* fix: 🐛 remove .tsx extension from import statements

* fix: 🐛 remove .ts extension from import statements

* fix: 🐛 remove .ts extension from import statements

* chore: 🤖 add eslint to assess import extensions not required

* chore: 🤖 format

* chore: 🤖 temporary custom resolve tsconfig path

* refactor: 💡 export from correct theme boundary

* chore: 🤖 node externals in vite, remove alias

* chore: 🤖 use relative paths

* chore: 🤖 use externalize deps

* chore: 🤖 for ESM compatibility, tweak/handle CJS components

* chore: revert ts alias rewrite to relative

* chore: lint do not allow barrel imports

* chore: remove excludes from tsconfig

* chore: set vite settings to preserve file struct in output

* fix: solve import cycles

* fix: solve import cycles in stories

* fix: build amends

* fix: add .js extension

* chore: analyze and visualise bundle

* chore: split ESM, CJS distribution

* chore: format

* fix: 🐛 lint code block

* fix: 🐛 import Separator

* chore: format

* fix: 🐛 import Separator

* chore: 🤖 add changeset

* chore: 🤖 use 0.0.251-rc.62

* chore: 🤖 resolve conflict resolution, deleted files which were removed in main branch

* chore: 🤖 resolve conflict resolution, middle truncator

* chore: 🤖 resolve conflict resolution, missing container changes

* refactor: 💡 FileMultiUpload to follow FileUpload due to middle truncator

* fix: 🐛 prevent icon success pushed right

* fix: 🐛 remove file size from multiple file upload

* chore: 🤖 merge conflict amend for ButtonGroup

* chore: 🤖 remove comment

* refactor: 💡 reduce import path redundancy (WIP, pt1)

* refactor: 💡 accordion as index

* refactor: 💡 rename component by directory name to index

* refactor: 💡 update import statements to prefer index

* chore: 🤖 remove old indexes (this was a failed attempt, which imported the reduntant name)

* fix: 🐛 import statements

* fix: 🐛 import statements

* fix: 🐛 import statements

* fix: 🐛 icon names in types

* chore: 🤖 add note

* chore: 🤖 format

* fix: 🐛 icon names location

* fix: 🐛 icon names location

* chore: 🤖 update changeset

* refactor: 💡 use component level barrel, to allow devs see component name on editor

* fix: 🐛 import statements

* chore: 🤖 add eslint to prevent imports from index and request use of leafs when possible

* fix: 🐛 dayjs version

* fix: 🐛 merge from main,m incorrectly changed

* chore: 🤖 prepare merge main

* fix: 🐛 merge conflicts

* refactor: 💡 further paths pass

* chore: 🤖 add note

* chore: 🤖 format

* refactor: 💡 further paths pass for AutoComplete

* fix: 🐛 merge conflict

* refactor: 💡 further paths pass for Collapsible

* fix: 🐛 merge conflict

* fix: 🐛 file extension

* chore: 🤖 add hmr benchmark

* chore: 🤖 make component build name index to remove redundancy

* chore: 🤖 generate exports, e.g. expose direct component imports and its types

chore: 🤖 generate exports, e.g. expose direct component imports and its types

* perf: ⚡ benchmark hmr deep nested components

* chore: 🤖 link @clickhouse/click-ui to itself, required for benchmark

* perf: ⚡ generate component exports, e.g. speedy component and type access

* refactor: 💡 remove unused re-export files

* chore: 🤖 update changeset

* chore: 🤖 remove comments

* test: 💍 update test (due to merge issues)

* fix: 🐛  error TS2724: './IconButton' has no exported member named 'IconButtonSize'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants