rive-async-patterns
**Scope**: Async instance lifecycle, null-guarding `rive`, loading states, event callback sequencing. **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 **Generated**: 2026-04-14
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
**Scope**: Async instance lifecycle, null-guarding `rive`, loading states, event callback sequencing. **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 **Generated**: 2026-04-14
Agent definition
rive-async-patterns.mdRive Async Patterns Reference
> **Scope**: Async instance lifecycle, null-guarding `rive`, loading states, event callback sequencing. > **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 > **Generated**: 2026-04-14
---
`useRive` returns null `rive` until `.riv` loads and WASM initializes. This async gap is the most common runtime error source — firing inputs before ready silently fails or throws. Guard: state machine input access, manual playback, and Zustand bridging.
Pattern Table
| Pattern | Use When | Avoid When | |---------|----------|------------| | `if (!rive) return` guard in `useEffect` | accessing rive in effects | synchronous render path | | `onLoad` callback | one-time post-load setup | polling rive for non-null | | `onStateChange` for animation complete | waiting for animation finish | setTimeout sequencing | | `isLoading` from `useRive` | showing loading placeholder | — (always show feedback) |
---
Correct Patterns
Null-Guarding the Rive Instance in Effects
const { rive, RiveComponent } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
});
useEffect(() => {
if (!rive) return;
const attackInput = rive
.stateMachineInputs('CombatStateMachine')
?.find(i => i.name === 'triggerAttack');
attackInput?.fire();
}, [rive, triggerAttack]);`rive` transitions null → instance once. Listing it as dependency ensures the effect fires at the exact moment it's ready.
---
Using onLoad for One-Time Post-Load Setup
const inputsRef = useRef<Record<string, StateMachineInput>>({});
const { rive, RiveComponent } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
onLoad: () => {
const inputs = rive!.stateMachineInputs('CombatStateMachine') ?? [];
inputs.forEach(input => { inputsRef.current[input.name] = input; });
},
});`onLoad` fires once after the Rive runtime signals artboard ready.
---
Detecting Animation Completion via onStateChange
Do not use `setTimeout`. Use `onStateChange`:
const { rive, RiveComponent } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
onStateChange: (event: StateChangeEvent) => {
if (event.data.includes('idle')) {
dispatch({ type: 'ANIMATION_COMPLETE' });
}
},
});`onStateChange` is driven by Rive's internal clock, stays in sync regardless of frame rate.
---
Bridging Rive Inputs to Zustand Store
Store a callback, not the Rive instance, in Zustand:
const { rive, RiveComponent } = useRive({ ... });
useEffect(() => {
if (!rive) return;
useCombatStore.getState().registerFireInput(
(inputName: string) => {
const inputs = rive.stateMachineInputs('CombatStateMachine');
inputs?.find(i => i.name === inputName)?.fire();
}
);
return () => useCombatStore.getState().registerFireInput(null);
}, [rive]);A callback defers instance lookup to call time, avoiding stale references.
---
Pattern Catalog
Guard Rive Instance Access in Effects
**Detection**:
grep -rn 'rive\.' --include="*.tsx" | grep -v 'useEffect\|onLoad\|onStateChange\|useCallback\|//' | grep -v 'RiveComponent\|useRive\|import'
**Signal**: `rive.stateMachineInputs(...)` outside a guarded effect — `rive` is null on first render, throws `TypeError`.
**Fix**: Move to `useEffect` with `[rive]` dependency and `if (!rive) return` guard.
---
Use onStateChange for Animation Sequencing
**Detection**:
grep -rn 'setTimeout' --include="*.tsx" | grep -i 'anim\|rive\|attack\|hit\|state'
**Signal**: `setTimeout(() => { recoverInput?.fire(); }, 300)` — duration drifts from actual animation at non-60fps.
**Fix**: Use `onStateChange` to detect state transitions. In `@rive-app/react-canvas` 4.x, `event.data` is `string[]` of current state names.
---
Use Effect Dependencies Instead of Polling
**Detection**:
grep -rn 'setInterval\|while.*rive\|poll' --include="*.tsx" | grep -i 'rive'
**Signal**: `setInterval` polling for `rive !== null`. Reimplements what `useRive` already does with `onLoad`.
**Fix**: Use `useEffect([rive])` or `onLoad` callback.
---
Error-Fix Mappings
| Error | Root Cause | Fix | |-------|------------|-----| | `TypeError: Cannot read properties of null (reading 'stateMachineInputs')` | `rive` accessed before load | `if (!rive) return` guard; `useEffect([rive])` | | `useStateMachineInput returns null` | Input name typo or state machine name mismatch | Check Rive Editor for exact case-sensitive names | | Animation fires once then stops | `autoplay: true` with one-shot, no loop | Use state machine with looping idle | | `onStateChange` not firing | State machine name mismatch | Check `stateMachines:` param spelling | | Inputs fire, no visual change | Wrong artboard selected | Add `artboard: 'ArtboardName'` param |
---
Detection Commands
# Direct rive access outside effect/callback (null dereference risk)
grep -rn 'rive\.' --include="*.tsx" | grep -v 'useEffect\|onLoad\|onStateChange\|useCallback\|//'
# setTimeout animation sequencing (timing drift)
grep -rn 'setTimeout' --include="*.tsx" | grep -i 'anim\|attack\|hit\|fire\|input'
# Polling for rive instance
rg 'setInterval' --type tsx -l | xargs grep -l 'rive'
# Missing null guard in effects
grep -rn 'useEffect' --include="*.tsx" -A5 | grep -v 'if.*rive\|!rive' | grep 'rive\.'
---
See Also
- `rive-react-setup.md` — Full `useRive` parameter reference
- `rive-performance.md` — WebGL cleanup, lazy loading, canvas sizing
- `rive-animation-library.md` — State machine design, `onStateChange` states
Read more
Rive Async Patterns Reference
> **Scope**: Async instance lifecycle, null-guarding `rive`, loading states, event callback sequencing. > **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 > **Generated**: 2026-04-14
---
`useRive` returns null `rive` until `.riv` loads and WASM initializes. This async gap is the most common runtime error source — firing inputs before ready silently fails or throws. Guard: state machine input access, manual playback, and Zustand bridging.
Pattern Table
| Pattern | Use When | Avoid When | |---------|----------|------------| | `if (!rive) return` guard in `useEffect` | accessing rive in effects | synchronous render path | | `onLoad` callback | one-time post-load setup | polling rive for non-null | | `onStateChange` for animation complete | waiting for animation finish | setTimeout sequencing | | `isLoading` from `useRive` | showing loading placeholder | — (always show feedback) |
---
Correct Patterns
Null-Guarding the Rive Instance in Effects
const { rive, RiveComponent } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
});
useEffect(() => {
if (!rive) return;
const attackInput = rive
.stateMachineInputs('CombatStateMachine')
?.find(i => i.name === 'triggerAttack');
attackInput?.fire();
}, [rive, triggerAttack]);`rive` transitions null → instance once. Listing it as dependency ensures the effect fires at the exact moment it's ready.
---
Using onLoad for One-Time Post-Load Setup
const inputsRef = useRef<Record<string, StateMachineInput>>({});
const { rive, RiveComponent } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
onLoad: () => {
const inputs = rive!.stateMachineInputs('CombatStateMachine') ?? [];
inputs.forEach(input => { inputsRef.current[input.name] = input; });
},
});`onLoad` fires once after the Rive runtime signals artboard ready.
---
Detecting Animation Completion via onStateChange
Do not use `setTimeout`. Use `onStateChange`:
const { rive, RiveComponent } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
onStateChange: (event: StateChangeEvent) => {
if (event.data.includes('idle')) {
dispatch({ type: 'ANIMATION_COMPLETE' });
}
},
});`onStateChange` is driven by Rive's internal clock, stays in sync regardless of frame rate.
---
Bridging Rive Inputs to Zustand Store
Store a callback, not the Rive instance, in Zustand:
const { rive, RiveComponent } = useRive({ ... });
useEffect(() => {
if (!rive) return;
useCombatStore.getState().registerFireInput(
(inputName: string) => {
const inputs = rive.stateMachineInputs('CombatStateMachine');
inputs?.find(i => i.name === inputName)?.fire();
}
);
return () => useCombatStore.getState().registerFireInput(null);
}, [rive]);A callback defers instance lookup to call time, avoiding stale references.
---
Pattern Catalog
Guard Rive Instance Access in Effects
**Detection**:
grep -rn 'rive\.' --include="*.tsx" | grep -v 'useEffect\|onLoad\|onStateChange\|useCallback\|//' | grep -v 'RiveComponent\|useRive\|import'
**Signal**: `rive.stateMachineInputs(...)` outside a guarded effect — `rive` is null on first render, throws `TypeError`.
**Fix**: Move to `useEffect` with `[rive]` dependency and `if (!rive) return` guard.
---
Use onStateChange for Animation Sequencing
**Detection**:
grep -rn 'setTimeout' --include="*.tsx" | grep -i 'anim\|rive\|attack\|hit\|state'
**Signal**: `setTimeout(() => { recoverInput?.fire(); }, 300)` — duration drifts from actual animation at non-60fps.
**Fix**: Use `onStateChange` to detect state transitions. In `@rive-app/react-canvas` 4.x, `event.data` is `string[]` of current state names.
---
Use Effect Dependencies Instead of Polling
**Detection**:
grep -rn 'setInterval\|while.*rive\|poll' --include="*.tsx" | grep -i 'rive'
**Signal**: `setInterval` polling for `rive !== null`. Reimplements what `useRive` already does with `onLoad`.
**Fix**: Use `useEffect([rive])` or `onLoad` callback.
---
Error-Fix Mappings
| Error | Root Cause | Fix | |-------|------------|-----| | `TypeError: Cannot read properties of null (reading 'stateMachineInputs')` | `rive` accessed before load | `if (!rive) return` guard; `useEffect([rive])` | | `useStateMachineInput returns null` | Input name typo or state machine name mismatch | Check Rive Editor for exact case-sensitive names | | Animation fires once then stops | `autoplay: true` with one-shot, no loop | Use state machine with looping idle | | `onStateChange` not firing | State machine name mismatch | Check `stateMachines:` param spelling | | Inputs fire, no visual change | Wrong artboard selected | Add `artboard: 'ArtboardName'` param |
---
Detection Commands
# Direct rive access outside effect/callback (null dereference risk) grep -rn 'rive\.' --include="*.tsx" | grep -v 'useEffect\|onLoad\|onStateChange\|useCallback\|//' # setTimeout animation sequencing (timing drift) grep -rn 'setTimeout' --include="*.tsx" | grep -i 'anim\|attack\|hit\|fire\|input' # Polling for rive instance rg 'setInterval' --type tsx -l | xargs grep -l 'rive' # Missing null guard in effects grep -rn 'useEffect' --include="*.tsx" -A5 | grep -v 'if.*rive\|!rive' | grep 'rive\.'
---
See Also
- `rive-react-setup.md` — Full `useRive` parameter reference
- `rive-performance.md` — WebGL cleanup, lazy loading, canvas sizing
- `rive-animation-library.md` — State machine design, `onStateChange` states
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

