Skip to content

rive-react-setup

<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How 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.

<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->

Agent definition

rive-react-setup.md

Rive React Setup Reference

<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->

`@rive-app/react-canvas` — React wrapper over Rive Web runtime. Handles canvas lifecycle, WASM loading, resize. Version 4.x supports React 16.8–19. WASM bundle ~150KB gzip.

Installation

npm install @rive-app/react-canvas

WASM bundled in npm package, loaded at runtime on first mount.

Basic useRive Pattern

import { useRive } from '@rive-app/react-canvas';

function PlayerCharacter() {
  const { RiveComponent, rive } = useRive({
    src: '/assets/characters/player.riv',
    stateMachines: 'CombatStateMachine',
    autoplay: true,
  });

  // rive is null until the .riv file loads — always guard before use
  useEffect(() => {
    if (!rive) return;
    // rive instance is ready here
  }, [rive]);

  return (
    <div style={{ width: 400, height: 400, position: 'relative' }}>
      <RiveComponent />
    </div>
  );
}

`RiveComponent` fills its container. Wrap in explicit-sized div. Do not set width/height on `RiveComponent` directly.

useRive Parameters

useRive({
  src: string,                   // path to .riv file, or ArrayBuffer
  artboard?: string,             // specific artboard (default: first artboard)
  animations?: string | string[], // animation clip names to play directly (no state machine)
  stateMachines?: string | string[], // state machine name(s) to activate
  autoplay?: boolean,            // default: false — set true for immediate playback
  layout?: Layout,               // canvas fit/alignment — import Layout from @rive-app/react-canvas
  onLoad?: () => void,           // fires when .riv finishes loading
  onStateChange?: (event: StateChangeEvent) => void, // fires on state machine transition
  onPlay?: () => void,
  onPause?: () => void,
  onStop?: () => void,
})

`stateMachines` name is case-sensitive, must match Rive Editor exactly.

Return Values

const {
  rive,            // Rive instance — null until loaded
  RiveComponent,   // JSX element — mount this in render
  setCanvasRef,    // ref setter for custom canvas placement
  setContainerRef, // ref setter for custom container placement
} = useRive(params);

`rive` exposes Web runtime API: `rive.play()`, `rive.pause()`, `rive.reset()`, `rive.stop()`.

State Machine Inputs

`useStateMachineInput` returns a reference to a named input from the active state machine.

import { useRive, useStateMachineInput } from '@rive-app/react-canvas';

const SM = 'CombatStateMachine'; // exact name from Rive Editor

function PlayerCharacter() {
  const { RiveComponent, rive } = useRive({
    src: playerRiv,
    stateMachines: SM,
    autoplay: true,
  });

  // Trigger — one-shot event (attack, hit, signature)
  const attackTrigger = useStateMachineInput(rive, SM, 'attack');
  const hitTrigger    = useStateMachineInput(rive, SM, 'hit');

  // Boolean — sustained state (blocking, stunned)
  const blockInput    = useStateMachineInput(rive, SM, 'isBlocking');

  // Number — health, charge level, anger meter
  const healthInput   = useStateMachineInput(rive, SM, 'health');

  // All inputs are null until rive loads — always guard
  const fireAttack = () => { if (attackTrigger) attackTrigger.fire(); };
  const setBlock   = (v: boolean) => { if (blockInput) blockInput.value = v; };
  const setHealth  = (hp: number) => { if (healthInput) healthInput.value = hp; };

  return <div style={{ width: 400, height: 400 }}><RiveComponent /></div>;
}

Input names are case-sensitive, must match Rive Editor. Mismatch returns `null` silently — log return values in dev to catch typos.

Input Object Types

| Input type | Hook return type | API | |------------|-----------------|-----| | Trigger | `SMITrigger \| null` | `.fire()` — one-shot event | | Boolean | `SMIBoolean \| null` | `.value` (get/set boolean) | | Number | `SMINumber \| null` | `.value` (get/set number) |

Replacing img + motion.div

**Before:**

<motion.div
  animate={{ y: [0, -3, 0] }}
  transition={{ duration: 4, repeat: Infinity }}
  style={{ width: 400, height: 400 }}
>
  <img src="/sprites/player.png" alt="player" style={{ width: '100%' }} />
</motion.div>

**After (Rive):**

<div style={{ width: 400, height: 400 }}>
  <RiveComponent />
</div>

Remove `motion.div` entirely. Idle bob lives in the `.riv` file. Do not wrap `RiveComponent` in `motion.div`.

Wiring to Zustand Combat Store

Zustand is source of truth. Bridge to Rive inputs via `useEffect`. Never call `input.fire()` inside Zustand actions — keep Rive coupling in the component.

import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
import { useCombatStore } from '../stores/combatStore';
import playerRiv from '../assets/characters/player.riv?url';

const SM = 'CombatStateMachine';

function PlayerCharacter() {
  const { RiveComponent, rive } = useRive({ src: playerRiv, stateMachines: SM, autoplay: true });

  const attackTrigger = useStateMachineInput(rive, SM, 'attack');
  const hitTrigger    = useStateMachineInput(rive, SM, 'hit');
  const blockInput    = useStateMachineInput(rive, SM, 'isBlocking');

  // Subscribe only to what this component needs
  const lastAction = useCombatStore(s => s.playerLastAction);
  const isBlocking = useCombatStore(s => s.playerIsBlocking);

  useEffect(() => {
    if (lastAction.type === 'attack' && attackTrigger) attackTrigger.fire();
    if (lastAction.type === 'hit'    && hitTrigger)    hitTrigger.fire();
  }, [lastAction, attackTrigger, hitTrigger]);

  useEffect(() => {
    if (blockInput) blockInput.value = isBlocking;
  }, [isBlocking, blockInput]);

  return <div style={{ width: 400, height: 400 }}><RiveComponent /></div>;
}

Timestamp trick for re-triggering same action type

`useEffect` won't re-fire for consecutive identica

Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked