{"record":{"id":"307d30d174d71202","repo":"moeru-ai/airi","slug":"usereplayable-must-be-used-within-a-replayable-com","errorCode":null,"errorMessage":"useReplayable must be used within a Replayable component","messagePattern":"useReplayable must be used within a Replayable component","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/components/animations/use-replayable.ts","lineNumber":12,"sourceCode":"import { inject, onUnmounted } from 'vue'\n\ninterface ReplayableContext {\n  registerReplayCallback: (callback: () => void | Promise<void>) => () => void\n  isReplaying: () => boolean\n}\n\nexport function useReplayable(replayFn?: () => void | Promise<void>) {\n  const context = inject<ReplayableContext>('replayable')\n\n  if (!context) {\n    console.warn('useReplayable must be used within a Replayable component')\n\n    return {\n      registerReplay: () => () => {},\n      isReplaying: () => false,\n    }\n  }\n\n  const registerReplay = (callback: () => void | Promise<void>) => {\n    return context.registerReplayCallback(callback)\n  }\n\n  // Auto-register if callback provided\n  let unregister: (() => void) | undefined\n  if (replayFn) {\n    unregister = registerReplay(replayFn)\n  }\n\n  onUnmounted(() => {","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/components/animations/use-replayable.ts#L1-L30","documentation":"useReplayable() calls Vue's inject('replayable'), which only resolves inside a component tree where Replayable.vue has run provide('replayable', ...). Outside that subtree the injection is undefined, so the composable warns and hands back inert stubs (registerReplay is a no-op disposer, isReplaying always false) instead of throwing. Replay registration silently does nothing.","triggerScenarios":"Calling useReplayable() in a component that is not a descendant of <Replayable> (packages/stage-ui/src/components/animations/Replayable.vue); using it in a composable invoked outside setup(); teleporting a child outside the provider subtree (injection follows the component tree, not DOM).","commonSituations":"Copying a component using useReplayable into a different page without its Replayable wrapper; restructuring layouts so the provider ancestor is lost; unit tests mounting the leaf component in isolation.","solutions":["Wrap the consuming component in <Replayable> from packages/stage-ui/src/components/animations/Replayable.vue.","Verify the provide/inject key string ('replayable') matches between provider and composable.","In tests, mount with a Replayable ancestor or global.provide stub for 'replayable'."],"exampleFix":"<!-- before -->\n<ConfettiBurst v-if=\"show\" />\n\n<!-- after -->\n<Replayable>\n  <ConfettiBurst v-if=\"show\" />\n</Replayable>","handlingStrategy":"validation","validationCode":"// inside setup(): verify the ancestor exists\nconst ctx = inject<ReplayableContext | null>('replayable', null)\nif (!ctx) { /* component is outside <Replayable>; skip replay wiring */ }","typeGuard":"function isReplayableContext(v: unknown): v is ReplayableContext {\n  return !!v && typeof (v as ReplayableContext).registerReplayCallback === 'function'\n}","tryCatchPattern":null,"preventionTips":["Always mount replayable animations inside <Replayable> from components/animations/Replayable.vue.","In unit tests, provide a stub via global.provide({ replayable: { registerReplayCallback: () => () => {}, isReplaying: () => false } }).","Remember Teleport does not break injection, but moving the component in the tree does."],"tags":["vue","provide-inject","composable","component-tree"],"backgroundTag":"injection-context-missing","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}