How to integrate GrapesJS into a Remix app (complete guide 2026)

Embed GrapesJS in a Remix app: mount it client-side in useEffect, save content to a Remix action, and export clean HTML/CSS.

DevFuture Development
DevFuture Development
May 21, 2026a month ago
7 min read4 views

Why GrapesJS fits Remix

GrapesJS needs the DOM, so in a Remix app you initialise it inside useEffect (client-only) while Remix's loaders and actions handle data on the server. This guide mounts the editor, saves through a Remix action, and exports HTML/CSS.

1. Mount the editor client-side

Create app/routes/editor.tsx. Import GrapesJS inside the effect so it never runs during SSR.

import { useEffect, useRef } from 'react';
import 'grapesjs/dist/css/grapes.min.css';

export default function Editor() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let editor: any;
    (async () => {
      const grapesjs = (await import('grapesjs')).default;
      editor = grapesjs.init({
        container: ref.current!,
        height: '100vh',
        fromElement: false,
        storageManager: false,
        components: '<h1>Hello from GrapesJS</h1>',
      });

      // Persist via a Remix action.
      document.getElementById('save')!.onclick = async () => {
        await fetch('/editor', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            html: editor.getHtml(),
            css: editor.getCss(),
            project: editor.getProjectData(),
          }),
        });
      };
    })();
    return () => editor?.destroy();
  }, []);

  return (
    <>
      <button id="save">Save</button>
      <div ref={ref} />
    </>
  );
}

2. Handle the save in a Remix action

// same app/routes/editor.tsx
import type { ActionFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { savePage } from '~/models/page.server';

export async function action({ request }: ActionFunctionArgs) {
  const data = await request.json();
  await savePage('home', data);   // your DB write
  return json({ status: 'ok' });
}

The action runs on the server, so your database client stays out of the browser bundle.

3. Load saved content back

export async function loader() {
  return json(await getPage('home'));
}
// in the effect, after init:
// const saved = await fetch('/editor?_data=...'); editor.loadProjectData(saved.project);

Common pitfalls in Remix

Code on a screen in a dark editor
Initialise the editor only in useEffect; keep the DB in the action.

Remix's server/client split is where things go wrong. Importing GrapesJS at module top level runs it during SSR and crashes the route — import it dynamically inside useEffect instead. Keep all persistence in the route's action (server) and never import your database client into the component, or it leaks into the browser bundle. If you load a saved project, read it in the loader and call editor.loadProjectData() after init — don't try to render editor state during SSR. Finally, return editor.destroy() from the effect so client-side transitions don't stack editor instances.

Prerequisites

You'll need Node.js 18+ and a Remix 2 app. No Remix-specific GrapesJS package is required — the editor is browser-only and Remix's loaders/actions handle data on the server. Familiarity with routes, useEffect, and Remix actions is enough.

Add custom blocks to the editor

Register draggable blocks with the Block Manager after init (inside the effect):

editor.BlockManager.add('hero', {
  label: 'Hero section',
  category: 'Sections',
  content: '<section class="hero"><h1>Headline</h1><p>Copy</p></section>',
});

Pull ready-made block libraries and presets from GJS.Market for a richer set.

Storage deep-dive: action + loader

Keep persistence on the server. POST the project to the route's action and read it back from the loader, so your database client never leaks into the browser bundle:

export async function action({ request }) {
  const data = await request.json();
  await savePage('home', data);          // server-only DB write
  return json({ status: 'ok' });
}
export async function loader() {
  return json(await getPage('home'));    // returns the saved project
}

After init, call editor.loadProjectData(saved.project) to reopen a page.

Performance tips

Import GrapesJS dynamically inside useEffect so it stays out of the main bundle and the server render path, and return editor.destroy() so client-side transitions don't stack instances. Code-split heavy plugins behind the feature that uses them.

Security considerations

Authenticate and authorise the action before writing — never accept an unauthenticated POST that overwrites a page. Sanitise stored markup on output if non-admins can edit. Validate the payload size so a large project can't exhaust memory.

Troubleshooting common errors

“window/document is not defined” means GrapesJS ran during SSR — import and init it only inside useEffect. An unstyled canvas means the stylesheet import is missing. A blank editor means the container ref wasn't ready at init. Hydration warnings usually mean you tried to render editor state during SSR.

When to use GrapesJS with Remix

GrapesJS fits when your Remix app embeds a real visual page or email builder your users control, with your own storage and HTML output. For inline rich text, a lighter editor is enough; for full-page composition with layout, styling, and clean export, GrapesJS is the stronger, MIT-licensed, self-hosted choice.

Next steps

See the related GrapesJS + React and GrapesJS + Next.js guides, browse the GrapesJS marketplace, or start from the GJS.Market home page.

FAQ

Does GrapesJS work with Remix server rendering?

Yes — initialise it only in useEffect so it runs in the browser. The route is server-rendered; only the editor instance is client-only.

How do I save GrapesJS data in Remix?

POST the project data to a Remix action and persist it in your database. Actions run on the server.

How do I load saved content back?

Fetch the saved project from a loader and call editor.loadProjectData(saved) after grapesjs.init.

More tags:
Published May 21, 2026
Updated Jun 27, 2026
⚛️ React

Using GrapesJS with React?

Discover React-friendly plugins and starter packs — ready to drop into your GrapesJS-powered app.

Share this postTwitterFacebookLinkedIn
Published via
DevFuture Development
DevFuture Development
Visit shop →

More from DevFuture Development

Discover other insightful posts and stay updated with the latest content.

View all posts

Premium plugins from DevFuture Development

Hand-picked paid additions crafted by this creator.

Visit shop →