Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use Fabric.js with React?

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?

like image 512
Kitanotori Avatar asked Jun 01 '16 09:06

Kitanotori


People also ask

Can we use fabric JS in React?

react-faux-dom can be used with packages like d3 and fabric.

What is the use of fabric JS?

fabric. js is a powerful and quite simple javascript library for HTML5 canvas. It provide a interactive platform to work with the HTML5 canvas. Using fabric you can create object/shapes on canvas from simple geometrical shapes to complex shapes.

What is React Konva?

react-konva is a JavaScript library for drawing complex canvas graphics using React. It provides declarative and reactive bindings to the Konva Framework. Github Repo. It is an attempt to make React work with the HTML5 canvas library.

What can you do with JavaScript React?

React allows developers to create large web applications that can change data, without reloading the page. The main purpose of React is to be fast, scalable, and simple. It works only on user interfaces in the application. This corresponds to the view in the MVC template.


6 Answers

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.

like image 82
StefanHayden Avatar answered Oct 19 '22 03:10

StefanHayden


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!

like image 22
rishat Avatar answered Oct 19 '22 03:10

rishat


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>
}
like image 39
jantimon Avatar answered Oct 19 '22 02:10

jantimon


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;

like image 38
xaphod Avatar answered Oct 19 '22 03:10

xaphod


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:

  • We should use React.useCallback(fn, []) to prevent double calling of ref with null. If element is null, means componentWillUnmount, so we must dispose fabric here.
  • Please use // eslint-disable-next-line react-hooks/exhaustive-deps to surpress warnings, for the useCallback with empty array deps.
  • The fabricCanvas(fabric.Canvas) is like instance variable of useFabirc, it's also a Uncontrolled Components, which is fabric.Canvas object, can be used in all your app.

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

like image 37
Winlin Avatar answered Oct 19 '22 04:10

Winlin


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.

like image 29
Radomír Laučík Avatar answered Oct 19 '22 04:10

Radomír Laučík