Skip to content

fix: getKeplerFactory number of hook calls errors - #354

Merged
ilyabo merged 6 commits into
mainfrom
fix--getKeplerFactory-broken
Feb 10, 2026
Merged

ilyabo merged 6 commits into
mainfrom
fix--getKeplerFactory-broken

Conversation

@ilyabo

@ilyabo ilyabo commented Feb 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added a public KeplerInjector API that provides a stable entry point for resolving and using component factories.
  • Performance Improvements

    • Introduced persistent per-injector component caching so wrapped components are created once and reused, improving render stability, responsiveness, and reducing redundant work on re-renders.

Signed-off-by: Ilya Boyandin <[email protected]>
@coderabbitai

coderabbitai Bot commented Feb 10, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Replaced per-render hook memoization with persistent WeakMap caches to stabilize resolved Kepler component wrappers across renders. Exposed a new KeplerInjector.get that delegates to the shared getKeplerFactory. Removed useMemo import and added comments about cache lifecycle and invalidation.

Changes

Cohort / File(s) Summary
Kepler injector & caching
packages/kepler/src/components/KeplerInjector.tsx
Replaced React hook (useMemo) memoization with persistent caches (injectorToFactoryCache, factoryToWrapperCache) keyed by injector and factory. getKeplerFactory now returns a cached wrapper (creates & caches if missing). Added exported KeplerInjector.get and updated comments on invalidation tied to injector configure/reset.
Public API export
packages/kepler/src/index.ts
Exported KeplerInjector from ./components/KeplerInjector to surface the new API in the package barrel.

Sequence Diagram(s)

(Skipped — changes are internal caching and API surface; no multi-component runtime flow requiring a diagram.)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 I hid a wrapper in a WeakMap den,
Factories nestled, reused again and again.
No frantic remounts, just gentle hops,
Caches hum softly as render stops. 🎩✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: replacing useMemo-based memoization to resolve hook call order errors, which is the core change throughout the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix--getKeplerFactory-broken

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
packages/kepler/src/components/KeplerInjector.tsx (1)

207-215: KeplerInjector.get is a thin pass-through — consider whether it adds enough value to justify the extra API surface.

This is a convenience alias for getKeplerFactory. Having two ways to do the same thing can confuse consumers. If the intent is to provide a namespace for future methods (e.g., KeplerInjector.configure, KeplerInjector.reset), that motivation could be documented. Otherwise this is fine as-is.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/kepler/src/components/KeplerInjector.tsx (2)

165-187: ⚠️ Potential issue | 🟠 Major

Wrapped is recreated on every call — callers inside render will trigger remounts.

The inner Component is correctly cached in the WeakMap, but getKeplerFactory returns a new Wrapped function (i.e. a new React component type) on every invocation. If a caller happens to invoke getKeplerFactory(SomeFactory) during a component's render (rather than at module scope), React will see a different component type each render and unmount/remount the subtree.

Consider also caching the Wrapped wrapper in the same byFactory map (or a parallel cache) keyed by (injector, factory), so repeated calls for the same factory return a referentially stable component.

♻️ Sketch: cache the wrapper alongside the resolved component
+const wrapperCache = new WeakMap<
+  object,
+  Map<KeplerFactory, React.ComponentType<any>>
+>();
+
 export function getKeplerFactory<TFactory extends KeplerFactory>(
   factory: TFactory,
 ): ReturnType<TFactory> {
-  const Wrapped = ((props: Record<string, unknown>) => {
+  const injectorInstance = getKeplerInjector();
+  let byFactory = wrapperCache.get(injectorInstance as object);
+  if (byFactory === undefined) {
+    byFactory = new Map();
+    wrapperCache.set(injectorInstance as object, byFactory);
+  }
+  let Wrapped = byFactory.get(factory);
+  if (Wrapped !== undefined) {
+    return Wrapped as unknown as ReturnType<TFactory>;
+  }
+  Wrapped = ((props: Record<string, unknown>) => {
     const injectorInstance = getKeplerInjector();
     let byFactory = injectorToFactoryCache.get(injectorInstance as object);
     ...
     return <Component {...props} />;
-  }) as unknown as ReturnType<TFactory>;
-  return Wrapped;
+  }) as React.ComponentType<any>;
+  byFactory.set(factory, Wrapped);
+  return Wrapped as unknown as ReturnType<TFactory>;
 }

189-193: ⚠️ Potential issue | 🟡 Minor

Add KeplerInjector to barrel export if intended as public API.

KeplerInjector is exported from packages/kepler/src/components/KeplerInjector.tsx but not re-exported from packages/kepler/src/index.ts. If KeplerInjector is meant to be the stable public entry point, add it to the barrel export alongside the existing re-exports (getKeplerFactory, getKeplerInjector, etc.).

Consider adding JSDoc to KeplerInjector.get() noting that it should be called at module scope or in a constant initializer (outside component render) to keep the returned component reference stable.

🧹 Nitpick comments (1)
packages/kepler/src/components/KeplerInjector.tsx (1)

170-183: Mutating a cache during render is a side-effect — acceptable here but worth a comment.

Populating injectorToFactoryCache inside the render body is technically a side effect. It works because the write is idempotent and the result is deterministic for a given (injector, factory) pair, so it's safe under React 18 strict mode's double-invoke semantics. The existing comment on lines 158-159 partially explains this, but a brief inline note at the mutation site (e.g., "Idempotent lazy init — safe under concurrent/strict mode") would help future readers.

Signed-off-by: Ilya Boyandin <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/kepler/src/components/KeplerInjector.tsx (1)

187-191: ⚠️ Potential issue | 🟡 Minor

Add KeplerInjector to the package index exports.

KeplerInjector is exported from packages/kepler/src/components/KeplerInjector.tsx but not re-exported from packages/kepler/src/index.ts. If it is intended as a public API, add it to the export list alongside the related function exports.

🤖 Fix all issues with AI agents
In `@packages/kepler/src/components/KeplerInjector.tsx`:
- Around line 158-185: The wrapper component returned by getKeplerFactory is
anonymous which hinders React DevTools; after creating Wrapped (the per-factory
cached component that renders Component obtained from injectorInstance.get), set
a descriptive displayName on Wrapped (e.g., using Component.displayName ||
Component.name combined with a prefix like "KeplerInjector(") before caching it
in the Map stored on injectorToFactoryCache so the cached Wrapped has a readable
displayName for debugging.

Comment thread packages/kepler/src/components/KeplerInjector.tsx Outdated
Signed-off-by: Ilya Boyandin <[email protected]>
…ponent from the current injector during render.

Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
@ilyabo
ilyabo merged commit c52fa70 into main Feb 10, 2026
4 checks passed
@ilyabo
ilyabo deleted the fix--getKeplerFactory-broken branch February 10, 2026 21:15
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.

2 participants