-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How can I use Fabric.js in something like React? #5951
Comments
takes a bit of time to organize an answer, but the things you mentioned are more or less good. |
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.
and there I can set things on the object like
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. Consider creating an hook like 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 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
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
Everything is put in a context provider function that provides the context to it's children in App.js:
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')` |
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 ( 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 |
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? |
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). 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 |
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:
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 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 The canvas is rendered after component render, and can be set to change when we update the color state (e.g. with Thanks for those points, I don't know the answer to
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. |
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? |
Cool, I'm sure it will. I will move the example to CodePen and send you the link! |
@saninmersion I wouldn't recommend storing mutative objects like a canvas in state variables. For the rest that looks like a great intake. |
Hi, I tried @Robbie-Cook solution, it's pretty simple but i can't understand why, the state inside the context is not updated. I can see that fabric events work well, it triggers the callback and set the new state but inside my consumers, the state doesn't change. It only changes when i move to another activeObject. So for example i can't move and object and display the left and top values changing in real time. |
To use fabricJS with NextJS like framework, where you have to target the canvas using ref and edit with fabricjs capabilities you can follow the below work around.
"use client";
import { fabric } from "fabric";
import React, { useCallback, useEffect, useRef } from "react";
export function useCanvas(
ref?: React.ForwardedRef<HTMLCanvasElement>,
init?: (canvas: fabric.Canvas) => any,
saveState = false,
deps: any[] = []
) {
const elementRef = useRef<HTMLCanvasElement>(null);
const fc = useRef<fabric.Canvas | null>(null);
const data = useRef<any>(null);
const setRef = useCallback(
(el: HTMLCanvasElement | null) => {
elementRef.current = el;
ref && (ref.current = elementRef.current);
// dispose canvas
fc.current?.dispose();
// set/clear ref
if (!el) {
fc.current = null;
return;
}
const canvas = new fabric.Canvas(el);
window.canvas = fc.current = canvas;
// invoke callback
init && init(canvas);
},
[saveState, ...deps]
);
useEffect(() => {
// disposer
return () => {
// we avoid unwanted disposing by doing so only if element ref is unavailable
if (!elementRef.current) {
fc.current?.dispose();
fc.current = null;
}
};
}, [saveState]);
return [fc, setRef] as [typeof fc, typeof setRef];
}
export const Canvas = React.forwardRef<
HTMLCanvasElement,
{
onLoad?: (canvas: fabric.Canvas) => any;
saveState?: boolean;
}
>(({ onLoad, saveState }, ref) => {
const [canvasRef, setCanvasElRef] = useCanvas(ref, onLoad, saveState);
return <canvas ref={setCanvasElRef} />;
});
"use client";
import { useCallback, useRef } from "react";
import { fabric } from "fabric";
import { Canvas } from "@/components";
export function CanvasMaker() {
const canvasRef: any = useRef(null);
const fabricRef: any = useRef(null);
const onCanvasLoad = useCallback(async (initFabricCanvas: fabric.Canvas) => {
const text = new fabric.Textbox("fabric.js sandbox", {
originX: "center",
top: 0,
});
initFabricCanvas.add(text);
initFabricCanvas.centerObjectH(text);
fabricRef.current = initFabricCanvas;
}, []);
const onAddClick = () => {
const rect = new fabric.Rect({
height: 280,
width: 200,
fill: "yellow",
selectable: true,
hasControls: true,
});
fabricRef.current.add(rect);
};
return (
<div>
<Canvas onLoad={onCanvasLoad} ref={canvasRef} saveState />
<button onClick={onAddClick}>Add React</button>
</div>
);
} |
Just chiming in with my working global context setup: const id = nanoid()
const instance = new fabric.Canvas(id, {
width: window.innerWidth,
height: window.innerHeight,
preserveObjectStacking: true,
})
function CanvasProvider({ children }: Required<PropsWithChildren>) {
const [initialized, setInitialized] = useState(false)
const [canvas] = useState<fabric.Canvas>(() => instance)
useEffect(() => {
const canvasNode = document.getElementById(id)
if (canvasNode) {
canvasNode.replaceWith(canvas.wrapperEl)
}
canvas.requestRenderAll()
setInitialized(true)
// no need for cleanup since fabric init should only happen once when the script is loaded
// in fact if you *do* return a cleanup it will fail since its set up to not recreate the instance on next render
}, [canvas])
const findObjectById = useCallback(
(id: string) => canvas.getObjects().find((obj) => obj.id === id),
[canvas]
)
const value = useMemo(
() => ({
id,
initialized,
canvas,
findObjectById,
}),
[canvas, findObjectById, initialized]
)
return (
<CanvasContext.Provider value={value}>{children}</CanvasContext.Provider>
)
} Actual canvas element somewhere within the tree: function Canvas(props: CanvasProps) {
const { id } = useCanvas()
return (
<Wrapper>
<canvas id={id} {...props} />
</Wrapper>
)
} For ex. accessing the active object somewhere, I configure a handler first: const { canvas } = useCanvas()
useEffect(() => {
const handleSelectionCreated = (
event: Partial<TEvent<TPointerEvent>> & {
selected: FabricObject[]
}
) => {
// I currently use RTK, but any state manager should still work
dispatch(setCurrentlySelected(event.selected[0].id)) // I added the id prop manually
}
canvas.on("selection:created", handleSelectionCreated)
return () => canvas.off("selection:created", handleSelectionCreated)
}) Finally: const { findObjectById } = useCanvas()
const currentlySelectedId = useSelector(selectCurrentlySelected)
const activeObject = findObjectById(currentlySelectedId)
// ... do what you need to do with the object Setting internal canvas state is pretty straightforward too, just don't forget to call Like I've mentioned, everything works so far but the only thing to note (and something I wanted to report) is that I had to add a tiny bit of extra code: const canvasNode = document.getElementById(id)
if (canvasNode) {
canvasNode.replaceWith(canvas.wrapperEl)
} Seems that fabric does init correctly with this setup but fail to update the DOM? Anyway, I'm not a React expert and am still wondering if this is a good idea at all too so I'd appreciate some thoughts on it. |
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?
The text was updated successfully, but these errors were encountered: