Fabric.js: How can I use Fabric.js in something like React?

Created on 5 Nov 2019  路  25Comments  路  Source: fabricjs/fabric.js

I have already asked a question to SO but got no replies except a comment that points to an old, similar question with an answer that uses outdated technology.

https://stackoverflow.com/questions/58689343/how-can-i-use-something-like-fabric-js-with-react

How would I do this in ES6, latest React and without relying on some third-party modules that are abandoned for years?

stale

Most helpful comment

Here's my solution using hooks/context:

import React, { createContext, useState } from 'react';

// Here are the things that can live in the fabric context.
type FabContext = [
  // The canvas
  fabric.Canvas | null,
  // The setter for the canvas
  (c: fabric.Canvas) => void
];

// This is the context that components in need of canvas-access will use:
export const FabricContext = createContext<FabContext>([null, () => {}]);

/**
 * This context provider will be rendered as a wrapper component and will give the
 * canvas context to all of its children.
 */
export const FabricContextProvider = (props: {children: JSX.Element}): JSX.Element => {
  const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);

  const initCanvas = (c: fabric.Canvas): void => {
    setCanvas(c);
  };

  return (
    <FabricContext.Provider value={[canvas, initCanvas]}>
      {props.children}
    </FabricContext.Provider>
  );
};

Then, when the usage is needed:

const [canvas, initCanvas] = useContext(FabricContext);

// either use the canvas or set the canvas after initializing it using `new fabric.Canvas('my-id')`

All 25 comments

takes a bit of time to organize an answer, but the things you mentioned are more or less good.
You need to make the fabricCanvas accessible to all components with something like context. You should initialize it in the component and not at app level. All the rest is plain fabric stuff and dispatching actions.
I'll try to answer there.

Thanks for your response, it helps me a lot to see if I am looking into the right direction.

I already started to fiddle around with React.Context and try to implement it this way. I thought about having the fabric.Canvas object somehow in the Context, so a thing like "DrawButton" can toggle fabricObject.isDrawingMode = true/false. This should give me the basic understanding how to construct the data store and access it from children. Starting from there I probably can figure out how to do stuff like getting the active object (to determine which toolbox I should open), a list of all objects on the canvas (to implement something like a "layer" list) and eventually rendering the whole canvas to something like SVG or PNG using fabrics .toXYZ method.

As a first prototype I had thrown everything into App.js and just trigger App.js methods that access this.the_canvas directly. But it went messy pretty fast so now I am trying to find a way to make it more modular.

I'm in no way a React expert, but what we did for vuejs was a wrapper component that exposes all the needed methods of Fabric and handles the communication in-between. I can give you an example, but just in vuejs if you are firm with this.

I am not firm with Vue, even less than with React, but maybe it could give some ideas. But basically that's what I thought it would work with React.Context: You write a context provider that holds all the stuff in it's state and provides it as a context to it's toolbar children and the canvas component.
But I am not sure if that's the best practice to do that...
Also I noticed a strange issue when playing with this: I create the fabric canvas in a component FabricCanvas:

componentDidMount() {
    this.the_canvas = new fabric.Canvas('main-canvas', {
        preserveObjectStacking: true,
        height: 375,
        width: 375
    });
}

and there I can set things on the object like

componentDidUpdate() {
    this.the_canvas.isDrawingMode = this.context.drawMode;
    console.log(" FabricCanvas this.the_canvas.isDrawingMode: " + this.the_canvas.isDrawingMode);
}

Which I can trigger as a function in the context (let's say a button sets context.drawMode = true which triggers component update and sets the isDrawingMode). But when I try to move that whole thing, including creation of the fabric.Canvas object into the context provider one level above FabricCanvas, the canvas gets created, the html-canvas has the default blue fabric selection box and I can console.log it and see it's a fabric canvas object but when I then set isDrawingMode = true on that object, it shows isDrawingMode: true in console.log however the actual canvas never switches to draw mode, it stays as it is (I get the selection box, no freehand drawing). I don't know why this happens must be something with how Javascript handles the DOM tree I guess?

Which makes it awkward, because I now have to decide in contextDidUpdate what change happened and set the this.the_canvas object attributes accordingly respectively need to call a local function that does things like new fabric.Text to the canvas.

So i m probably late to the party.
I do not use context os much part for sharing some global object that is good to call fabric from every compontent ( the alternative is to import a global variable in a single file everywhere, making your canvas a singleton and the app very difficult to refactor later if you need more than one canvas for any reason).

Consider creating an hook like withFabricCanvasAccess that uses useContext and lets you have the fabricCanvas availabe to trigger some changes.

Different case is for UI updates. You will have a store and you have to put some fabric information there to update your UI.

Use fabric events like selection:created and selection:updated to send in the store for example, the information of the object that has been selected, type, color, stroke or all of it. Use this store information to draw your UI with the classic react-redux binding of your choice ( or for the store you chose ).

Be careful with re-renders. Every state update will start to redraw all of your app if you do not put gates somewhere ( a react Memo, a connect with plain props, a PureComponent )

I'm not sure in those 15 days what you did so far and how is going.

The context thing did not work in object oriented. When I had a component set a parameter of fabrics canvas (like canvas.isDrawingMode), it did not get propagated to the canvas created, the mode was set in the fabric object but didnt affect the real canvas. After moving the whole thing to react hooks and functional approach, it worked. I don't know why, it seems like it was working on a copy of the canvas object.

However eventually I got stuck on accessing fabric's .on event listeners. The function canvas.on() is always undefined when I try to access it from the context.

probably the object was somehow serializing? can it be?

Can you post your work with hooks? did you use useRef or something different? this would be a valuable guide for other developers.

I didn't have yet the need for useRef hook.

I mostly use useState and useEffect. I put everything related to the canvas into a state, keep there also the objectList and all flags and settings.

The canvas is it's own component which calls a function from the context provider upon mount which initializes the fabric.Canvas

const FabricCanvas = (props) => {
    const context = useContext(FabricContext);

    useEffect(() => {
        context.initCanvas('c');
    }, []); //runs only once on mount

const ContextProvider = (props) => {
    const [canvas, setCanvas] = useState(false);

    const initCanvas = (c) => {
        setCanvas(new fabric.Canvas(c));
    }

To work with fabric events, I have an own useEffect hook that gets called everytime something changes on the canvas, thus I can set functions to handle different events

const [objectList, setObjectList] = useState(['none']);

useEffect(() => {
    if(!canvas) {
        return;
    }
    function handleEvent(e) {
        if(!e) {
            return;
        }
        setObjectList(canvas.getObjects());
    }
    handleEvent();
    canvas.on("object:added", handleEvent);
    canvas.on("object:removed", handleEvent);
    return () => {
        canvas.off("object:added");
    }
}, [canvas]);

Everything is put in a context provider function that provides the context to it's children in App.js:

const App = (props) => {
    return (
        <ContextProvider>
                    <DrawButton />
                    <TextButton />
                    <FabricCanvas />
                    <ObjectList />
        </ContextProvider>
    );
}

The use of hooks allows me to put the components where ever I want in the whole context (and maybe even in the future cascade multiple contexts) thus giving me the freedom to logically arrange the app's components as I need them which was impossible using class based way.

Keep in mind that this is a work in progress any many things are not working yet and solutions I found so far might not be the best way to do it and could thus change in the future.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Here's my solution using hooks/context:

import React, { createContext, useState } from 'react';

// Here are the things that can live in the fabric context.
type FabContext = [
  // The canvas
  fabric.Canvas | null,
  // The setter for the canvas
  (c: fabric.Canvas) => void
];

// This is the context that components in need of canvas-access will use:
export const FabricContext = createContext<FabContext>([null, () => {}]);

/**
 * This context provider will be rendered as a wrapper component and will give the
 * canvas context to all of its children.
 */
export const FabricContextProvider = (props: {children: JSX.Element}): JSX.Element => {
  const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);

  const initCanvas = (c: fabric.Canvas): void => {
    setCanvas(c);
  };

  return (
    <FabricContext.Provider value={[canvas, initCanvas]}>
      {props.children}
    </FabricContext.Provider>
  );
};

Then, when the usage is needed:

const [canvas, initCanvas] = useContext(FabricContext);

// either use the canvas or set the canvas after initializing it using `new fabric.Canvas('my-id')`

export const FabricContext = createContext<FabContext>([null, () => {}]);

What's this for?

export const FabricContext = createContext<FabContext>([null, () => {}]);

What's this for?

That creates the context that components can import and use to get/set the canvas. In my example there's also some typescript (<FabContext>) but if you're using plain old JS you can leave that out.

import React, { useContext } from 'react';
import { FabricContext } from './somewhere';

const MyComponent = () => {
    const [canvas, initCanvas] = useContext(FabricContext);
    useEffect(() => {
        const localCanvas = new fabric.Canvas('c');
        initCanvas(localCanvas);
    }, []);

  return (
    <canvas
      id='c'
      width={TEMPLATE_WIDTH}
      height={TEMPLATE_HEIGHT}
    />
  );
}

if you want to do it more reacty, instead of the ID you can pass a element ref. You can make a sort of custom hook useFabricCanvas(canvasRef). I still trying to get an hold on hooks before feeling like giving suggestions.

Yeah that's basically how I did it in my example. Just that I create the fabric canvas in an own component and give the created canvas back into the context by calling context.initCanvas(c) from the component which sets the whole canvas reference into state variable accessible by the context. I just didn't use a type, don't know what's that for.

@maniac0s How did you go about doing "stuff like getting the active object (to determine which toolbox I should open)" and getting default values for tools based on selected layers?

@maniac0s How did you go about doing "stuff like getting the active object (to determine which toolbox I should open)" and getting default values for tools based on selected layers?

Writing this from my head since I am currently on a different project for a few weeks: Again mostly hardcoded stuff (can be made more dynamically with react tho).
I save like everything on the context and thus there's also an activeObject variable. The context provider has a state variable "activeObject" and can update this on events which then gets promoted to the context. That always get's set when an object is clicked which I get with an onClick event on the canvas and calling the fabric function active = canvas.getActiveObject() which returns the currently activated Object, so I can use setActiveObjiect(active) to update the state variable.

A toolbox component then decides, according to what's in "activeObject", the properties of the actual toolbox to be displayed. I have made local functions for that and here you can endlessly cascade the components depending on what you need. For instance if activeObject === "textbox" I display fontlist, fontsizes, colorpicker, alignment ect inputfilelds and for that I have in the toolbox component a function that implements a <TextTools> component which has locally hardcoded a list of fonts, sizes ect and includes a <ColorPicker type="fill"> for the text fill color.
Toolbox in it's return only defines JSX when activeObject !== "none", so the toolbox component displays only when there's an object with a type selected.

Could we get some docs for React added to the website? I am keen to help for this.

Also keen to help add demos for React too.

If you have some idea to standardize a react approach, please do!

i use now creatRect to get the canvas reference and then i mount the fabricJS canvas on top of the element with a useEffect, the main problem to solve are:

  • make the canvas accessible in all the app ( context? )
  • make the same canvas accessible in utility functions that are possibly tirggered by events and are outside react domain
  • state! how do i tell another component to re-render because the active object change from red to blue?

any small tutorial is welcome!

The latest demos use codepen prefill embed, thos should allow for JS transpilation and react too.

Hey @asturur , thanks!

I made a small demo https://codesandbox.io/s/react-fabric-example-87hh4 ,

However it doesn't cover make the canvas accessible in all the app, but it is accessible in utility classes / functions when canvas is passed as a parameter. We just need to check whether it is null.

In terms of how we can tell another component to re-render because the active object change from red to blue, we can hook into the react component state for this, e.g. add a listener to the canvas that triggers the setState of another component.

The canvas is rendered after component render, and can be set to change when we update the color state (e.g. with useEffect(() => {...}, [props.color]), or never change (e.g. with useEffect(() => {...}, []),

Thanks for those points, I don't know the answer to

make the canvas accessible in all the app ( context? )`

yet

I did start pushing my code that I used to create and test a feature for my project that was created from the help of this thread a while back.
Here is the repo

The repo seems good to me. If it works good we should totally link it somewhere.

Great @asturur , I am super keen.

It might be good if other colloborators can make changes to the codesandbox, so that it's not just me. Maybe I could add you as a collaborator, or there could be a fabric.js codesandbox account?

Do codepen allow for the same?
if it has to be editable live as an example on the website we need to use codepen, otherwise you can clone/move the repo in the fabricjs org and get write access to it together with whoever wants to collaborate on it, or it can be there and someone will just open prs on it if they want to improve a part.

Cool, I'm sure it will. I will move the example to CodePen and send you the link!
Cheers!

@saninmersion I wouldn't recommend storing mutative objects like a canvas in state variables. For the rest that looks like a great intake.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

amancqlsys picture amancqlsys  路  5Comments

eddieyangtx picture eddieyangtx  路  5Comments

urcoder picture urcoder  路  5Comments

mlev picture mlev  路  3Comments

bhaskardas9475 picture bhaskardas9475  路  4Comments