INTEGRATION

Element Editor with Remix

Similar pattern to Next.js — client-only rendering plus loader/action symmetry for the data round-trip.

Install

npm install @element-editor/react maplibre-gl

Element needs React and React DOM 19.2 or newer.

Client-only rendering

The map engine needs window. In Remix, the pattern is ClientOnly from remix-utils or a manual dynamic check:

// app/components/Editor.tsx
import { ClientOnly } from 'remix-utils/client-only'
import { ElementEditor, type ElementEditorProps } from '@element-editor/react'
import '@element-editor/react/style.css' // includes MapLibre's CSS

export function Editor(props: ElementEditorProps) {
  return (
    <ClientOnly fallback={<div>Loading editor…</div>}>
      {() => (
        <div style={{ height: '80vh' }}>
          <ElementEditor {...props} />
        </div>
      )}
    </ClientOnly>
  )
}

ClientOnly defers render to the client, which is what Element needs.

Loader and action round-trip

Remix's loader/action model pairs well with Element's FeatureCollection types:

// app/routes/map.tsx
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'
import { json } from '@remix-run/node'
import { useLoaderData, useFetcher } from '@remix-run/react'
import type { FeatureCollection } from '@element-editor/react'
import { Editor } from '~/components/Editor'

export async function loader({ params }: LoaderFunctionArgs) {
  const data: FeatureCollection = await db.getFeatures(params.id)
  return json({ data })
}

export async function action({ request }: ActionFunctionArgs) {
  const data: FeatureCollection = await request.json()
  await db.saveFeatures(data)
  return json({ ok: true })
}

export default function MapRoute() {
  const { data } = useLoaderData<typeof loader>()
  const fetcher = useFetcher()

  return (
    <Editor
      initialData={data}
      onChange={(updated) => {
        fetcher.submit(JSON.stringify(updated), {
          method: 'POST',
          encType: 'application/json',
        })
      }}
    />
  )
}

Loader gives you the initial GeoJSON; action receives the updated shape. Types line up both ways.

Known gotchas

  • Keep ClientOnly. The map only starts in the browser. The Leaflet entry (@element-editor/react/leaflet) touches window as soon as it's imported, so with it, also move the import into a .client.tsx module.
  • Map CSS. @element-editor/react/style.css includes MapLibre's CSS. Import it at app/root.tsx or alongside the editor component.
  • Remix Vite vs Remix classic. Both work. The Vite version needs no special config beyond ClientOnly.

Ready to ship?