Combining React and PixiJS

Charles King About a 11 minute read

Here at Rincon Strategies, we are big fans ReactJS and PixiJS. The former gives you a new way to build your web applications, with a focus on performance and a declarative style. The latter is a blazing fast and feature rich webGL rendering engine for games and multimedia in the browser.

Both have their strengths and weaknesses, so when needed, we love the pattern of mixing both in a project and utilizing the React Component Life Cycle to to wrap our PixiJS game component and gain some added benefits of a declarative ReactJS application.

LayoutFigure 1

Let's say you have a webapp that will mix some React components with a Game canvas and you need to keep some shared state in both. e.g. Control button states, inventory etc. Drawing buttons and handling various click, hover, and toggled states is a very imperative process in PixiJS. So if your interactive components don't need to be in the canvas, move them out and let React handle those elements, and pass props to the Pixi game, which can in turn, update what needs to be updated via the Component Life Cycle hooks.

PixiJS Canvas Component

To show how we do this at Rincon, let's setup a simple app to zoom the PixiJS Canvas from a button outside the game scene-

import React, { Component, PropTypes } from 'react';
import PIXI from "pixi.js"

export default class Canvas extends Component {

        /**
        * Define our prop types
        **/
        static propTypes = {
            zoomLevel: PropTypes.number.isRequired
        };

        constructor( props ) { 
            super(props);
        }
        
        /**
        * In this case, componentDidMount is used to grab the canvas container ref, and 
        * and hook up the PixiJS renderer
        **/
        componentDidMount() {
           //Setup PIXI Canvas in componentDidMount
           this.renderer = PIXI.autoDetectRenderer(1366, 768);
           this.refs.gameCanvas.appendChild(this.renderer.view);
           
           // create the root of the scene graph
           this.stage = new PIXI.Container();
           this.stage.width = 1366;
           this.stage.height = 768;
        }
        /**
        * Render our container that will store our PixiJS game canvas. Store the ref
        **/
        render() {
            return (
                    <div className="game-canvas-container" ref="gameCanvas">              
                    </div>
             );
        }
}

In this section, we setup our Canvas component to accept a very simple zoomLevel PropType, render a container to hold our game canvas, and then use the lifecycle method componentDidMount to hook up our PixiJS Renderer. The important piece here is that we have to use componentDidMount to setup the Pixi Canvas, since that method runs when the DOM has been rendered on the client and we can access the required node via refs.

The next step is to start the PixiJS game loop.

import React, { Component, PropTypes } from 'react';
import PIXI from "pixi.js"

export default class Canvas extends Component {

        /**
        * Define our prop types
        **/
        static propTypes = {
            zoomLevel: PropTypes.number.isRequired
        };

        constructor( props ) { 
            super(props);
            
            //bind our animate function
            this.animate = this.animate.bind(this);
        }
        
        /**
        * In this case, componentDidMount is used to grab the canvas container ref, and 
        * and hook up the PixiJS renderer
        **/
        componentDidMount() {
           //Setup PIXI Canvas in componentDidMount
           this.renderer = PIXI.autoDetectRenderer(1366, 768);
           this.refs.gameCanvas.appendChild(this.renderer.view);
           
           // create the root of the scene graph
           this.stage = new PIXI.Container();
           this.stage.width = 1366;
           this.stage.height = 768;
           
           //start the game
           this.animate();
        }
        /**
        * Animation loop for updating Pixi Canvas
        **/
        animate() {
            // render the stage container
            this.renderer.render(this.stage);
            this.frame = requestAnimationFrame(this.animate);
        }
        
        /**
        * Render our container that will store our PixiJS game canvas. Store the ref
        **/
        render() {
             return (
                    <div className="game-canvas-container" ref="gameCanvas">              
                    </div>
             );
        }
}

We accomplish this by making a simple animation function, which we pre-bind in the constructor. This function is called once in componentDidMount and recursively renders our stage with the PixiJS renderer. Now that the plumbing is out of the way, lets have it do something interesting.

import React, { Component, PropTypes } from 'react';
import PIXI from "pixi.js"

export default class Canvas extends Component {

        /**
        * Define our prop types
        **/
        static propTypes = {
            zoomLevel: PropTypes.number.isRequired
        };

        constructor( props ) { 
            super(props);
            
            //bind our animate function
            this.animate = this.animate.bind(this);
            //bind our zoom function
            this.updateZoomLevel = this.updateZoomLevel.bind(this);
        }
        
        /**
        * In this case, componentDidMount is used to grab the canvas container ref, and 
        * and hook up the PixiJS renderer
        **/
        componentDidMount() {
           //Setup PIXI Canvas in componentDidMount
           this.renderer = PIXI.autoDetectRenderer(1366, 768);
           this.refs.gameCanvas.appendChild(this.renderer.view);
           
           // create the root of the scene graph
           this.stage = new PIXI.Container();
           this.stage.width = 1366;
           this.stage.height = 768;
           
           //start the game
           this.animate();
        }
        /**
        * shouldComponentUpdate is used to check our new props against the current
        * and only update if needed
        **/
        shouldComponentUpdate(nextProps, nextState) {
            //this is easy with 1 prop, using Immutable helpers make 
            //this easier to scale
            
            return nextProps.zoomLevel !== this.props.zoomLevel;
        }
        /**
        * When we get new props, run the appropriate imperative functions 
        **/
        componentWillReceiveProps(nextProps) {
            this.updateZoomLevel(nextProps);
        }
        
        /**
        * Update the stage "zoom" level by setting the scale
        **/
        updateZoomLevel(props) {
            this.stage.scale.x = props.zoomLevel;
            this.stage.scale.y = props.zoomLevel;
        }
        
        /**
        * Animation loop for updating Pixi Canvas
        **/
        animate() {
            // render the stage container
            this.renderer.render(this.stage);
            this.frame = requestAnimationFrame(this.animate);
        }
        
        /**
        * Render our container that will store our PixiJS game canvas. Store the ref
        **/
        render() {
            return (
                    <div className="game-canvas-container" ref="gameCanvas">              
                    </div>
            );
        }
}

Here we created a new function updateZoomLevel, bound it in the constructor, and called it when we receive new props. One important thing to notice here, is we used two new component life cycle methods, shouldComponentUpdate and componentWillReceiveProps. shouldComponentUpdate is used to determine whether we should bother running making any Pixi updates, by comparing the new zoom level to the old zoom level. This could technically be done inside the updateZoomLevel function, but it's a good practice to keep the methods as simple as possible. Finally, componentWillReceiveProps is used to call our various imperative methods to update the game scene.

React Application Component

Voila! Now, our React application can easily use this game Canvas and externally control the zoom level of the Pixi game using React's wonderful declarative style and synthetic event system.

import React, { Component } from 'react';
import Canvas from "./canvas";

export default class Application extends Component {
    constructor(props) {
        super(props);
      
        //store our zoom level in state
        this.state = {
            zoomLevel:1.0
        };
        
        //pre bind our zoom handlers
        this.onZoomIn = this.onZoomIn.bind(this);
        this.onZoomOut = this.onZoomOut.bind(this);
    }
    /**    
     * Event handler for clicking zoom in. Increments the zoom level 
     **/
    onZoomIn() {
        let zoomLevel = this.state.zoomLevel += .1;
        this.setState({zoomLevel});
    }
    /**    
     * Event handler for clicking zoom out. Decrements the zoom level 
     **/
    onZoomOut() {
        let zoomLevel = this.state.zoomLevel -= .1;
        
        if (zoomLevel >= 0) {
            this.setState({zoomLevel});
        }
        
    }
        
    render() {
         return (
             <div>
                <button onClick={this.onZoomIn}>Zoom In</button>
                <button onClick={this.onZoomOut}>Zoom Out</button>
                <Canvas zoomLevel={this.state.zoomLevel}/>
             </div>
         );
    }
}

Conclusion

As with a lot of React JS examples, the benefits of this approach might not become clear until you application grows. However, we have used this pattern in large applications and it certainly helps reign in the complexity of the very imperative (but powerful) style of writing a PixiJS application.

Extra

Wanna follow along and are having trouble getting PixiJS to work with Webpack? Michael Jackson posted a great tweet on how to get this working.

Comments