6.4 KiB
Agent Development Guidelines - The Last StarTyper
React 19 Best Practices
⚠️ CRITICAL RULES - NO EXCEPTIONS
- NO useCallback - React 19's compiler handles optimization automatically
- NO useMemo - React 19's compiler handles memoization automatically
- NO functions as useEffect dependencies - Only use primitive values or state values
Example - What NOT to do:
// ❌ BAD - Uses useCallback
const handleClick = useCallback(() => {
doSomething();
}, [dependency]);
// ❌ BAD - Function as dependency
useEffect(() => {
handleClick();
}, [handleClick]); // Never use functions as dependencies!
Example - What to do instead:
// ✅ GOOD - Plain function
const handleClick = () => {
doSomething();
};
// ✅ GOOD - Only value dependencies
useEffect(() => {
handleClick();
}, [someStateValue]); // Only primitive/state values
Event Handler Best Practices
Avoid Stale Closures
When passing objects with methods to hooks, access properties directly inside handlers to avoid stale closures:
// ❌ BAD - Destructuring creates stale references
const { gameStatus, startGame } = gameManager;
useEffect(() => {
if (gameStatus === 'ready') startGame();
}, [gameStatus]);
// ✅ GOOD - Direct access ensures fresh values
useEffect(() => {
if (gameManager.gameStatus === 'ready') {
gameManager.startGame();
}
}, [gameManager.gameStatus]);
Keyboard Event Handling
Always prevent default behavior when handling game controls:
const handler = (ev: KeyboardEvent) => {
if (ev.code === "Space") {
ev.preventDefault(); // Prevent button activation
// handle space key
}
if (ev.key === "Escape") {
ev.preventDefault(); // Prevent dialog close
// handle escape key
}
};
UI/UX Considerations
Button Focus Management
Buttons retain focus after clicking, which can interfere with keyboard controls:
// Always blur buttons after clicking to prevent Space key from re-triggering
onClick={(e) => {
handleAction();
e.currentTarget.blur(); // Remove focus
}}
Hook Architecture
State Management
- Use refs for values that need persistence without re-renders
- Use state only for values that trigger UI updates
- Keep game loop data in refs to avoid unnecessary renders
// Game loop data - use refs
const lettersRef = useRef<FallingLetter[]>([]);
const playerXRef = useRef<number>(400);
// UI state - use useState
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
const [score, setScore] = useState(0);
Effect Dependencies
Only include values that actually trigger the effect logic:
// ✅ GOOD - Only necessary dependencies
useEffect(() => {
gameManager.setGameMode(gameMode);
}, [gameMode]); // Only the value that changes
// ❌ BAD - Including objects/functions
useEffect(() => {
gameManager.setGameMode(gameMode);
}, [gameMode, gameManager]); // gameManager is unnecessary
Common Pitfalls & Solutions
Problem: Game state resets unexpectedly
Cause: Multiple state updates or function calls in wrong order Solution: Check for race conditions, ensure proper state checks before updates
Problem: Keyboard controls not working
Cause: DOM element has focus (usually buttons) Solution: Blur active elements, use preventDefault()
Problem: Stale values in event handlers
Cause: Closure over old values when using destructuring Solution: Access object properties directly inside handlers
Problem: Infinite re-renders
Cause: Functions as dependencies or state updates in render Solution: Remove function dependencies, move state updates to effects
Game-Specific Patterns
Game Mode Changes
- Only quit active games when changing modes
- Don't reset state unnecessarily
- Preserve user context when possible
const changeGameMode = (mode: GameMode) => {
setGameModeState(mode);
// Only quit if actively playing
if (gameStatus === 'started') {
quitGame();
}
};
Animation Frame Loops
- Store animation frame IDs in refs
- Always cleanup on unmount
- Use refs for loop data to avoid re-renders
const gameLoopRef = useRef<number | null>(null);
const startLoop = () => {
const loop = (time: number) => {
// Update game state
gameLoopRef.current = requestAnimationFrame(loop);
};
gameLoopRef.current = requestAnimationFrame(loop);
};
const stopLoop = () => {
if (gameLoopRef.current) {
cancelAnimationFrame(gameLoopRef.current);
gameLoopRef.current = null;
}
};
Testing & Debugging
Browser Focus Issues
When debugging keyboard input issues:
- Check if any DOM element has focus (inspect element, check document.activeElement)
- Test with blur() on suspected elements
- Try unfocusing/refocusing browser window - if this fixes it, it's a focus issue
State Synchronization
When game state seems out of sync:
- Check for multiple state sources (refs vs state)
- Verify effect dependencies are correct
- Look for race conditions in async operations
- Add console logs at state transitions
Code Quality Standards
Type Safety
- Always use TypeScript types
- Avoid
anytype - Define explicit return types for hooks
Naming Conventions
- Hooks:
use[Feature](e.g.,useGame,useBoard) - Event handlers:
handle[Event](e.g.,handleTypedLetter) - Refs:
[name]Ref(e.g.,gameLoopRef,containerRef) - Managers:
[feature]Manager(e.g.,gameStatusManager)
File Organization
src/
├── use[Feature]/
│ ├── index.ts # Public exports
│ └── use[Feature].ts # Hook implementation
├── types.ts # All TypeScript types
└── index.ts # Package exports
Performance Considerations
Minimize Re-renders
- Use refs for non-UI state
- Keep component trees shallow
- Avoid passing new objects/arrays as props
Canvas Rendering
- Only redraw when necessary
- Use requestAnimationFrame for smooth animations
- Clear only changed regions when possible
Remember
- React 19 = No useCallback/useMemo
- No functions as dependencies
- Blur buttons after clicking
- preventDefault on game controls
- Access object properties directly in handlers
- Use refs for game loop data
- Test focus issues by unfocusing browser
When in doubt, keep it simple. React 19's compiler will optimize for you.