I'm working on a Nuxt3 project with Fabric.js and Pinia. I want to manage the Fabric.js canvas instance globally using a Pinia store. The goal is to add text to the canvas using actions from the store.
However, when adding a fabric.Textbox directly in the component, it works perfectly (resizable and draggable). But when adding the same textbox through the store action, the text is only draggable but not resizable.
Here’s what I have so far:
Pinia Store (stores/canvasStore.js):
import { defineStore } from "pinia";
import * as fabric from "fabric";
export const useCanvasStore = defineStore("canvas", {
state: () => ({
canvas: null,
}),
actions: {
setCanvas(canvasInstance) {
this.canvas = canvasInstance;
},
addText(text = "New Text") {
if (!this.canvas) return;
const textbox = new fabric.Textbox(text, {
left: 150,
top: 200,
fontSize: 24,
fill: "black",
width: 200,
selectable: true,
hasControls: true,
});
this.canvas.add(textbox);
this.canvas.setActiveObject(textbox);
},
},
});
Canvas Initialization in Component (CanvasManager.vue):
<template>
<canvas ref="canvasRef" class="border"></canvas>
<AddAddTextButton />
</template>
<script setup>
import { ref, onMounted } from "vue";
import * as fabric from "fabric";
import { useCanvasStore } from "@/stores/canvasStore";
const canvasRef = ref(null);
const canvasStore = useCanvasStore();
onMounted(() => {
const canvas = new fabric.Canvas(canvasRef.value, {
width: 800,
height: 600,
backgroundColor: "#fff",
});
canvasStore.setCanvas(canvas);
});
</script>
Add Text Button (AddTextButton.vue)
<template>
<button @click="addTextToCanvas">Add Text</button>
</template>
<script setup>
import { useCanvasStore } from "@/stores/canvasStore";
const canvasStore = useCanvasStore();
const addTextToCanvas = () => {
canvasStore.addText("Resizable Text");
};
</script>
When the addText method is called via the store, the textbox is added but not resizable. If I move the same fabric.Textbox logic directly to the component (outside the store), resizing works perfectly.
Working example:
<template>
<div class="flex justify-center flex-col items-center">
<canvas ref="canvasRef"></canvas>
<button
@click="addTextBlock"
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
>
Add Text Block
</button>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as fabric from "fabric";
const canvasRef = ref(null);
let canvas = null;
onMounted(() => {
canvas = new fabric.Canvas(canvasRef.value, {
width: 500,
height: 500,
backgroundColor: "lightgray",
});
canvas.renderAll();
});
const addTextBlock = () => {
if (!canvas) return;
const textbox = new fabric.Textbox("New Text Bzlocks", {
left: 150,
top: 200,
fontSize: 30,
fill: "black",
width: 200,
});
canvas.add(textbox);
};
</script>
Why does adding a textbox through the Pinia store break resizing and editing? What am I doing wrong
Since the store is used only on client side (onMounted
), no implications are foreseen from the Nuxt SSR part.
The snippets aren't equivalent because let canvas
is non-reactive object, while Pinia store is reactive. It's always dangerous and also ineffective to make random objects reactive. Reactive proxies have overhead and the objects they wrap aren't guaranteed to work correctly.
To make canvas
non-reactive, it can either be made shallow ref, this prevents the property from being deeply reactive when a ref is unwrapped in state
:
state: () => ({
canvas: shallowRef(null),
}),
Alternatively, an object can be marked as non-reactive, but this needs to be done on every property assignment:
setCanvas(canvasInstance) {
this.canvas = markRaw(canvasInstance);
},
This is one of reasons it's a good practice to have a setter instead of mutating store.canvas
outside the store.