React

I Made a React Game: 3 Brutal Mistakes to Avoid in 2025

Just built a game in React? I share 3 brutal, performance-killing mistakes I made so you can avoid them in 2025. Learn about state, rendering, and more.

A

Alex Donovan

Senior Frontend Engineer specializing in React performance and creative web applications.

6 min read14 views

I Made a React Game: 3 Brutal Mistakes to Avoid in 2025

"I know React," I told myself, brimming with a dangerous level of confidence. "How hard can it be to build a simple 2D browser game?"

Famous last words.

My journey started with excitement. I envisioned a cool side-scroller, maybe with some quirky physics. I'd leverage my existing skills, build a declarative UI for the menus, and have a playable prototype in a weekend. But the reality was a laggy, stuttering mess. The browser tab's memory usage skyrocketed, and my laptop fan sounded like it was preparing for takeoff. My dream of a silky-smooth 60 FPS was dead on arrival.

The problem wasn't React. The problem was how I was using it. I was trying to force a UI library to behave like a dedicated game engine, and it was fighting me every step of the way. After weeks of painful refactoring and performance profiling, I finally got it right. In this post, I'm sharing the three most brutal mistakes I made so you can skip the headache and start building fun, performant games with React today.

Mistake #1: Treating Everything as React State

This was my first and most catastrophic error. As a React developer, your instinct is to put any dynamic value into state using useState. Score? State. Player name? State. Player's X/Y coordinates? State. This works perfectly for UIs, but for a game loop, it's performance suicide.

A game needs to update dozens of values—positions, velocities, animations—up to 60 times per second. If each update triggers a setState call, you're telling React to re-evaluate and potentially re-render your entire component tree 60 times a second. This is the source of the lag and stutter. React's reconciliation algorithm is fast, but it's not designed for that kind of high-frequency, granular update.

The `useRef` and `requestAnimationFrame` Solution

The solution is to separate your game state (high-frequency data like positions) from your UI state (low-frequency data like score or health).

  1. Game State: Store all your rapidly changing data in a useRef hook. A ref is a mutable object that persists for the lifetime of the component. Crucially, updating a ref's .current property does not trigger a re-render.
  2. Game Loop: Run your game logic (physics, input checks, position updates) inside a requestAnimationFrame loop. This is the browser's optimized way to run animations, ensuring your code executes right before the next paint.
  3. UI State: Use useState or useReducer only for things that the user actually sees change in the DOM, like the score, a health bar, or a "Game Over" screen. You can update this state from within your game loop, but far less frequently.
Advertisement

Here's a comparison of the mental models:

AspectThe Wrong Way (useState)The Right Way (useRef)
State Containerconst [position, setPosition] = useState({x: 0, y: 0})const playerRef = useRef({x: 0, y: 0})
Update MechanismsetPosition(newPos) -> Triggers re-renderplayerRef.current = newPos -> No re-render
PerformanceVery poor. The component tree re-renders on every frame.Excellent. Logic is decoupled from rendering.
Best ForUI data (score, lives, game status)Game logic data (positions, velocities, timers)

Mistake #2: Creating a Component for Every Game Object

My next brilliant idea was to embrace React's component model. I'd have a <Player /> component, and an array of 100 enemies would be rendered with enemies.map(e => <Enemy key={e.id} ... />). Each component would be a `div` styled with absolute positioning.

This seems intuitive, but it's deeply inefficient for two reasons:

  1. DOM Bloat: You're creating hundreds, maybe thousands, of DOM nodes. The browser has to manage all of them, which is slow.
  2. Reconciliation Overhead: Even if you memoize components, React still has to do the work of diffing the virtual DOM against the actual DOM for every single moving object on every frame. This overhead adds up quickly.

Embrace the Canvas

The industry-standard solution for 2D (and 3D) web games is the <canvas> element. It's a single DOM node that gives you a blank slate to draw on using JavaScript's imperative Canvas API.

Your React architecture shifts completely:

  • You have a single <GameCanvas /> component.
  • Inside this component, you manage your game loop with requestAnimationFrame and your game objects as a simple array of JavaScript objects in a useRef (e.g., [{id: 1, x: 10, y: 20, sprite: '...'}, ...]).
  • On every frame, you clear the canvas and iterate through your array of objects, drawing each one onto the canvas at its current position.

This approach bypasses the React reconciliation process for game objects entirely. React manages the single <canvas> element, but everything inside is pure, fast, imperative drawing. You get the best of both worlds: React for your UI and menus, and a high-performance canvas for the game itself.

// The Bad Way: Component for every object
function Game() {
  const [enemies, setEnemies] = useState([...]);

  // Game loop updates state, causing constant re-renders
  return (
    <div className="game-world">
      <Player />
      {enemies.map(enemy => <Enemy key={enemy.id} position={enemy.position} />)}
    </div>
  );
}

// The Good Way: Single canvas component
function Game() {
  const canvasRef = useRef(null);
  const gameObjectsRef = useRef({ player: {...}, enemies: [...] });

  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    let animationFrameId;

    const gameLoop = () => {
      // 1. Update positions in gameObjectsRef.current
      // 2. Clear canvas: context.clearRect(0, 0, canvas.width, canvas.height)
      // 3. Draw everything from gameObjectsRef.current onto the canvas
      animationFrameId = requestAnimationFrame(gameLoop);
    }
    gameLoop();

    return () => cancelAnimationFrame(animationFrameId);
  }, []);

  return <canvas ref={canvasRef} />;
}

Mistake #3: Reinventing the Wheel (Especially Physics)

Once I figured out state and rendering, I hit the next wall: implementing game mechanics. I spent a full day writing a wonky collision detection function. Then I tried to implement gravity and friction. It was a buggy, time-consuming nightmare, and I wasn't even working on the *fun* parts of my game yet.

Don't be a hero. Unless you're a physics expert or your goal is to learn how to build a physics engine, use a library. The React and JavaScript ecosystems are packed with incredible tools that handle the hard stuff for you.

Stand on the Shoulders of Giants: The React Game Dev Ecosystem

Using a library isn't cheating; it's being efficient. These libraries are highly optimized and battle-tested. They let you focus on game design, not on trigonometry.

Here are some essential library categories and top-tier recommendations for 2025:

CategoryRecommended LibraryWhat it Does
Rendering Enginereact-three-fiberThe gold standard. Provides a declarative React component API for the powerful Three.js 3D library. It handles the canvas, loop, and rendering for you.
2D Graphicsreact-konvaA great choice for 2D games and complex graphics. It gives you a React-like API for the Konva 2D canvas library.
Physics@react-three/rapierA wrapper for the blazing-fast Rapier physics engine, designed to work seamlessly with react-three-fiber. Handles collision, gravity, forces, etc.
State ManagementZustandA small, fast, and unopinionated state manager. Perfect for managing global UI state (score, game phase) without the boilerplate of Redux.

By combining react-three-fiber for rendering and @react-three/rapier for physics, you can build a surprisingly complex 3D game with a familiar React component syntax, while they handle the messy, performance-critical parts under the hood.

Putting It All Together: React is a UI Library, Not a Game Engine

The overarching lesson is this: use React for what it's best at—managing UI. Your game's main menu, settings screen, heads-up display (HUD), and inventory are all perfect use cases for React's component model and state management.

For the core, high-performance game loop, let dedicated tools do the heavy lifting. That means:

  • Separating state: Use useRef for game logic, useState for UI.
  • Rendering on a canvas: Avoid creating a DOM node for every game object.
  • Leveraging the ecosystem: Use libraries for rendering, physics, and input to save time and gain performance.

Building a game in React is not only possible but can be an incredibly fun and rewarding experience. By avoiding these common pitfalls, you can harness the power of React's ecosystem to build the UI while relying on proven, high-performance techniques for the game itself. Now go build something amazing!

Tags

You May Also Like