Integración de GrapesJS en una aplicación Next.js con envoltorio @grapesjs/react

Integra GrapesJS de forma fluida con React, TypeScript y Next.js usando el paquete oficial @grapesjs/react para el desarrollo de aplicaciones modernas.

DevFuture Development
DevFuture Development
24 de octubre de 2025hace 8 meses
21 lectura mínimaVistas de 6,530
GrapesJS is a battle-tested drag-and-drop builder framework that powers page editors, email designers, and headless CMS UIs. If your stack is Next.js (App Router) with React 19, the official@grapesjs/reactwrapper makes the integration declarative and SSR-safe. This guide walks through a clean setup, both UI modes, plugins, server-side persistence, and — crucially — how to put real React components inside the editor canvas, which used to be a hard problem and now isn't.

What changed in 2026 (and why this guide was rewritten)

If you're coming from an older tutorial, three things are worth re-checking:

  • Next.js 16 is the current LTS (16.2.x at the time of writing). Next.js 14 reached EOL in October 2025; Next.js 15 LTS ends in October 2026. The App Router is no longer "new" — it's the default, and 'use client' is the well-established boundary you need.
  • @grapesjs/reactv2 is the current major (requires React 18/19, works with Next.js 15+). It works fine on Next.js 16 as well.
  • GrapesJS Studio SDKnow ships a React Renderer plugin that can render real React components inside the canvas — both client-side and SSR. For open-source GrapesJS, the community Reactor Preset offers a lighter path to the same idea.

We'll cover both.

Two integration paths

Before any code, pick the right tool for the job:

Path A — open-source GrapesJS +@grapesjs/react.This is the focus of this guide. Free, MIT-licensed, gives you the full GrapesJS API, and lets you build a custom UI around the canvas with React. The canvas itself renders plain HTML/CSS. If you need React components in the canvas, you bolt them on with a preset like Reactor.

Path B — GrapesJS Studio SDK.A commercial product (free tier available) that ships a richer UI, a React Renderer for components in the canvas with first-class SSR support, asset/storage modules, and ongoing maintenance. Pick this if you're building a content product where the editor is core to the business and you want one vendor to call.

The rest of this guide is Path A unless noted otherwise.

What@grapesjs/reactactually does

The package — also listed on the GrapesJS marketplace as"React UI for GrapesJS"(the official one from the GrapesJS team) — is a small wrapper around the core GrapesJS modules. It does three useful things:

  1. Lifecycle management.It initializes the editor on mount and callseditor.destroy()on unmount. No manualuseEffectcleanup, no double-init under React Strict Mode.
  2. React context for the editor.Once mounted, the editor instance is available anywhere in the subtree throughuseEditor()or<WithEditor>. You write toolbars, layer panels, and inspectors as ordinary React components.
  3. Optional Custom UI mode.Drop a<Canvas />component anywhere in your layout, and the wrapper disables GrapesJS's built-in panels — leaving you free to build the entire chrome in React.

It doesnotrender React components inside the canvas itself. That's by design: the canvas is a plain HTML/CSS iframe, which keeps GrapesJS deterministic. If you need React in the canvas, see "Rendering real React components in the canvas" below.

Setting it up in a Next.js App Router project

We'll assume you have a Next.js 16 project with TypeScript. If you're still on 15, the code below works without changes.

1. Install dependencies

npm install grapesjs @grapesjs/react # or: pnpm add grapesjs @grapesjs/react 

grapesjsis a peer dependency, so it must be installed alongside the wrapper. Use GrapesJS>= 0.22.xto match@grapesjs/reactv2.

2. Create the editor as a client component

Two patterns work. Pick one.

Pattern A —'use client'directive on a dedicated component.Recommended when the editoristhe page.

// app/editor/Editor.tsx 'use client'; import grapesjs, { type Editor } from 'grapesjs'; import GjsEditor from '@grapesjs/react'; import 'grapesjs/dist/css/grapes.min.css'; export default function PageBuilder() { const onEditor = (editor: Editor) => { // Save the instance, register events, load initial data, etc. console.log('Editor loaded', editor); }; return ( <div style={{ height: '100dvh' }}> <GjsEditor grapesjs={grapesjs} grapesjsCss="https://unpkg.com/grapesjs/dist/css/grapes.min.css" onEditor={onEditor} options={{ height: '100%', storageManager: false, }} /> </div> ); } 
// app/editor/page.tsx import PageBuilder from './Editor'; export default function Page() { return <PageBuilder />; } 

Pattern B —next/dynamicwithssr: false.Use this when the editor is one piece of a larger page and you want explicit code-splitting.

// app/editor/page.tsx 'use client'; import dynamic from 'next/dynamic'; const PageBuilder = dynamic(() => import('./Editor'), { ssr: false, loading: () => <div>Loading editor…</div>, }); export default function Page() { return <PageBuilder />; } 

Either approach guarantees the editor only runs in the browser. GrapesJS toucheswindowanddocumentat import time in some code paths, so it must never enter a server bundle.

3. A note on the CSS

The wrapper accepts agrapesjsCssprop (a URL) that it injects into the document. You can alsoimport 'grapesjs/dist/css/grapes.min.css'directly from your client component — Next.js will hoist it into the page's CSS bundle. Both work; one is enough. Using the URL approach can shave a few ms off the initial parse but adds a network round-trip.

Default UI vs Custom UI: pick one consciously

@grapesjs/reactsupports two modes, and they're not interchangeable mid-flight.

Default UI (zero children inside<GjsEditor>)

GrapesJS renders its full default chrome: top bar, blocks panel, style manager, layer manager. You configure them through the standard GrapesJSoptions(panels, blockManager, styleManager, etc.). This is the fastest path to a working editor and is what we showed above.

Custom UI (you provide children, including<Canvas />)

The moment you put JSX children inside<GjsEditor>and include<Canvas />, the wrapper disables GrapesJS's built-in panels and hands you full control of the layout.

'use client'; import grapesjs, { type Editor } from 'grapesjs'; import GjsEditor, { Canvas, useEditor } from '@grapesjs/react'; import 'grapesjs/dist/css/grapes.min.css'; function Toolbar() { const editor = useEditor(); const save = async () => { const html = editor.getHtml(); const css = editor.getCss(); const data = editor.getProjectData(); // structured JSON await fetch('/api/pages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html, css, data }), }); }; return ( <div className="flex items-center gap-2 border-b p-2"> <button onClick={save} className="rounded bg-blue-600 px-3 py-1 text-white"> Save page </button> <button onClick={() => editor.UndoManager.undo()}>Undo</button> <button onClick={() => editor.UndoManager.redo()}>Redo</button> </div> ); } export default function CustomEditor() { return ( <GjsEditor grapesjs={grapesjs} grapesjsCss="https://unpkg.com/grapesjs/dist/css/grapes.min.css" options={{ height: '100%', storageManager: false }} > <div className="flex h-dvh flex-col"> <Toolbar /> <div className="flex-1"> <Canvas /> </div> </div> </GjsEditor> ); } 

useEditor()only returns the editorinsidea<GjsEditor>subtree, after init. If you call it before the editor is ready (e.g. in a parent component), it will throw. The wrapper also exports<WithEditor>for render-prop style and per-module hooks (useBlocks,useLayers, etc.) for building specific panels.

Plugins and custom blocks

GrapesJS gets most of its functionality from plugins. Two ways to register them:

Viaoptions.plugins(best for third-party plugins)

npm install grapesjs-preset-webpage 
import presetWebpage from 'grapesjs-preset-webpage'; <GjsEditor grapesjs={grapesjs} grapesjsCss="https://unpkg.com/grapesjs/dist/css/grapes.min.css" options={{ height: '100%', storageManager: false, plugins: [presetWebpage], pluginsOpts: { 'grapesjs-preset-webpage': { // see the plugin docs for the full options surface blocks: ['link-block', 'quote', 'text-basic'], }, }, canvas: { // Inject the plugin's CSS into the canvas iframe styles: [ 'https://unpkg.com/grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css', ], }, }} /> 

Two things worth flagging:

  • pluginsOptsis keyed by the plugin's registered name string (here'grapesjs-preset-webpage'), not by a JS reference. Older guides sometimes write[plugin.name as string]— that only works if the function's.namematches the registered name, which isn't guaranteed. Hardcoding the string is safer.
  • canvas.stylesinjects stylesheets into the iframe —notinto your app. This is how plugin CSS, framework CSS (Bootstrap, Tailwind), and your design tokens reach the canvas. We'll come back to this.

ViaonEditor(best for your own blocks and component types)

const onEditor = (editor: Editor) => { editor.Blocks.add('alert-box', { label: 'Alert Box', category: 'Basic', content: { type: 'alert-box' }, // reference the component type media: '<svg viewBox="0 0 24 24" width="20" height="20">…</svg>', }); editor.Components.addType('alert-box', { model: { defaults: { tagName: 'div', attributes: { class: 'alert-box' }, components: [{ type: 'text', content: 'Alert message…' }], stylable: ['background-color', 'padding', 'border-radius'], }, }, }); }; 

Defining the component type — not just the block — is what lets GrapesJS round-trip the element correctly when the user reloads a saved page. WithoutComponents.addType, the alert box will load back as a generic<div>and lose any custom behavior.

Rendering real React components inside the canvas

This is the part that trips people up:@grapesjs/reactis for the UI around the canvas, not inside it. The canvas itself is an isolated iframe that renders plain HTML and CSS. If you have a React<Hero>component and you want it to appear, editable, on the canvas, you have two practical options.

Option 1 — Reactor Preset (open-source, free)

The Reactor Preset for GrapesJS is a community preset that wires React components into GrapesJS as first-class components. It's based on the well-knowngrapesjs-react-custom-uiStackBlitz demo, MIT-licensed, and a good fit if you're staying on open-source GrapesJS (0.21.xfamily at the time of writing).

It's the lightest way to test the idea before committing to the Studio SDK. Expect to do some glue work for SSR — Reactor was built for client-side editing demos, not Next.js production, so you'll want to confine it to client components and likely render theoutputof the editor (the saved HTML) statically.

Option 2 — Studio SDK React Renderer (commercial)

The official Studio SDKReact Renderer plugin, released in 2025, was built specifically for this use case. You register a map of React components with prop schemas, and the editor lets users drop them into the canvas, edit props in a side panel, and save the result as structured JSON. The renderer supports both CSR and SSR, so the same component tree renders in the editor and in your production Next.js pages.

import StudioEditor from '@grapesjs/studio-sdk/react'; import rendererReact from '@grapesjs/studio-sdk-plugins/dist/rendererReact'; import { Hero } from '@/components/Hero'; const reactRenderer = { components: { Hero: { component: Hero, props: () => [ { type: 'text', name: 'title', label: 'Title' }, { type: 'text', name: 'subtitle', label: 'Subtitle', value: 'Default' }, ], }, }, }; // Inside a client component: <StudioEditor options={{ licenseKey: process.env.NEXT_PUBLIC_GRAPESJS_LICENSE!, plugins: [rendererReact.init(reactRenderer)], }} />; 

If your editor is a core product feature and you want a single team to maintain the integration, this is the path. If it's a side feature for an internal CMS, Reactor +@grapesjs/reactis usually enough.

Persisting editor content with Server Actions

A real editor needs to save somewhere. The cleanest pattern in App Router is a Server Action — no API route, no fetch boilerplate.

// app/editor/actions.ts 'use server'; import { db } from '@/lib/db'; export async function savePage(payload: { id: string; html: string; css: string; data: object; // editor.getProjectData() }) { await db.page.upsert({ where: { id: payload.id }, create: payload, update: { html: payload.html, css: payload.css, data: payload.data, }, }); } 
// In your Toolbar component (client): import { savePage } from './actions'; const save = async () => { await savePage({ id: pageId, html: editor.getHtml(), css: editor.getCss(), data: editor.getProjectData(), }); }; 

StoregetProjectData(), not just HTML.It's the structured JSON representation of the project (components, styles, assets, pages), and it's what you reload into the editor on the next session viaeditor.loadProjectData(saved). The HTML/CSS pair is what you serve to end users.

On the read path, fetch the saved page in a Server Component and pass it as a prop to your client editor:

// app/editor/[id]/page.tsx import { db } from '@/lib/db'; import PageBuilder from '../Editor'; export default async function Page({ params }: { params: { id: string } }) { const page = await db.page.findUnique({ where: { id: params.id } }); return <PageBuilder initialData={page?.data ?? null} />; } 

Then inPageBuilder:

const onEditor = (editor: Editor) => { if (initialData) editor.loadProjectData(initialData); }; 

Tailwind (and other CSS frameworks) in the canvas

This is the single most-asked question for the Next.js + GrapesJS combo, so it deserves its own section.

The canvas is a sandboxed iframe. Tailwind utility classes in your blocks won't do anything unless Tailwind's generated stylesheet is loadedinside the iframe. Two patterns work:

Pattern 1 — Build a static stylesheet and inject it

Generate a Tailwind CSS file at build time (npx tailwindcss -o public/tailwind.css) and inject it viacanvas.styles:

options={{ canvas: { styles: ['/tailwind.css'], }, }} 

Caveat: Tailwind's JIT only generates classes it sees in your source files. If you want users to typearbitraryTailwind classes in the editor, you need either the full Tailwind reset + safelist, or a CDN build (https://cdn.tailwindcss.com) that JITs on the fly inside the iframe.

Pattern 2 — Use the Tailwind CDN inside the iframe

options={{ canvas: { scripts: ['https://cdn.tailwindcss.com'], }, }} 

This gives users the full Tailwind class space at the cost of a CDN dependency and a slightly larger canvas. Great for prototyping, fine for internal tools, probably not ideal for high-volume production editors.

The marketplace also ships aTailwind preset for GrapesJSthat bundles a class autocomplete and a curated block library — worth a look if you don't want to wire this up by hand.

Best practices that actually matter

Most performance and reliability issues with GrapesJS in Next.js trace back to four things:

Memoize youroptionsobject.If you buildoptionsinline in your render, you create a new object reference on every parent re-render. The wrapper is mostly stable to that, butonEditorand other callbacks should be wrapped inuseCallbackfor the same reason. Stable references → no unintended re-init.

const options = useMemo( () => ({ height: '100%', storageManager: false, plugins: [presetWebpage] }), [], ); const onEditor = useCallback((editor: Editor) => { /* … */ }, []); 

Code-split the editor.GrapesJS plus a couple of plugins is ~500 KB gzipped. If your editor lives at/editor/*, dynamic import (Pattern B above) keeps that out of your home page bundle. With App Router and route-level code splitting, just having the editor in a separate route segment already does most of this work — butnext/dynamicmakes it explicit.

Don't try to two-way-bind GrapesJS state.GrapesJS is its own state machine. Read from it on save (getProjectData()) or on specific events (editor.on('component:selected', …)), but don't try to mirror every change into Redux/Zustand. You'll burn cycles and create race conditions.

Mind React Strict Mode.In dev, components mount twice. The wrapper handles this for the editor itself. But if you register blocks or events inonEditor, guard against double registration —editor.Blocks.get('id')beforeadd, or use idempotent registrations.

Common pitfalls

A short, opinionated list of things that come up in support threads:

  • window is not definedat build time.Something is importinggrapesjsfrom a server component or a shared layout. Move the import behind 'use client'ornext/dynamic({ ssr: false }).
  • Editor mounts but panels are unstyled. grapesjsCss prop missing, or the import path is wrong. Verify with DevTools thatgrapes.min.cssis actually loaded.
  • Plugin blocks look broken inside the canvas.Plugin CSS not incanvas.styles. Each plugin's CSS needs to reach the iframe explicitly.
  • Editor flashes / re-initializes on parent state change.Unstable prop references. Memoizeoptions,onEditor, and any plugin lists.
  • Memory grows over time during navigation.Should not happen with the wrapper — it callseditor.destroy()on unmount. If it does, you've probably attached event listeners onwindowordocumentthat aren't being cleaned up inuseEffectcleanup functions.

Open-source @grapesjs/react vs Studio SDK: when to choose what

A quick decision framework:

  • Default to@grapesjs/react if the editor is a feature inside a larger product, you have React/TypeScript on staff, and you can absorb the integration work (Tailwind in iframe, asset uploads, custom blocks). It's free and you control everything.
  • Pick the Studio SDKif the editor is the product, you need React components in the canvas with SSR, you want vendor support, and the license fits your budget. The total cost of ownership is usually lower if you'd otherwise rebuild the same features.
  • Use Reactor Presetas a middle path: open-source GrapesJS plus a community preset to get React components in the canvas, with the understanding that you're on your own for production hardening.

Wrapping up

The@grapesjs/reactwrapper turns what used to be a fiddly DOM integration into a normal React component. Combined with App Router's 'use client'boundary, Server Actions for persistence, and a clear strategy for CSS-in-iframe, you can ship a production page builder without much custom plumbing.

Two follow-ups worth bookmarking:

  • The@grapesjs/reactGitHub repo has a full custom-UI example (panels, layers, asset manager) built around the hooks API. It's the best reference once you've outgrown the default UI.
  • The marketplace's Next.js-tagged plugins include SSR-safe blocks, storage adapters, and the Tailwind preset — worth browsing before you build any of those from scratch.

Happy building.


⚡ Next.js

¿Construir con GrapesJS + Next.js?

Evita los dolores de cabeza SSR. Explora plugins de GrapesJS seguros para SSR y diseñados para Next.js proyectos.

Plugins mencionados en este artículo

Comparte esta publicaciónTwitterFacebookLinkedIn
Publicado a través de
DevFuture Development
DevFuture Development
Visita la tienda →

Más de DevFuture Development

Descubre otras publicaciones interesantes y mantente al día con el contenido más reciente.

Ver todas las publicaciones

Plugins premium de DevFuture Development

Añadidos pagados seleccionados a mano por este creador.

Visita la tienda →