---
title: "Leiho for React: visual editing on WordPress"
description: "React with Vite, Remix or React Router: one component next to your app, and field() on what editors should change."
url: https://leiho.dev/frameworks/react
lang: en
updated: 2026-11-04
---
React · Also Remix and React Router

# Visual editing for React, on WordPress.

React with Vite, Remix or React Router: one component next to your app, and field() on what editors should change.

1. 01
   ## Allow the editor to display your site
   Set the header on your host, and in vite.config.ts for vite dev and vite preview.
   ```
   /*
     Content-Security-Policy: frame-ancestors 'self' https://cms.example.com
   ```
   ```
   {
     "headers": [
       {
         "source": "/(.*)",
         "headers": [
           { "key": "Content-Security-Policy", "value": "frame-ancestors 'self' https://cms.example.com" }
         ]
       }
     ]
   }
   ```
   ```
   import { defineConfig } from 'vite';
   import react from '@vitejs/plugin-react';
   
   const csp = "frame-ancestors 'self' https://cms.example.com";
   
   export default defineConfig({
     plugins: [react()],
     server: { headers: { 'Content-Security-Policy': csp } },
     preview: { headers: { 'Content-Security-Policy': csp } },
   });
   ```
2. 02
   ## Add the bridge
   Install the package, then add the component next to your app in src/main.tsx.
   ```Terminal
   npm install leiho
   ```
   ```tsx
   import { StrictMode } from 'react';
   import { createRoot } from 'react-dom/client';
   import { LeihoBridge } from 'leiho/react';
   import App from './App';
   
   createRoot(document.getElementById('root')!).render(
     <StrictMode>
       <App />
       <LeihoBridge wpOrigin="https://cms.example.com" />
     </StrictMode>
   );
   ```
   Remix or React Router: put it in app/root.tsx.
3. 03
   ## Read the content
   The app fetches one page at a time, from the browser.
   ```ts
   import { useEffect, useState } from 'react';
   
   export function usePage(path: string) {
     const [page, setPage] = useState<any>(null);
   
     useEffect(() => {
       const url = new URL('https://cms.example.com/wp-json/leiho/v1/content');
       url.searchParams.set('path', path);
       let cancelled = false;
       fetch(url)
         .then((res) => res.json())
         .then((items) => {
           if (!cancelled) setPage(items[0] ?? null);
         });
       return () => {
         cancelled = true;
       };
     }, [path]);
   
     return page;
   }
   ```
   Remix or React Router loaders: use loadLeihoContent from leiho/content instead.
4. 04
   ## Mark what editors can change
   Spread field() on each element editors should change. Always render the element, even when its value is empty.
   ```JSX
   import { field, entry } from 'leiho';
   // field() writes data-wp-post + data-wp-field, entry() names a section.
   // Flexible content rows: block(page, 'sections', i, 'Label').
   
   <section {...entry(page, 'Hero')}>
     <h1 {...field(page, 'hero_title')}>{page.fields.hero_title}</h1>
     <img {...field(page, 'hero_image')} src={page.fields.hero_image?.url} alt={page.fields.hero_image?.alt} />
     {page.fields.cards?.map((card, i) => (
       <article key={i} {...field(page, `cards.${i}.title`)}>{card.title}</article>
     ))}
   </section>
   ```

Replace https://cms.example.com with your WordPress address. In wp-admin, **Leiho → Get started** shows these steps with your address filled in, checks each one, and can give the whole setup to your coding agent as one prompt.

Other frameworks

- [**Next.js**](https://leiho.dev/frameworks/nextjs)
- [**Astro**](https://leiho.dev/frameworks/astro)
- [**Nuxt**](https://leiho.dev/frameworks/nuxt)
- [**SvelteKit**](https://leiho.dev/frameworks/sveltekit)
- [**HTML, PHP and others**](https://leiho.dev/frameworks/html)

## Edit the real site.

Free and open source. Try it on a real WordPress, in your browser.

- [Try it in your browser](https://leiho.dev/try)
- [View on GitHub](https://github.com/pixelersagency/leiho)
