Fabric.js: App State vs Canvas Behavior
Fabric.js editors keep application state (your data model) and canvas behavior (rendering and interaction) separate, but the separation is not absolute—the library actively synchronizes the two. You manage state through JavaScript objects and arrays, while the canvas is a visual projection of that state, with events and methods bridging the gap. This article explains how the architecture works, where the boundaries are, and how to use it effectively in your own projects.
Background: Why the Separation Exists
Fabric.js was designed to make HTML5 canvas interactive without forcing developers to write low-level drawing code. Early canvas libraries required you to redraw everything manually on every change. Fabric introduced an object model: every shape, image, or text on the canvas is a JavaScript object with properties like position, scale, rotation, and fill. The library then renders those objects to the canvas automatically.
This design inherently separates two concerns:
- Application state — the data you care about: which objects exist, their properties, their order, and any custom data you attach.
- Canvas behavior — how the canvas responds to mouse events, how objects are drawn, hit-testing, selection, and transformation controls.
Fabric’s API is built around keeping these two in sync. When you change a property on an object, Fabric marks the canvas as dirty and re-renders on the next frame. When a user drags an object, Fabric updates the object’s left and top properties and fires events so your application code can react.
How Fabric.js Manages State Internally
At the core is the fabric.Canvas instance, which holds a reference to the canvas element and maintains an internal collection of objects. You add objects via canvas.add(obj) or canvas.insertAt(obj, index). The canvas also keeps track of the active selection, the viewport transform (when zooming or panning), and the undo/redo stack if you enable it.
Each object (e.g., fabric.Rect, fabric.Circle, fabric.Text) stores its own state as plain properties. For example:
const rect = new fabric.Rect({
left: 100,
top: 50,
width: 80,
height: 60,
fill: 'red',
angle: 15
});
canvas.add(rect);
Here, left, top, width, etc., are part of the object’s state. The canvas behavior—how the rectangle responds to drag, resize, or rotation—is handled by Fabric’s event system and control logic. You rarely touch the rendering code directly; you modify the object’s properties and call canvas.requestRenderAll() (or rely on automatic re-render after certain operations).
Events: The Bridge Between State and Behavior
Fabric uses a rich event system to notify your application when the canvas behavior changes state. Key events include:
object:modified— fired after a user finishes moving, scaling, or rotating an object.object:moving,object:scaling,object:rotating— fired continuously during the operation.selection:created,selection:updated,selection:cleared— when the user selects or deselects objects.object:added,object:removed— when objects enter or leave the canvas.
These events give you a clean hook to update your own state model. For example, if you maintain a separate array of objects for saving to a server, you might listen to object:modified and update the corresponding entry in your array. This pattern keeps your application state as the source of truth, while the canvas behavior is just the interactive view.
Serialization and Deserialization
Fabric provides built-in methods to convert the canvas state to JSON and back: canvas.toJSON() and fabric.Canvas.loadFromJSON(). This is the cleanest way to persist state or send it to a backend. The JSON includes all object properties, but not the canvas’s behavioral settings like selection styles or event listeners.
When you load from JSON, Fabric recreates the objects and re-renders the canvas. You can also pass a callback to loadFromJSON to run code after everything is set up. This separation is deliberate: the JSON is a pure data snapshot, and the canvas behavior is re-applied by the library when loading.
Common Misconception: State Is Not the DOM
One frequent misunderstanding is that the canvas element’s internal bitmap is the state. It is not. The bitmap is a rendering artifact. If you resize the canvas or change the device pixel ratio, Fabric re-renders from the object model. The objects are the state; the pixels are the output.
Another misconception is that you need to manually redraw after every change. In most cases, Fabric handles re-rendering automatically when you use its methods (e.g., set() on an object triggers a re-render). However, if you modify an object’s properties directly without using set(), you must call canvas.requestRenderAll() to update the display. This is a common source of bugs for developers new to Fabric.
Practical Patterns for Keeping State Separate
While Fabric’s objects can serve as your state, many applications benefit from a separate data model. Here are two common approaches:
- Canvas as the single source of truth — You use Fabric objects directly and serialize them with
toJSON()when saving. This is simplest for small projects. - External state model — You keep your own array of plain objects (e.g., with IDs, custom fields, or business logic) and synchronize with Fabric objects via events. This is better for complex apps where the same data is used outside the editor.
For the external model, a typical pattern is to store a custom property on each Fabric object, like obj.id = 'myId', and then use that to map between your state and the canvas. When you load data, you create Fabric objects from your model; when the user edits, you update your model in the object:modified handler.
Performance and Rendering Behavior
Fabric separates the rendering loop from your state changes. By default, it uses a requestAnimationFrame-based system to batch re-renders. This means if you change ten properties in a single frame, Fabric only redraws once. This is a deliberate design choice to keep behavior smooth.
However, the separation can cause confusion about when the canvas actually updates. If you need to read a pixel value or measure something immediately after a change, you may need to call canvas.renderAll() (synchronous) instead of requestRenderAll() (asynchronous). Understanding this distinction is key to debugging visual glitches.
Frequently Asked Questions
Do I need to keep my own state if I use Fabric?
No, but it depends on your app. Fabric objects themselves are stateful. If you only need to save/load the canvas, use toJSON() and loadFromJSON(). If you need to share data with other parts of your application (e.g., a list of shapes in a sidebar), maintaining a separate model is cleaner and avoids coupling your UI to Fabric’s API.
Why does the canvas not update when I change a property?
Most likely you changed the property directly (e.g., rect.left = 200) instead of using rect.set('left', 200). Direct assignment does not trigger a re-render. Use set() or call canvas.requestRenderAll() after direct changes.
Can I use Fabric without a canvas element?
Yes, you can create objects and serialize them to JSON without a canvas, but you cannot interact with them visually. The fabric.Canvas is required for rendering and user interaction. For headless operations, you can use fabric.StaticCanvas which renders but does not handle events.
Is Fabric.js state saved in the canvas element itself?
No. The canvas element only contains pixel data. Fabric stores the object model in JavaScript memory. If you reload the page, the state is gone unless you serialize it. That is why toJSON() is essential for persistence.
How do I undo/redo with Fabric?
Fabric does not have built-in undo/redo. You must implement it yourself by saving snapshots of canvas.toJSON() before each operation, or by tracking object changes via events. Many developers use a simple stack of JSON snapshots for this purpose.
Does Fabric mix state and behavior in the same object?
Yes, by design. Each object has both data properties (state) and methods (behavior). The separation is conceptual: you control the data, and Fabric controls the rendering and interaction. The library does not force you to keep them in separate places, but it provides the tools to do so if you wish.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.