moeru-ai/airi · warning
useReplayable must be used within a Replayable component
Error message
useReplayable must be used within a Replayable component
What it means
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.
Source
Thrown at packages/stage-ui/src/components/animations/use-replayable.ts:12
import { inject, onUnmounted } from 'vue'
interface ReplayableContext {
registerReplayCallback: (callback: () => void | Promise<void>) => () => void
isReplaying: () => boolean
}
export function useReplayable(replayFn?: () => void | Promise<void>) {
const context = inject<ReplayableContext>('replayable')
if (!context) {
console.warn('useReplayable must be used within a Replayable component')
return {
registerReplay: () => () => {},
isReplaying: () => false,
}
}
const registerReplay = (callback: () => void | Promise<void>) => {
return context.registerReplayCallback(callback)
}
// Auto-register if callback provided
let unregister: (() => void) | undefined
if (replayFn) {
unregister = registerReplay(replayFn)
}
onUnmounted(() => {View on GitHub (pinned to 677329427f)
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'.
Example fix
<!-- before --> <ConfettiBurst v-if="show" /> <!-- after --> <Replayable> <ConfettiBurst v-if="show" /> </Replayable>
Defensive patterns
Strategy: validation
Validate before calling
// inside setup(): verify the ancestor exists
const ctx = inject<ReplayableContext | null>('replayable', null)
if (!ctx) { /* component is outside <Replayable>; skip replay wiring */ } Type guard
function isReplayableContext(v: unknown): v is ReplayableContext {
return !!v && typeof (v as ReplayableContext).registerReplayCallback === 'function'
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Widgets element not found
- No toast root provided, cannot close toast
- Electron ipcRenderer is not available. Pass it explicitly to
- version ${rawValue.value.version} doesn't satisfy the versio
- property key 'version' wasn't found in the value of key ${ke
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/307d30d174d71202.
Report an issue: GitHub.