56

I have an application using heavily HTML5 canvas via Fabric.js. The app is written on top of Angular 1.x, and I am planning to migrate it to React. My app allows writing text and drawing lines, rectangles, and ellipses. It is also possible to move, enlarge, shrink, select, cut, copy, and paste one or more of such objects. It is also possible to zoom and pan the canvas using various shortcuts. In short, my app utilizes Fabric.js to its full extent.

I couldn't find much information on how to use Fabric.js together with React, so my concern is that 1. is it possible without major modifications, and 2. does it make sense or should I instead use some other extensive canvas library that has better support for React?

The only example of React+Fabric.js I could find, was react-komik, which however is much more simpler than my app. My main concerns are the event processing and DOM manipulation of Fabric.js, and their effect on React.

There seems to be also a canvas library for React, called react-canvas, but it seems lacking a lot of features compared to Fabric.js.

What do I have to take into account (regarding DOM manipulation, event processing, etc.) when using Fabric.js in a React app?

Kitanotori
  • 1,741
  • 5
  • 16
  • 23
  • 1
    You can take a look into https://github.com/lavrton/react-konva. I think it should be very easy to port into to Fabric (instead of Konva). – lavrton Jun 23 '16 at 15:29
  • That's interesting library, but it's not what I asked. I'm already using Fabric.js in large extend, so I prefer to avoid switching to other canvas library or tampering with Fabric.js internals too much, because it makes maintenance bothersome. – Kitanotori Aug 22 '16 at 10:14
  • 1
    react-faux-dom can be used with packages like d3 and fabric. – vijayst Aug 28 '16 at 09:20

6 Answers6

61

We had this same issue in our app of how to use Fabric.js inside of react. My recommendation is to treat fabric as an uncontrolled component. Have fabric instance that your whole app can talk to and make fabric calls and then when anything changes use .toObject() call to put the whole fabric state into your Redux store. Then your React app can read fabric state from your global Redux state as you would do in any normal React app.

I can't get an example working in the StackOverflow code editor but here is a JSFiddle example that implements the pattern I am recommending.

Antony
  • 1,253
  • 11
  • 19
StefanHayden
  • 3,569
  • 1
  • 31
  • 38
  • Calling toObject on each change of the state sounds like an overkill. Doesn't it make your app horribly slow? – Kitanotori Sep 29 '16 at 04:38
  • no. it's a very fast call. Also you don't call it when *anything* changes just at the end. So if you are dragging a shape you only call `toObject` at the very end on mouse up. – StefanHayden Sep 29 '16 at 12:14
  • 1
    Would it be possible to add your example on jsfiddle or anywhere else as it returns 404 now? Thank you. – Radomír Laučík Sep 30 '16 at 11:29
  • Thanks for the example. Could you please elaborate on the pros of adding a Redux store in the example above (or any app for drawing stuff)? The store only holds a copy of the fabric state, which the rest of the app can read from, but all changes/actions are done on the fabricCanvas. (I am a newbie in this area) – markussvensson Nov 10 '16 at 08:05
23

I've used Fabric for a proof-of-concept project and the general idea is the same as for, say, D3. Keep in mind that Fabric operates over DOM elements, while React renders data into DOM, and usually the latter is deferred. There are two things that will help you make sure your code works:

Wait until component is mounted

To do that, place your Fabric instantiation into componentDidMount:

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c');

    // do some stuff with it
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas id="c" />
      </div>
    )
  }
}

Placing Fabric constructor into componentDidMount ensures it won't fail because by the moment this method is executed, the DOM is ready. (but the props sometimes aren't, just in case if you use Redux)

Use refs to calculate actual width and height

Refs are references to actual DOM elements. You can do with refs what you can do with DOM elements using DOM API: select children, find parent, assign style properties, calculate innerHeight and innerWidth. The latter is precisely what you need:

componentDidMount() {
  const canvas = new fabric.Canvas('c', {
    width: this.refs.canvas.clientWidth,
    height: this.refs.canvas.clientHeight
  });

  // do some stuff with it
}

Don't forget to define refs property of this. To do that, you'll need a constructor. The whole thing would look like

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  constructor() {
    super()
    this.refs = {
      canvas: {}
    };
  }

  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c', {
      width: this.refs.canvas.clientWidth,
      height: this.refs.canvas.clientHeight
    });

    // do some stuff with it
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas
          id="c"
          ref={node => {
            this.refs.canvas = node;
          } />
      </div>
    )
  }
}

Mix Fabric with component state or props

You can make your Fabric instance react to any component props or state updates. To make it work, simply update your Fabric instance (which, as you could see, you can store as part of component's own properties) on componentDidUpdate. Simply relying on render function calls won't be really helpful because none of the elements that are rendered would ever change on new props or new state. Something like this:

import React, { Component } from 'react';
import { fabric } from 'react-fabricjs';
import styles from './MyComponent.css';

class MyComponent extends Component {
  constructor() {
    this.refs = {
      canvas: {}
    };
  }

  componentWillMount() {
    // dispatch some actions if you use Redux
  }

  componentDidMount() {
    const canvas = new fabric.Canvas('c', {
      width: this.refs.canvas.clientWidth,
      height: this.refs.canvas.clientHeight
    });

    this.fabric = canvas;

    // do some initial stuff with it
  }

  componentDidUpdate() {
    const {
      images = []
    } = this.props;
    const {
      fabric
    } = this;

    // do some stuff as new props or state have been received aka component did update
    images.map((image, index) => {
      fabric.Image.fromURL(image.url, {
        top: 0,
        left: index * 100 // place a new image left to right, every 100px
      });
    });
  }

  render() {
    return (
      <div className={styles.myComponent}>
        <canvas
          id="c"
          ref={node => {
            this.refs.canvas = node;
          } />
      </div>
    )
  }
}

Simply replace image rendering with the code you need and that depends on new component state or props. Don't forget to clean up the canvas before rendering new objects on it, too!

rishat
  • 8,206
  • 4
  • 44
  • 69
  • Can you please help me on this? I could not set Background image in canvas http://stackoverflow.com/questions/41581511/selected-image-is-not-shown-on-canvas – Serenity Jan 11 '17 at 02:12
  • Seems like you are experienced with reactjs and fabricjs. I need your help. I could set background image but could not set background color. https://stackoverflow.com/questions/37565041/how-can-i-use-fabric-js-with-react . Can you please see this? – pythonBeginner Jun 02 '17 at 01:12
  • I'm a bit confused how this works. I can't see `react-fabricjs` exposing `fabric` object anywhere. So this line: `import { fabric } from 'react-fabricjs';` creates an error for me. – Alex Jun 22 '18 at 18:44
  • I think my answer simply got outdated. If you solve the issue with the latest version of react-fabricjs, do you mind editing the answer or writing a new one? – rishat Jun 23 '18 at 19:48
10

With react 16.8 or newer you can also create a custom hook:

import React, { useRef, useCallback } from 'react';
const useFabric = (onChange) => {
    const fabricRef = useRef();
    const disposeRef = useRef();
    return useCallback((node) => {
        if (node) {
            fabricRef.current = new fabric.Canvas(node);
            if (onChange) {
                disposeRef.current = onChange(fabricRef.current);
            }
        }
        else if (fabricRef.current) {
            fabricRef.current.dispose();
            if (disposeRef.current) {
                disposeRef.current();
                disposeRef.current = undefined;
            }
        }
    }, []);
};

Usage

const FabricDemo = () => {
  const ref = useFabric((fabricCanvas) => {
    console.log(fabricCanvas)
  });
  return <div style={{border: '1px solid red'}}>
    <canvas ref={ref} width={300} height={200} />
  </div>
}
jantimon
  • 36,840
  • 23
  • 122
  • 185
  • can't you just do this--- ``` const useFabric = ( opts: { width, number }, onChange ) => { const ref = useRef(null); useEffect(() => { const fabricInst = new fabric.Canvas(ref.current); onChange(fabricInst); }, [onChange, opts.height, opts.width]); return ref; }; ``` – U Avalos Apr 22 '20 at 03:01
  • This won't work if the ref is `null` for the first `useEffect` run as changing the ref will not execute `useEffect` again – jantimon Apr 22 '20 at 08:04
  • Right but useEffect used this way is supposed to be equivalent to componentDidMount. If i'm not mistaken, the ref is guaranteed to exist by then – U Avalos Apr 22 '20 at 10:52
  • No because you could lazy render a div e.g. `return someCondition ?
    ...
    : null` - also shown in the official docs: https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
    – jantimon Jun 24 '20 at 10:22
  • I'm trying to use this but i'm not clever enough: when I later try to access ref.current (from FabricDemo above), it is always null. But the canvas is showing on the page ok – xaphod Sep 09 '20 at 15:28
  • Thanks jantimon, a super and elegant answer. Hat tip from me. :-) – Charlie Benger-Stevenson Nov 30 '21 at 09:26
6

I followed answer of StefanHayden and here is the test code.

Create Fabric Canvas Object

Create a custom hook to return a fabricRef(Callback Refs):

const useFabric = () => {
  const canvas = React.createRef(null);
  const fabricRef = React.useCallback((element) => {
    if (!element) return canvas.current?.dispose();

    canvas.current = new fabric.Canvas(element, {backgroundColor: '#eee'});
    canvas.current.add(new fabric.Rect(
      {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
    ));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return fabricRef;
};

Note that:

Finally, create a canvas and pass the ref:

function App() {
  const fabricRef = useFabric();
  return <canvas ref={fabricRef} width={640} height={360}/>;
}

But we can't use the fabricCanvas in other place, I will explain in next chapter.

Codepen

Share Fabric Canvas Object by Context

By Context which store the Refs mutable value, we could use the fabric canvas object anywhere.

First, we define the context, which take a ref as value:

const FabricContext = React.createContext();

function App() {
  return (
    <FabricContext.Provider value={React.createRef()}>
      <MyToolKit />
      <MyFabric />
    </FabricContext.Provider>
  );
}

Then, we can use it in custom hook. We don't create the ref now, but use the ref in context:

const useFabric = () => {
  const canvas = React.useContext(FabricContext);
  const fabricRef = React.useCallback((element) => {
    if (!element) return canvas.current?.dispose();

    canvas.current = new fabric.Canvas(element, {backgroundColor: '#eee'});
    canvas.current.add(new fabric.Rect(
      {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
    ));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return fabricRef;
};

function MyFabric() {
  const fabricRef = useFabric();
  return <canvas ref={fabricRef} width={640} height={360} />;
}

And we could also use it in anywhere, only when the context is available:

function MyToolKit() {
  const canvas = React.useContext(FabricContext);
  const drawRect = () => {
    canvas.current?.add(new fabric.Rect(
      {top: 100, left: 100, width: 100, height: 100, fill: 'red'}
    ));
  };
  return <button onClick={drawRect}>Draw</button>;
}

All over the lifecycle of App, it's able to use the fabric canvas object in anywhere now.

Codepen

Share Fabric Canvas Object by props

If don't want share by Context, like global variables, we could also share with parent by props.

CodePen

Share Fabric Canvas Object by forwardRef

If don't want share by Context, like global variables, we could also share with parent by forwardRef.

CodePen

Winlin
  • 1,136
  • 6
  • 25
3

I don't see any answers here using React's functional components, and I couldn't get the hook from @jantimon to work. Here's my approach. I have the canvas itself take its dimensions from the layout of the page, then have a "backgroundObject" at (0,0) that acts as the drawable area for the user.

import React, { useState, useRef, useEffect } from 'react';

const { fabric } = window;

// this component takes one prop, data, which is an object with data.sizePixels, an array that represents the width,height of the "backgroundObject"

const CanvasComponent = ({ data }) => {
  const canvasContainer = useRef();
  const canvasRef = useRef();
  const fabricRef = useRef();
  const [error, setError] = useState();

  const initCanvas = ({ canvas, size }) => {
    console.log('*** canvas init');
    console.log(canvas);

    let rect = new fabric.Rect({
      width: size[0],
      height: size[1],
      fill: 'white',
      left: 0,
      top: 0,
      selectable: false,
      excludeFromExport: true,
    });
    canvas.add(rect);

    rect = new fabric.Rect({
      width: 100,
      height: 100,
      fill: 'red',
      left: 100,
      top: 100,
    });
    canvas.add(rect);
  };

  // INIT
  useEffect(() => {
    if (!canvasRef.current) return;
    if (fabricRef.current) return;
    const canvas = new fabric.Canvas('canvas', {
      width: canvasContainer.current.clientWidth,
      height: canvasContainer.current.clientHeight,
      selection: false, // disables drag-to-select
      backgroundColor: 'lightGrey',
    });
    fabricRef.current = canvas;
    initCanvas({ canvas, size: data.sizePixels });
  });

  // INPUT CHECKING
  if (!data || !Array.isArray(data.sizePixels)) setError('Sorry, we could not open this item.');

  if (error) {
    return (
      <p>{error}</p>
    );
  }

  return (
    <>
      <div style={{
        display: 'flex',
        flexDirection: 'row',
        width: '100%',
        minHeight: '60vh',
      }}
      >
        <div style={{ flex: 3, backgroundColor: 'green' }}>
          Controls
        </div>
        <div ref={canvasContainer} style={{ flex: 7, backgroundColor: 'white' }}>
          <canvas id="canvas" ref={canvasRef} style={{ border: '1px solid red' }} />
        </div>
      </div>

      <div>
        <button
          type="button"
          onClick={() => {
            const json = fabricRef.current.toDatalessJSON();
            setDebugJSON(JSON.stringify(json, null, 2));
          }}
        >
          Make JSON
        </button>
      </div>
    </>
  );
};

export default CanvasComponent;

xaphod
  • 6,392
  • 2
  • 37
  • 45
1

There is also react-fabricjs package which allows you to use fabric objects as react components. Actually, Rishat's answer includes this package, but I don't understand how it is supposed to work as there is no 'fabric' object in the react-fabricjs (he probably meant 'fabric-webpack' package). An example for simple 'Hello world' component:

import React from 'react';
import {Canvas, Text} from 'react-fabricjs';

const HelloFabric = React.createClass({
  render: function() {
    return (
      <Canvas
        width="900"
        height="900">
          <Text
            text="Hello World!"
            left={300}
            top={300}
            fill="#000000"
            fontFamily="Arial"
          />
      </Canvas>
    );
  }
});

export default HelloFabric;

Even if you don't want to use this package, exploring it's code might help you to understand how to implement Fabric.js in React by yourself.

  • see also my answer to question about connecting fabric.js and redux where I use plain fabricjs and save it's content into redux store: http://stackoverflow.com/questions/37742465/how-to-make-friends-fabric-js-and-redux/40021187#40021187 – Radomír Laučík Oct 13 '16 at 12:40
  • Looks like the react-fabricjs project is dead (github link shows a 404). – ncrypticus Nov 16 '20 at 19:16
  • 1
    The comment is from 2016 and looks like the project was not much maintained since then, but still could be found here: https://github.com/neverlan/react-fabricjs Anyway, in 2020, I'd suggest to explore the answer above from user jantimon. – Radomír Laučík Nov 20 '20 at 16:42