Skip to content

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

Open
punkbit wants to merge 130 commits intomainfrom
refactor/component-path-redundancy
Open

refactor: 💡 resolve component path redundancy and public API encapsulation#798
punkbit wants to merge 130 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
Collaborator

@vineethasok vineethasok 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 ok with almost all the changes except converting the test and stories to index format
The reason we added the same name as the component is it easily navigate to the appropriate file and test/stories file.
I understand it is possible using the folder name but we might add subcomponents inside the same folder which might need stories or tests
Would love to know your thoughts

import { SidebarCollapsibleTitleProps } from './SidebarCollapsibleTitle/SidebarCollapsibleTitle';
export type { Menu, SplitButtonProps } from './SplitButton/SplitButton';
export type { ToastProps } from './Toast/Toast';
import { SidebarNavigationTitleProps } from './SidebarNavigationTitle';
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Since we are making changes to this files
Maybe we can also add the type prefix to all the content which are imported here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@vineethasok ok thank you. What is the "content" you are referring to?

{
group: ['**/index', '**/index.ts', '**/index.tsx'],
message:
"Do not import from index files within the same component directory. Import directly from source files (e.g., './Button' instead of './index').",
Copy link
Collaborator

Choose a reason for hiding this comment

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

@hoorayimhelping @serdec
We were following a pattern in our company not to use ./ or ../ but @ based importing right?
Just to confirm the pattern we follow in click-ui and other products matches the same standard for coding

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The task here is not to request that the user use relative paths, but to prevent imports from index files. Please check the goal of the PR "path redundancy".

@punkbit
Copy link
Collaborator Author

punkbit commented Feb 20, 2026

I'm ok with almost all the changes except converting the test and stories to index format The reason we added the same name as the component is it easily navigate to the appropriate file and test/stories file. I understand it is possible using the folder name but we might add subcomponents inside the same folder which might need stories or tests Would love to know your thoughts

Ok that's great! You'll be happy to know that the first pass on file architecture is done in separate PR, the first pass is awaiting approve here #832

The PR here is stricly about "resolve component path redundancy".

@punkbit
Copy link
Collaborator Author

punkbit commented Feb 20, 2026

LSP Direct to source

See PR attached video demo "LSP Direct to source"

@hoorayimhelping see LSP Direct to source See PR attached video demo "LSP Direct to source"

@workflow-authentication-public
Copy link
Contributor

workflow-authentication-public bot commented Feb 20, 2026

📚 Storybook Preview Deployed

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

Built from commit: 33899910237b40a4975c96d0340108933628a6f9

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
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