Using PixiJS inside a React web app

  1. Overview
  2. Getting started
  3. Setting up PixiJS
  4. Creating a React wrapper
  5. Wrapping up

Overview

A couple of months ago, I was looking for a JavaScript library to use for game projects. I have used Love2D in the past, as well as some of Apple's game frameworks, but I wanted something that would work in the browser. I found PixiJS, which is a 2D rendering engine. It's a lightweight library that, so it can be used to create games and other interactive experiences and isn't too opinionated about how you structure your code.

I wanted something that would work with React since I only wanted the game to be a part of the page. PixiJS does have a React wrapper called pixi-react, but all of the documentation is for writing the graphics code in React syntax instead of using the PixiJS API directly.

Here is what the React syntax looks like:

<Stage width={300} height={300} options={{ backgroundColor: 0xeef1f5 }}>
  <Container position={[150, 150]}>
    <Sprite anchor={0.5} x={-75} y={-75} image="/pixi-react/img/bunny.png" />
    <Sprite anchor={0.5} x={0} y={0} image="/pixi-react/img/bunny.png" />
    <Sprite anchor={0.5} x={75} y={75} image="/pixi-react/img/bunny.png" />
  </Container>
</Stage>

Here is what the PixiJS API looks like:

const app = new PIXI.Application({ width: 300, height: 300, backgroundColor: 0xeef1f5 })
document.body.appendChild(app.view)

const container = new PIXI.Container()
container.position.set(150, 150)
app.stage.addChild(container)

const bunny = PIXI.Sprite.from('/pixi-react/img/bunny.png')
bunny.anchor.set(0.5)
bunny.x = -75
bunny.y = -75
container.addChild(bunny)

const bunny2 = PIXI.Sprite.from('/pixi-react/img/bunny.png')
bunny2.anchor.set(0.5)
bunny2.x = 0
bunny2.y = 0
container.addChild(bunny2)

const bunny3 = PIXI.Sprite.from('/pixi-react/img/bunny.png')
bunny3.anchor.set(0.5)
bunny3.x = 75
bunny3.y = 75
container.addChild(bunny3)

I spent some time fighting with PixiJS and React to get them to work together, but I finally got it working.

Getting started

I set up a sample project on GitHub that you can use to follow along. The sample project covers the basics of using PixiJS in React, but I will go over the main points here.

You will need a recent version of Node.js installed to run the sample project. You can check your version with node -v.

This is what I currently have installed:

$ node -v
v19.5.0

After you have downloaded the sample project, you can install the dependencies with npm install.

$ npm install

and then start the development server with npm run dev.

$ npm run dev

After the development server starts, you can navigate to http://localhost:5173/ to see the sample project. You will see a simple web app with an animation of React logos. This is similar to the example in the PixiJS getting started guide.

web app demo image

Setting up PixiJS

There are two PixiJS packages that we need to install to use PixiJS in React. One is the main PixiJS package, and the other is the React wrapper.

$ npm install pixi.js @pixi/react

Creating a React wrapper

Now that we have PixiJS installed, we can create a React component to wrap the PixiJS code. We can create a Pixi Canvas by adding a Stage component to our App.

import { Stage } from "@pixi/react";

function App() {
  return (
    <>
      <h1>Vite + React + PixiJS</h1>
      <Stage width={800} height={500}>
      </Stage>
    </>
  );
}

This will create a blank canvas to which we can add a custom React Pixi component.

import "./App.css";
import PixiApp from "./pixi";
import { Stage } from "@pixi/react";

function App() {
  return (
    <>
      <h1>Vite + React + PixiJS</h1>
      <Stage width={800} height={500}>
        <PixiApp />
      </Stage>
    </>
  );
}

Here is the PixiApp component:

import { useApp } from '@pixi/react';

export default function PixiApp() {
  let app = useApp();
  return (
    <></>
  );
}

The useApp hook gives us access to the PixiJS application instance. This is what we need to execute the PixiJS code directly. We could start executing PixiJS code here, but I prefer to keep the PixiJS code separate from the React code. Let's add another component to hold the PixiJS code.

import { Container} from 'pixi.js';

export default function run(app) {
    const container = new Container();
    app.stage.addChild(container);
}

Then we can adjust our PixiApp component to use the run function.

import { useApp } from '@pixi/react';
import run from './pixi-example';

export default function PixiApp() {
  let app = useApp();
  app.stage.removeChildren();
  run(app);

  return (
    <></>
  );
}

Note that we remove all of the children on from the stage before running the PixiJS code so the PixiJS code can be run multiple times without adding more children to the stage. There are other ways around this problem, but this is the simplest workaround when re-rendering react components.

Also, if you need to access the Window, you will need to call the run function in a useEffect hook.

import { useApp } from '@pixi/react';
import run from './pixi-example';

export default function PixiApp() {
  let app = useApp();

  useEffect(() => {
    app.stage.removeChildren();
    run(app);
  }, [app]);

  return (
    <></>
  );
}

Wrapping up

Now that we have the React wrapper set up, we can start writing PixiJS code.

import { Container, Sprite, Texture} from 'pixi.js';
import reactLogo from './assets/react.svg'

export default function run(app) {
    const container = new Container();
    app.stage.addChild(container);

    // Create a new texture
    const texture = Texture.from(reactLogo);

    // Create a 5x5 grid of bunnies
    for (let i = 0; i < 25; i++) {
        const logo = new Sprite(texture);
        logo.anchor.set(0.5);
        logo.x = (i % 5) * 40;
        logo.y = Math.floor(i / 5) * 40;
        container.addChild(logo);
    }

    // Move container to the center
    container.x = app.screen.width / 2;
    container.y = app.screen.height / 2;

    // Center logo sprite in local container coordinates
    container.pivot.x = container.width / 2;
    container.pivot.y = container.height / 2;

    // Listen for animate update
    app.ticker.add((delta) => {
        // rotate the container!
        // use delta to create frame-independent transform
        container.rotation -= 0.01 * delta;
    });
}