Using Pixi.js in React with ReactPixi

Integrating the high-performance 2D rendering engine Pixi.js with React’s declarative UI can be challenging because Pixi.js relies on an imperative canvas API, while React manages a virtual DOM. This article explains how to bridge this gap using the official wrapper library @pixi/react (formerly ReactPixi). You will learn how to set up a PixiJS stage within a React application, handle state updates, create interactive graphics, and apply performance optimization techniques to build seamless 2D web experiences.

Why Use ReactPixi?

Directly manipulating a Pixi.js canvas inside a React component using standard React hooks (like useEffect) can quickly lead to messy, unmaintainable code. @pixi/react solves this by providing custom React fiber reconcilers. This allows you to write PixiJS code declaratively using JSX tags such as <Stage />, <Container />, and <Sprite />.

By using ReactPixi, any changes in your React state automatically propagate to your PixiJS elements, giving you the best of both worlds: React’s state management and PixiJS’s WebGL rendering speeds.

Setting Up Your First ReactPixi Application

To get started, you need to install the required packages. Run the following command in your terminal:

npm install pixi.js @pixi/react

Once installed, you can render a basic PixiJS application using React components. The following code demonstrates how to set up a viewport (Stage) and render a simple graphic:

import React from 'react';
import { Stage, Graphics } from '@pixi/react';

const MyPixiApp = () => {
  const drawCircle = (g) => {
    g.clear();
    g.beginFill(0xde3249);
    g.drawCircle(150, 150, 50);
    g.endFill();
  };

  return (
    <Stage width={300} height={300} options={{ backgroundColor: 0x1099bb }}>
      <Graphics draw={drawCircle} />
    </Stage>
  );
};

export default MyPixiApp;

The <Stage /> component creates the canvas element and initializes the PixiJS Application instance. The <Graphics /> component is used to draw vector shapes inside that canvas.

Managing State and Interaction

To make your canvas interactive, you can bind React state to PixiJS component properties. When the React state changes, the wrapper library efficiently updates only the changed properties of the underlying PixiJS objects.

import React, { useState } from 'react';
import { Stage, Sprite } from '@pixi/react';

const InteractiveCanvas = () => {
  const [position, setPosition] = useState({ x: 100, y: 100 });

  const moveSprite = () => {
    setPosition({
      x: Math.random() * 400,
      y: Math.random() * 400,
    });
  };

  return (
    <Stage width={500} height={500} options={{ backgroundColor: 0x1099bb }}>
      <Sprite
        image="https://pixijs.com/assets/bunny.png"
        x={position.x}
        y={position.y}
        interactive={true}
        pointerdown={moveSprite}
        anchor={0.5}
      />
    </Stage>
  );
};

In this example, clicking the bunny sprite triggers the moveSprite function. This updates the React state, which immediately updates the x and y props of the <Sprite /> component, causing the bunny to move on the screen.

Accessing the Raw PixiJS Instance

There are scenarios where you need direct access to the underlying PixiJS objects—such as setting up complex filters, shaders, or accessing the PixiJS Ticker. You can achieve this using the useApp hook provided by @pixi/react.

import React, { useEffect } from 'react';
import { Stage, useApp } from '@pixi/react';

const TickerComponent = () => {
  const app = useApp();

  useEffect(() => {
    const tick = (delta) => {
      // Custom game loop logic using the raw PixiJS app instance
    };

    app.ticker.add(tick);
    return () => app.ticker.remove(tick);
  }, [app]);

  return null;
};

// Usage
const App = () => (
  <Stage>
    <TickerComponent />
  </Stage>
);

Note: Custom hooks like useApp can only be used in components that are children of the <Stage /> component, as they rely on React Context.

Best Practices for Performance

While @pixi/react makes integration easy, combining React’s virtual DOM reconciliation with WebGL can sometimes introduce performance bottlenecks. Use these strategies to keep your application running at 60 FPS: