Integrating GrapesJS into a Next.js 13+ App with the @grapesjs/react Wrapper

Seamlessly integrate GrapesJS with React, TypeScript, and Next.js using the official @grapesjs/react package for modern app development.

DevFuture Development
DevFuture Development
October 24, 20254 days ago
By DevFuture
32 min read143 views

GrapesJS is a powerful drag-and-drop template builder that many developers use to build page editors and email designers. If you're already using GrapesJS and want to embed it in a modern React application, you're in luck – the official @grapesjs/react package makes integration with React (and Next.js 13+ App Router) much cleaner. In this guide, we’ll explore how GrapesJS fits into a Next.js (React + TypeScript) architecture using the official wrapper, with practical examples, tips on plugins and custom components, and best practices for performance and reliability.


Why Use the @grapesjs/react Wrapper? (Architectural Overview)

The @grapesjs/react package is the official React wrapper for GrapesJS, designed to simplify using GrapesJS inside React apps github.com. Instead of manually initializing the GrapesJS editor in a useEffect and handling DOM manipulation, the wrapper provides a declarative React component that manages the editor’s lifecycle for you.

How it works: Under the hood, the wrapper creates the GrapesJS editor instance on component mount and provides it via React context to your components. This means you can structure a custom UI around the GrapesJS canvas using React, or simply use GrapesJS’s default UI. Importantly, the wrapper’s goal is not to replace GrapesJS’s own canvas or allow React components inside the canvas – it’s about integrating the GrapesJS editor around your React app. In other words, your React components remain outside the GrapesJS iframe; the GrapesJS canvas still renders HTML/CSS content as usual (which keeps things stable and isolated).


Next.js App Router considerations: Next.js 13+ introduced the App Router and React Server Components. GrapesJS is a browser-centric library (it accesses the DOM), so we must ensure it only runs on the client. The @grapesjs/react wrapper is compatible with the latest React 18/19 and Next.js versions (v2 of the package supports React 19 and Next.js 15+), but you still need to avoid server-side rendering for the editor component. We’ll achieve this by using Next’s use client directive or dynamic imports. The result is a seamless integration even in Next.js’s SSR environment – GrapesJS will load only on the client where it can access window and the DOM.


Setting Up GrapesJS in a Next.js 13+ Project

Let’s walk through setting up a Next.js App with GrapesJS and the React wrapper, using TypeScript for type safety. We assume you already have a Next.js 13+ project (using the App Router structure).


1. Install GrapesJS and the React wrapper: In your project, add the required packages:


npm install grapesjs @grapesjs/react
# or with yarn:
# yarn add grapesjs @grapesjs/react

The GrapesJS core library is a peer dependency of the wrapper, so you must install it separately. Ensure you have GrapesJS version >= 0.22.x for full compatibility.


2. Create a client component for the editor: In Next.js App Router, you might create a new page (for example, /app/editor/page.tsx) that will host the GrapesJS editor. This page should use a Client Component, since it involves browser-only logic. You can either mark the page as a client component or, better, encapsulate the editor in a separate component. Here we'll do it in one file for simplicity:


'use client'; // Ensure this page is rendered as a client-side component

import grapesjs, { Editor } from 'grapesjs';
import GjsEditor from '@grapesjs/react';
// Optionally import or include GrapesJS CSS:
import 'grapesjs/dist/css/grapes.min.css'; // core GrapesJS styles for panels, etc.

export default function PageBuilder() {
 // Handler to capture the editor instance once it’s ready
 const onEditorReady = (editor: Editor) => {
  console.log('GrapesJS Editor initialized:', editor);
  // You can store the editor instance in state or context if needed
 };

 return (
  <div style={{ height: '100vh' }}>
   <GjsEditor
    grapesjs={grapesjs}
    grapesjsCss="https://unpkg.com/grapesjs/dist/css/grapes.min.css"
    options={{
     // GrapesJS init configuration:
     height: '100%', // fill container height
     storageManager: false, // disable storage for this example (no local storage)
     // If you want to start with some content you can include it here:
     // components: '<h1>Hello GrapesJS</h1>',
     // style: 'h1{color:red;}',
    }}
    onEditor={onEditorReady}
   />
  </div>
 );
}

Let’s unpack this code:

  • We used the 'use client' directive at the top to ensure this page (and thus the GrapesJS component) is not attempted to be rendered on the server. GrapesJS will only load in the browser, avoiding SSR errors.
  • We import grapesjs and pass it to the <GjsEditor> component as a prop. This is required – the wrapper needs the GrapesJS library instance to initialize the editor
  • . (You could also pass a GrapesJS CDN URL string instead, but importing from NPM is typical in Next.)
  • We also pass a grapesjsCss prop pointing to the GrapesJS core CSS. This ensures the GrapesJS default styles are loaded (for the editor frame and panels). In this case we use the unpkg CDN link, but since we also imported the CSS locally, either approach works. The CDN approach loads the CSS asynchronously for you
  • The options prop contains the GrapesJS initialization config. This is equivalent to what you would pass to grapesjs.init(). For example, we set height: '100%' and disabled the storage manager (so changes are not auto-saved). You can include any GrapesJS options here – e.g., container is handled by the wrapper internally, but you can specify canvas settings, plugins, asset manager options, etc. (We’ll add plugins in a moment.)
  • The onEditor prop is a callback that gives you the GrapesJS editor instance once it’s created. We use it here just to log the editor or perform any setup actions. This is where you could load data into the editor, register events, or store the editor object for later use.


Running this in Next.js: When you navigate to /editor, the page will load the GrapesJS editor. The first load might take a moment (GrapesJS is a large library), but subsequent navigations will be faster thanks to caching. In development, you should see the GrapesJS default UI appear – the canvas (an empty white area by default) and the GrapesJS panels (Style Manager, Layer Manager, etc.) around it.


Handling SSR properly: If you forget to add 'use client' or otherwise try to import GrapesJS in a server context, you’ll encounter errors (e.g. "window is not defined"). An alternative pattern is to use Next’s dynamic import with ssr: false to ensure the component only loads on the client


'use client';
import dynamic from 'next/dynamic';
// Dynamically import the GrapesJS editor component to disable SSR
const GrapesEditorNoSSR = dynamic(() => import('@grapesjs/react').then(mod => mod.default), { ssr: false });

export default function PageBuilder() {
 return <GrapesEditorNoSSR grapesjs={grapesjs} grapesjsCss="..." options={{ /* ... */ }} />;
}

This achieves a similar result, and can be useful if you want to code-split the GrapesJS heavy module. In our case, using 'use client' on the page is sufficient because it ensures the entire page is client-side.


Loading Custom Plugins, Blocks, and Components

One of the strengths of GrapesJS is its extensibility via plugins and the ability to define custom block types or components. In a React+Next integration, you have two main ways to load plugins or add custom blocks:


A. Via GrapesJS init options (preferred for plugins): You can pass a plugins array and pluginsOpts in the options prop to automatically load plugins on editor init. For example, to include the popular Webpage preset plugin (which provides a set of basic blocks like text, image, columns, etc.), do the following:


  1. Install the plugin package: npm install grapesjs-preset-webpage.
  2. Import the plugin function and include it in the options:
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: {
   // optional: configure the preset plugin
   [presetWebpage.name as string]: { 
    /* e.g., blocks: ['link-block', 'quote', ...] to customize included blocks */ 
   }
  },
  canvas: {
   styles: [
    // Include preset CSS so blocks have proper styling in the canvas:
    'https://unpkg.com/grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css'
   ]
  }
 }}
 onEditor={onEditorReady}
/>


In the above:

  • We added presetWebpage to the plugins list, so GrapesJS will run that plugin on initialization. Most GrapesJS plugins (whether official or community) can be added this way by importing the plugin and adding it to the plugins array.
  • We also used canvas.styles in the config to inject the preset's CSS into the GrapesJS canvas iframe. This is crucial for plugin blocks that rely on specific styling. The GrapesJS config allows specifying an array of CSS files (URLs) to load into the canvas.
  • We point to the preset’s CSS via unpkg. (Alternatively, you could serve the CSS locally and use a relative path or import it and provide the path.)
  • The pluginsOpts allows passing configuration to plugins. For the preset-webpage, you might limit which blocks to include, etc. If you don't need to configure, you can omit pluginsOpts or leave an empty object.


With the preset loaded, your GrapesJS editor will start with a set of basic blocks and components (text, images, columns, etc.) ready to drag into the canvas. You should see the Blocks panel populated and working.


B. Via the onEditor callback or React hooks (for custom logic): If you want to register a custom block or component type that you’ve created, you can do so once the editor is initialized. Using the editor instance provided by onEditor, for example:


const onEditorReady = (editor: Editor) => {
 // 1. Add a custom block
 editor.BlockManager.add('my-custom-block', {
  label: 'Alert Box',
  category: 'Basic',
  content: `<div class="alert-box">Hello! 🥳</div>`,
  attributes: { class: 'gjs-fonts gjs-f-b1' } // icon for block (using grapesjs default set)
 });

 // 2. Define a custom component type (for the alert-box, with some default behavior)
 editor.DomComponents.addType('alert-box', {
  model: {
   defaults: {
    // default content and style
    tagName: 'div',
    attributes: { class: 'alert-box' },
    components: [{ type: 'text', content: 'Alert message...'}],
    stylable: ['background-color', 'padding', 'border-radius'] // allow styling
   }
  }
 });
};

In the above snippet, when the editor loads, we add a new block called "Alert Box" that inserts a <div class="alert-box"> element, and we register a corresponding component type 'alert-box' so that GrapesJS knows how to handle that element in the canvas (here we just specify defaults). This approach leverages the standard GrapesJS APIs (BlockManager.add, DomComponents.addType, etc.) but is done within the React app’s context. Since we are in a React environment, it’s often convenient to use the hooks provided by the wrapper instead of the raw onEditor callback – specifically useEditor or WithEditor – which make the editor instance available in any component.


For example, the wrapper offers a useEditor() hook that returns the editor instance (once created) anywhere in the component tree that’s inside <GjsEditor>. You could create a custom React toolbar component that uses this hook:


import { useEditor } from '@grapesjs/react';

function SaveButton() {
 const editor = useEditor(); // editor is guaranteed to exist here (must be used after editor is ready)
 const handleSave = () => {
  const html = editor.getHtml();
  const css = editor.getCss();
  // ...send to server or process as needed
  console.log('Saved HTML', html);
 };
 return <button onClick={handleSave}>Save Page</button>;
}

// In your editor page JSX:
<GjsEditor grapesjs={grapesjs} options={...}>
 <div style={{ display: 'flex' }}>
  <SaveButton />
  <Canvas /> {/* renders the GrapesJS canvas here */}
 </div>
</GjsEditor>

In this example, we took advantage of the wrapper’s Custom UI mode. By including our own JSX children inside <GjsEditor> and using the <Canvas/> component, we override GrapesJS’s default UI entirely. We placed a SaveButton component (which uses useEditor to get the editor) next to the <Canvas/>. The <Canvas> component is provided by @grapesjs/react and represents the GrapesJS iframe/canvas; you can position it wherever you want in your layout. Once you include <Canvas/>, the wrapper will disable GrapesJS’s built-in panels. This allows you to build a completely custom UI around the editor while still controlling GrapesJS.


For a full-fledged example of custom UI, the GrapesJS team provides a StackBlitz demo (linked in their docs) showing a React app implementing panels, toolbox, layers, etc., using context providers and hooks. For most use cases, you may start with the default UI and gradually replace parts of it with custom React components as needed.


Best Practices: Performance, Cleanup, and Styling

Integrating a complex library like GrapesJS into React/Next.js requires some care. Here are some best practices and tips to keep your integration smooth:


Prevent Multiple Instances (React Strict Mode): In development, React 18’s Strict Mode intentionally mounts components twice to help catch side effects. This could cause the GrapesJS editor to initialize twice if not handled. The @grapesjs/react wrapper is built to avoid double-initialization, but be mindful if you ever call grapesjs.init manually. Always ensure you create the editor only once. Using the wrapper’s component (which internally uses useEffect to init and a ref for the container) prevents this issue.


Destroy the Editor on Unmount: If your GrapesJS editor component ever unmounts (for example, navigating away from the page in a Single Page App context), you should destroy the editor instance to free memory. The official wrapper handles cleanup for you – when the <GjsEditor> component unmounts, it will call editor.destroy() internally. If you were doing a manual integration, you’d implement a cleanup in useEffect like return () => editor.destroy(). Failing to destroy can lead to memory leaks (event listeners and timers inside GrapesJS would persist). Always dispose of the editor when done.


Isolate GrapesJS to Client Side: We’ve stressed this, but it’s worth repeating as a pitfall: GrapesJS cannot run on the server. Use 'use client' and/or dynamic imports with ssr:false to ensure no part of GrapesJS executes during SSR. If you see errors about window or document not found, it means something is still trying to run on the server. Wrapping imports or the component in a conditional if (typeof window !== 'undefined') can also guard against this, but Next’s built-in patterns are usually sufficient.


Styling Across the Iframe (Canvas Styles): GrapesJS uses an iframe for the canvas, meaning the content being edited is in a separate document from your app. This provides isolation (your app's CSS won’t bleed into the canvas, and vice versa), but it also means if your blocks/components rely on certain CSS (e.g., a CSS framework like Bootstrap or custom styles), you need to explicitly load those styles into the canvas. Utilize the canvas.styles array in the GrapesJS config to inject external CSS URL. As shown earlier, we included the preset CSS this way. You can also add global styles by passing a CSS string via the style (for one page) or styles (for multiple pages) option in GrapesJS config. For example, style: '.my-class{...}' will apply to the initial content. For larger stylesheets, use the canvas.styles with a URL or base64 data URL. This ensures your canvas content looks as expected.


Tip: If you want the GrapesJS canvas to inherit some base styling (fonts, resets, etc.), you can create a small CSS file for the canvas. Many developers include a reset and perhaps a default font family via canvas.styles. Also remember that GrapesJS’s default components (like text, link, image) come unstyled, so if you want a default look (e.g., your text blocks to use a certain font or color), consider injecting a stylesheet or using the GrapesJS CSS rules.


Customizing GrapesJS UI Styling: While the canvas is isolated, the GrapesJS editor UI (panels, buttons, icons) lives in the parent document and is styled by grapes.min.css. You can override these styles if needed to better match your app. For instance, you could override the .gjs-pn-commands class to restyle the top bar, via your own CSS in your app (since those elements are in the React DOM). Be careful with specificity and only override what’s needed to avoid breaking the layout. The wrapper doesn’t provide theming out-of-the-box, so custom CSS is the way to tweak the appearance.


Performance Tips: GrapesJS is heavy, but you can do a few things to keep your app responsive:

  • Use dynamic import (code splitting) so that GrapesJS and its plugins are not loaded until the user actually needs the editor page. Next.js will create a separate chunk for the editor, reducing initial load of your app.
  • Disable features you don't need. For example, if local storage autosave (StorageManager) isn’t used, keep it off (as we did with storageManager: false) to avoid unnecessary operations. Similarly, avoid enabling GrapesJS’s undo manager or style manager if you’re not using them heavily – though in most cases you’ll want them.
  • Leverage GrapesJS’s new improvements for large content. Recent versions introduced a Virtual DOM and Virtual List rendering optimizations. If you plan to have hundreds of components in the canvas, ensure you use a version with VirtualList support (GrapesJS 0.22+). The VirtualList feature renders only what’s needed in the viewport for huge lists, improving performance for large pages.
  • Clean up event listeners if you add custom ones. For example, if you attach a editor.on('component:selected', ...) in onEditor, and your component re-renders or adds another, make sure you aren't duplicating the listener multiple times. You might store a ref to indicate a listener was added, or remove the listener on unmount. Again, the wrapper helps by existing within the component lifecycle, but it can't know about custom events you attach.


Avoid Rendering React inside GrapesJS Canvas: As mentioned, the official wrapper is not intended to magically let you use React components inside the GrapesJS canvas. The canvas outputs plain HTML/CSS. If you have a React component you want to appear in GrapesJS, you’d typically have to compile it to HTML or use a plugin approach (some experimental plugins render frameworks inside GrapesJS, but those are advanced). The GrapesJS team has introduced a

React Renderer (as part of the enterprise Studio SDK) that can render React/Vue components on the canvas, but that’s outside the scope of this article. For most use cases, treat GrapesJS canvas content as static markup. You can always post-process the saved HTML/CSS with React on the front-end of your site if needed (e.g., mounting interactive widgets after loading a saved page).


State Management: If you need to sync GrapesJS data with your React state or a global store (Redux, etc.), avoid trying to keep GrapesJS’s internal state in sync with external state in real-time – it can get complicated. Instead, use event hooks to know when something changes and then pull the data out. For example, editor.on('change:component:style', ...) could indicate a style changed; you might update a form or something. But often a simpler approach is: when the user clicks “Save”, just take editor.getProjectData() or .getHtml() and store that. Trying to two-way bind every change is overkill and can hurt performance. Embrace GrapesJS as a sort of isolated subsystem within your React app.


Common Pitfalls and How to Avoid Them

To recap, here are some common integration pitfalls and how to avoid them:

  • ❌ SSR/Hydration Errors: If you see errors when Next.js tries to render the editor (or a blank screen where the editor should be), check that you used 'use client' or dynamic import. Also, do not import GrapesJS or plugins at the top of a shared layout or any server component. Only import them inside the client component or dynamic module. This ensures Next’s server never even sees those imports.
  • ❌ GrapesJS appears but panels are unstyled: This usually means the GrapesJS CSS didn’t load. Make sure you either imported the CSS in a client context or provided the grapesjsCss prop. If the canvas content looks unstyled (e.g., preset blocks have no styles), ensure you loaded the plugin’s CSS into the canvas as described (using canvas.styles).
  • ❌ Editor re-initializes or loses state on re-render: The wrapper component should maintain the editor instance across re-renders (it doesn’t destroy and recreate unless unmounted). However, if you pass a new options object on every render (e.g., created in parent component each time), you might inadvertently cause the component to think props changed. Ideally, define your options object outside of render or wrap it in useMemo to avoid constant prop changes. The same goes for callback props like onEditor – define them stably (or use useCallback). This way the <GjsEditor> won’t reset. In most cases, you will mount it once so it’s not a big issue, but keep it in mind.
  • ❌ Memory leaks after navigating away: As mentioned, ensure destruction of the editor. If using the wrapper, this is handled, but if you ever use GrapesJS in a custom way, always call editor.destroy(). Also remove any DOM event listeners you attached (e.g., if you directly added window or document events). GrapesJS itself will remove its internal listeners on destroy.
  • ❌ Using outdated GrapesJS version: The React wrapper often requires a minimum GrapesJS version (for example, @grapesjs/react v1.x required GrapesJS >=0.21.3). If you see type errors or runtime issues, ensure your GrapesJS package is up to date. The same goes for React version compatibility (v2 of the wrapper is made for React 18/19 and Next 15). Check the README of the wrapper for the compatibility matri if in doubt.
  • ❌ Attempting to nest editors or multiple editors on one page: While it is possible to have more than one GrapesJS editor instance on a page, it’s advanced and not commonly needed. If you do attempt it, ensure each has its own container and do not mix their configurations. The wrapper could theoretically be used twice, but you’d want to scope any styles or ensure performance can handle it. Most of the time, one editor at a time is plenty.

By being aware of these pitfalls, you can save yourself a lot of debugging time. Many issues boil down to environment mismatches (server vs client) and loading of assets (CSS/JS).


Advantages of Using @grapesjs/react vs Manual Integration

Finally, let’s reflect on why using the official React wrapper is advantageous compared to the “manual” approach (where you might call grapesjs.init() in a React useEffect). Experienced GrapesJS users might have integrated it into React before this package existed, so what does the wrapper add?

  • Simplified Lifecycle Management: The wrapper takes care of initializing the editor at the right time and destroying it when the component unmounts. You don’t have to write effect cleanup logic or deal with refs for the container div – it’s all encapsulated. This reduces boilerplate and the risk of memory leaks or double inits.
  • Declarative UI Building: By exposing React context and hooks (like useEditor), @grapesjs/react allows you to build custom controls in React that interact with GrapesJS seamlessly
  • . Without the wrapper, you’d have to pass the editor instance around manually or use a Context you create yourself. The official wrapper provides this out-of-the-box, making your code cleaner and more modular.
  • Support for Custom UI mode: As shown, you can opt to use GrapesJS’s default UI or disable it and create your own panels using React. The wrapper makes the latter much easier by providing components like <Canvas/> and context providers. Manual integration could also hide the default UI, but wiring your own UI into GrapesJS events is more complex without the predefined hooks and structure the wrapper offers.
  • TypeScript Friendly: Both GrapesJS and the @grapesjs/react are written in TypeScript. When you use the wrapper, you get strong typing for the editor instance (Editor type from grapesjs), which helps with autocompletion and catching errors. The example onEditor callback above knows that editor is of type Editor (imported from grapesjs)
  • . If you integrated manually, you might have used any or had to import GrapesJS types anyway – the wrapper simply makes it more integrated.
  • Next.js Compatibility: The official wrapper was tested against Next.js and even provides notes on which version to use for which Next version
  • . This means edge cases (like compatibility with React 18’s changes, or future React 19 features) are more likely to be handled. In fact, GrapesJS’s recent updates explicitly mention seamless Next.js integration, which the wrapper facilitates. Using a well-maintained official package reduces the chance that a future React update will break your GrapesJS setup.
  • Community and Support: Being the official integration, it’s likely to be supported by the GrapesJS core team and community. If you encounter issues, you can check the GitHub issues on GrapesJS/react or ask questions in forums/Stack Overflow and get answers that assume you used the official approach. In contrast, if you roll your own integration, you might be on your own for solving certain problems.

Are there any reasons to not use the wrapper? In most cases, the wrapper is recommended. However, if you have a very custom scenario where you need to tightly control when GrapesJS initializes (e.g., in response to some user action, outside of the component render flow) or you have an existing codebase built around a different integration, you might stick to manual init. Also, if your use case is extremely minimal (like just mounting GrapesJS in a plain HTML page), adding React at all might be unnecessary overhead. But for any medium-to-large application where React/Next is already in use, the wrapper is a clear win for integration.


Conclusion

Integrating GrapesJS into a modern Next.js 13+ application is now much more straightforward thanks to the @grapesjs/react package. We covered how to set up the GrapesJS editor in a React component, use it within Next’s App Router architecture, and extend it with plugins and custom blocks. We also discussed best practices around styling (using the canvas iframe to your advantage), performance considerations, and avoiding common pitfalls like SSR issues and memory leaks.


With this setup, you can embed a fully-featured visual page builder into your React app – whether it’s for a CMS, a newsletter builder, or a page-personalization tool in your product. Developers who are already familiar with GrapesJS will appreciate that all the powerful APIs (commands, components, storage, etc.) remain available and are now accessible through a React-friendly interface.


Next Steps: Now that you have GrapesJS running in React, you might want to explore further:

  • Persisting Data: Look into GrapesJS’s Storage Manager API to save and load editor content to a database or backend. In a Next.js app, you could create an API route to save the editor.getProjectData() JSON.
  • Custom Plugins: If the existing plugins don’t meet your needs, consider writing your own GrapesJS plugin. The GrapesJS documentation has a guide on creating plugins. You can then import and use your plugin in the React integration as we did with preset-webpage.
  • Further Reading: Check out the official GrapesJS blog and release notes for updates. For example, the introduction of React 19 support, new components like Icon and VirtualList, and improved documentation were highlighted in a recent release. Staying up-to-date will help you leverage new performance improvements and features as they come.

With the combination of GrapesJS’s rich editor capabilities and Next.js’s powerful application framework, you can deliver a great editing experience integrated into your web app. Happy building with GrapesJS and React!

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 →