Skill: ravenquasar/react-game-loop (version 1.0.0)

--- name: react-hooks-game-loop description: Create custom React hooks in TypeScript for game loops and animations. Use when the user asks for a React hook, requestAnimationFrame integration, game loop pattern, or delta-time handling. Handles stale-closure safety, unmount cleanup, error resilience, and configurable parameters. --- # React Game Loop Hooks Production-ready custom React hooks for game loops and animations using `requestAnimationFrame`. ## Installation Copy the hook file into your project: ```bash # From your skill directory cp <skill-dir>/assets/hooks/useGameLoop.ts src/hooks/ ``` > `<skill-dir>` resolves to your OpenClaw skill installation directory. ## Dependencies - **React** >= 16.8 (hooks support) - **TypeScript** >= 4.0 (for type safety) - **react-dom** >= 16.8 (for `requestAnimationFrame` types in browser environment) This is a browser-only hook — it requires `requestAnimationFrame`, which is not available in Node.js or SSR environments. ## API Reference ### `useGameLoop(options)` ```ts import { useGameLoop } from './hooks/useGameLoop'; ``` **Parameters:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `onFrame` | `(deltaTime: number, elapsedTime: number) => void` | — | Called every frame with delta (ms) and elapsed time (ms). **Required.** | | `maxDeltaTime` | `number` | `100` | Maximum delta clamp in ms. Prevents huge jumps after tab-switch or lag. | **Returns:** | Field | Type | Description | |-------|------|-------------| | `start` | `() => void` | Start the game loop | | `stop` | `() => void` | Stop the game loop | | `isRunning` | `boolean` | Current loop state | ## Patterns ### Stale-closure safety Use `useRef` for callbacks/state read inside async/closure contexts (e.g. `requestAnimationFrame`, `setInterval`, event listeners). Always sync ref in `useEffect`. ```ts const cbRef = useRef(onFrame); useEffect(() => { cbRef.current = onFrame; }, [onFrame]); ``` ### Cleanup on unmount Always return cleanup from `useEffect`. For RAF/intervals, store handle in ref and cancel on unmount. ```ts useEffect(() => { const id = requestAnimationFrame(tick); return () => cancelAnimationFrame(id); }, []); ``` ### Error resilience Wrap user callbacks in `try/catch` so errors don't break internal loop state. ```ts try { callbackRef.current(delta, elapsed); } catch (err) { console.error('hook callback error:', err); } ``` ### Delta-time clamping For game loops, clamp `deltaTime` to avoid huge jumps after tab-switch or lag spikes. ```ts const delta = Math.min(rawDelta, maxDeltaTime); ``` ## Example See `assets/examples/GameTimer.tsx` for a complete usage example showing elapsed time and FPS display. ## Reference - `assets/hooks/useGameLoop.ts` — requestAnimationFrame-based game loop with start/stop/isRunning controls, deltaTime clamping, and error resilience - `assets/examples/GameTimer.tsx` — basic usage example

path: assets/examples/GameTimer.tsx
content: import React, { useState, useCallback } from 'react';
import { useGameLoop } from '../hooks/useGameLoop';

export default function GameTimer() {
  const [elapsed, setElapsed] = useState(0);
  const [fps, setFps] = useState(0);

  const onFrame = useCallback((delta: number, total: number) => {
    setElapsed(total);
    setFps(Math.round(1000 / delta));
  }, []);

  const { start, stop, isRunning } = useGameLoop({ onFrame, maxDeltaTime: 100 });

  return (
    <div style={{ fontFamily: 'sans-serif', padding: 24 }}>
      <h1>⏱ Game Timer</h1>
      <p>Elapsed: {(elapsed / 1000).toFixed(2)} s</p>
      <p>FPS: {fps}</p>
      <button onClick={start} disabled={isRunning}>Start</button>
      <button onClick={stop} disabled={!isRunning}>Stop</button>
    </div>
  );
}


path: assets/hooks/useGameLoop.ts
content: /**
 * useGameLoop — Browser game loop via requestAnimationFrame
 */
import { useCallback, useEffect, useRef, useState } from 'react';

export interface UseGameLoopOptions {
  /** Called on every frame with (deltaTime, elapsedTime) in ms */
  onFrame: (deltaTime: number, elapsedTime: number) => void;
  /** Max delta clamp in ms (default 100) */
  maxDeltaTime?: number;
}

export interface UseGameLoopReturn {
  start: () => void;
  stop: () => void;
  isRunning: boolean;
}

export function useGameLoop({ onFrame, maxDeltaTime = 100 }: UseGameLoopOptions): UseGameLoopReturn {
  const [isRunning, setIsRunning] = useState(false);

  // Stale-closure-safe refs
  const callbackRef = useRef(onFrame);
  useEffect(() => { callbackRef.current = onFrame; }, [onFrame]);

  const isRunningRef = useRef(isRunning);
  useEffect(() => { isRunningRef.current = isRunning; }, [isRunning]);

  const rafRef = useRef<number | null>(null);
  const lastTimestampRef = useRef<number | null>(null);
  const elapsedTimeRef = useRef(0);

  const tick = useCallback((timestamp: number) => {
    if (!isRunningRef.current) return;

    if (lastTimestampRef.current !== null) {
      let delta = timestamp - lastTimestampRef.current;
      if (delta > maxDeltaTime) delta = maxDeltaTime;
      elapsedTimeRef.current += delta;
      try {
        callbackRef.current(delta, elapsedTimeRef.current);
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error('useGameLoop callback error:', err);
      }
    }

    lastTimestampRef.current = timestamp;
    rafRef.current = requestAnimationFrame(tick);
  }, [maxDeltaTime]);

  const start = useCallback(() => {
    if (isRunningRef.current) return;
    setIsRunning(true);
    lastTimestampRef.current = null; // skip stale timestamp
    rafRef.current = requestAnimationFrame(tick);
  }, [tick]);

  const stop = useCallback(() => {
    if (!isRunningRef.current) return;
    setIsRunning(false);
    if (rafRef.current !== null) {
      cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    }
  }, []);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
        rafRef.current = null;
      }
    };
  }, []);

  return { start, stop, isRunning };
}


path: clawhub.json
content: {
  "name": "react-hooks-game-loop",
  "displayName": "React Game Loop Hooks",
  "version": "1.0.0",
  "description": "Production-ready React hooks for game loops and animations. Includes useGameLoop with stale-closure safety, delta-time clamping, and error resilience.",
  "author": "Frieren",
  "license": "MIT",
  "pricing": {
    "model": "free"
  },
  "tags": ["react", "hooks", "typescript", "game", "animation", "raf"],
  "minOpenClawVersion": "1.2.0"
}

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)