@k8slens/use-inject
v1.0.2
Published
The hooks a React component injects with: useInject, and useSyncInject for values that must not suspend
Maintainers
Keywords
Readme
@k8slens/use-inject
useInject — how a React component reads an injected value.
import { useInject } from "@k8slens/use-inject";
const MyPanel = () => {
const getGreeting = useInject(greetingInjectable);
return <p>{getGreeting()}</p>;
};useInject hands back the injectable's factory, not the value. Calling it
produces the value, and instantiation parameters go to that call rather than to
useInject:
const getGreeting = useInject(greetingForNameInjectable);
return <p>{getGreeting("Ada")}</p>;Why this package exists
The hook itself lives in @k8slens/injectable-react under the name
useInject2. The 2 is an artefact of an internal DI migration and means
nothing to a consumer, and that package also carries an older, differently
behaved useInject from before the migration. So the name is claimed here, on
a package small enough to promise to everyone, and consumers are spared both
the digit and the ambiguity.
Asynchronous values: useSyncInject, never Suspense
When the value resolves later, use useSyncInject. It reads the value without
suspending and re-renders when it changes. Unlike useInject it hands back the
value, and a parameter goes to the hook:
const summary = useSyncInject(summaryForClusterInjectable, clusterId);useInject will hand you a promise if the factory returns one, and React's
use looks like the obvious next step. It is not. Suspense is incompatible
with MobX observation here: React throws away a suspended render and retries it,
mobx-react tracks the abandoned render's Reaction with a
FinalizationRegistry, and so cleanup waits on garbage collection that may be
late or never come. The observation outlives the component —
onBecomeUnobserved never fires and each suspension leaves another live
observer behind.
Verified in this repo, not inherited from a changelog: an observer that
suspends still holds its observation after unmount, while a non-suspending one
releases it. The code path is unchanged in the latest mobx-react-lite, and the
upstream fix (mobxjs/mobx#3777) has never been merged.
Be careful what you pass as an instantiation parameter
The cache is keyed by reference. Primitives behave as expected — equal strings or numbers resolve to one instance — and a reference type is fine too, provided the reference is stable. What breaks is building the value inline, where identity differs on every render:
// Trouble: a new object each render is a new key each render.
getGreetingFor({ name });
// Fine: a primitive resolves to one instance.
getGreetingFor(name);
// Also fine: one stable reference.
const subject = useInject(currentSubjectInjectable)();
getGreetingFor(subject);The cost of an unstable key is more than a re-suspension. Each render mints another instance the container then holds, so the cache grows unbounded while the component renders, and nothing is shared: two callers asking for "the same" thing get separate instances with separate state and separate observers.
So think about identity rather than type. A reference from DI, from props, or from a memo is as stable as a primitive; only a literal built inline is the hazard.
