Skip to content

React with Vite

How midcode starts a Vite project with your own config plus one plugin, what it marks in your JSX and what you can edit, with Preact, Solid and Qwik.

View as Markdown

midcode edits a React app built with Vite in full: text, styles, images, structure, components with variants and states, and variables, all written into your .tsx and .jsx files. It starts Vite with your own config and one extra plugin, so the canvas shows your real app.

Compared with Next.js, three things are missing in the released version: making pages, Site settings and the free canvas. The next release brings Site settings, written into your index.html, new pages where the routes are files, and the free canvas, kept in src/midcode-scratch.tsx. A Vite app whose router is written in code has no list of pages to read.

At a glance

React with Vite
Detected byvite in package.json, together with react (or preact, solid-js)
Runs withvite dev, with a config of midcode’s that loads yours and adds one plugin
Elements are marked byThat plugin, as Vite transforms each .tsx and .jsx file
EditingFull, in .tsx and .jsx files
ComponentsYes: instances, props, variants and states, variables
PagesNone listed, unless the routes are files (TanStack Router, Qwik City: next release). New pages only there
StylesTailwind 4 classes, or mid: classes with midcode.css imported in the module index.html loads
Tried withVite + React with Tailwind 4, without Tailwind and with Tailwind 3

How midcode runs it

midcode reads package.json. next, @react-router/dev, @remix-run/dev, astro, @sveltejs/kit and nuxt are checked first, each with its own page in these docs. If none is there and vite is, the project is a Vite project. It’s editable when react, preact or solid-js is a dependency too. (With svelte or vue instead, see Svelte and SvelteKit and Vue.)

midcode then starts Vite itself, with your project’s own vite and the node of your login shell (midcode’s own Node if the Mac has none):

Terminal
vite dev /Users/you/sites/dashboard \
  --config "/Users/you/Library/Application Support/midcode/injected/vite.config.mjs" \
  --port 4310 --strictPort --host localhost

The port is the first free one from 4310 up. The dev server log (the button with the status dot in the top bar) shows the command and the Node that runs it.

The --config is what makes the canvas editable. It points at a file in midcode’s own data folder, outside your project. This is the part of it that matters (root is your project’s folder, entry('vite') finds the Vite in your node_modules, and the two variables hold the path of your config and of midcode’s plugin):

~/Library/Application Support/midcode/injected/vite.config.mjs
// Written by midcode: the project's Vite config, plus the midcode plugin.
export default async (env) => {
  const vite = await import(entry('vite'))
  const user = await vite.loadConfigFromFile(env, process.env.MIDCODE_USER_CONFIG || undefined, root)
  const { midcode } = await import(pathToFileURL(process.env.MIDCODE_VITE_PLUGIN).href)
  const config = user?.config ?? {}
  return { ...config, root: config.root ?? root, plugins: [midcode(root), ...(config.plugins ?? [])] }
}

Your vite.config (.ts, .mts, .js, .mjs, .cjs or .cts, at the project root) is loaded by your own Vite and returned as it is, with one more plugin at the front. Your file is never modified and nothing is added to package.json. The plugin only applies while serving, so it can’t take part in a build, and it only exists in the dev server midcode started.

If your config serves over HTTPS (a certificate plugin), midcode reads the address from Vite’s Local: line and accepts a self-signed certificate for localhost.

Options from your dev scriptNext release

What your dev script asks of Vite is kept. With "dev": "vite --mode staging --base /app/", midcode starts vite dev … --mode staging --base /app/. The port, the host, the config and opening a browser stay midcode’s to say, so --port, --host, --config, --open and --strictPort are dropped. The script is only read when it does nothing but call Vite: a && vite, or FOO=1 vite, isn’t.

What the plugin adds

The plugin runs on .tsx and .jsx files outside node_modules and adds attributes to what Vite serves. The rest of each file stays where it was, so line numbers stay true.

src/App.tsx
import { Hero } from './components/Hero'

export default function App() {
  return (
    <main className="mx-auto max-w-5xl px-6">
      <Hero title="Ship the dashboard" />
    </main>
  )
}
src/components/Hero.tsx
export function Hero({ title }: { title: string }) {
  return (
    <section className="py-24">
      <h1 className="text-5xl font-semibold">{title}</h1>
    </section>
  )
}

The <section> reaches the page like this:

HTML
<section data-mc="src/components/Hero.tsx:3:5" data-mcu="Hero|src/App.tsx:6:7" class="py-24">
  <h1 data-mc="src/components/Hero.tsx:4:7" class="text-5xl font-semibold">Ship the dashboard</h1>
</section>
  • data-mc: where the element is written, as file, line and column of its <.

  • data-mcu: on the element a component returns, where that component is used. Nested components make a chain, outermost first.

  • data-mci: the same Hero|src/App.tsx:6:7, handed to the component as a prop. It shows in the page only if the component passes its props on to an element.

When midcode’s way doesn’t start

Some projects need what only their own script sets up: an env file loaded by a wrapper, another process, a toolchain that wraps Vite. If Vite started this way exits, or doesn’t answer in 45 seconds, midcode stops it and runs your dev script instead (npm run dev, or the same with your package manager, in your shell), reading the address from its output.

The site then shows, but without marks, so nothing on the canvas can be edited. You notice in three places:

  • a message when the canvas appears: “midcode couldn’t start this project its own way, so it runs your dev script: the site shows, but editing on the canvas needs midcode’s plugin. The log says why.” It has a “Send report” button.

  • the dev server log, which says what happened (“it stopped”, or “no answer in 45 s”) before the second start.

  • the right panel: under Code, a selected element has no file and line.

The same happens from the start when midcode can’t find vite among the project’s installed packages but there’s a dev script (a toolchain that wraps Vite).

What you can edit

Select the <h1> above and change its size. One class is swapped:

src/components/Hero.tsx
-      <h1 className="text-5xl font-semibold">{title}</h1>+      <h1 className="text-6xl font-semibold">{title}</h1>

Double-click it and type. The words aren’t in Hero.tsx, so midcode looks for the exact text in the project and edits it where it’s written:

src/App.tsx
-      <Hero title="Ship the dashboard" />+      <Hero title="Ship the new dashboard" />

The rest of the editor works as it does everywhere JSX is edited:

Pages

A Vite app has no convention for pages, and a router written in code (createBrowserRouter, <Routes>) isn’t read. The page menu lists Home only. To see another page, open the menu, type its path (/pricing) and press Enter.

Routes kept as files are read in the next release: see TanStack Start, and Qwik City below. Those are also the Vite apps where midcode adds and removes pages, because a page there is a file. With a router written in code, a new page is yours or your agent’s to write.

Site settingsNext release

In the next release the globe in the top bar opens Site settings in a Vite project too. The title, the description, the language, search engines, the favicon and the social image are written as tags in the <head> of the index.html at the project’s root:

index.html
-    <title>Vite + React</title>+    <title>Oak Studio</title>+    <meta name="description" content="Furniture made to last." />   </head>

A tag that’s there has its value changed, and a new one goes last in the <head>. Images are copied into public/ as favicon and og-image, with the extension of the file you picked (favicon.png, og-image.jpg). A value Vite fills in when it builds (%VITE_APP_TITLE%) shows as code and isn’t written. There’s one form for the whole app: a page has no <head> of its own. It was tried on a Vite project with Vue, which has the same index.html. See Site settings.

Styles

With Tailwind 4 (a CSS file with @import "tailwindcss" or an @theme block), midcode writes Tailwind’s classes: see Tailwind CSS.

Without it, midcode writes mid: utilities and compiles them to midcode.css. On your first style edit the file is created next to the module your index.html loads, and imported there after its last import:

src/main.tsx
 import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx'+import './midcode.css'

If index.html loads no module, midcode looks for src/main.*, src/index.* or src/entry-client.*. If it finds none, it writes midcode.css anyway and tells you to import it. See Without Tailwind.

Preact, Solid and QwikNext release

A Vite project with preact, solid-js, @builder.io/qwik or @qwik.dev/core in place of react is started and marked the same way: it’s the same JSX and the same plugin. The released version already edits Preact and Solid projects this way. What the next release adds:

  • These libraries write class, not className. A new attribute follows the file it goes into: when the file says class= and never className=, midcode writes class, and what you drag in from Insert is converted (className to class, htmlFor to for).

  • midcode’s own interactive components (Carousel, Tabs, Slideshow, Ticker and the others) are React. Without react in the project they’re refused with “This component needs a React page”.

  • Qwik City only starts with vite --mode ssr, which is why the options of your dev script are kept. Its pages are listed: each folder of src/routes that has an index.tsx, .jsx, .mdx or .md. “New page” in the page menu makes such a folder with an index.tsx in it.

One thing to know: in a file that has no class attribute yet, midcode has nothing to follow and writes className.

Tried with a Vite + Solid and a Vite + Preact project made by create-vite, and with a Qwik City project. SolidStart has never been tried.

Limits

  • No free canvas in the released version: F inserts a frame in the page instead of drawing one on the empty canvas. The next release brings it to a Vite app with an index.html at its root.

  • No new pages and no Site settings in the released version. In the next release Site settings needs an index.html at the project’s root, and pages are added only where routes are files.

  • In the next release, a .midcode/server.json in the project replaces midcode’s own start with your command or address. The site then shows without marks, to view and comment: see Any other stack.

  • Only .tsx and .jsx files are marked.

  • Your vite.config has to be at the project root. A --config in your dev script isn’t followed.

  • The dev server listens on midcode’s port, not the one in your config or script. An app that only works on one fixed port (an OAuth callback, an API that allows a single origin) won’t get it.

  • An image your code imports (import logo from './logo.svg') isn’t swapped from the canvas: replacing imported images in place is Next.js only. An image referenced by its path is replaced and copied into public/.

  • An element drawn by a package has no mark, and one that’s moved stays in its file and its function.

  • A Laravel app that uses Vite is a different case: the canvas shows what PHP serves. See Laravel.

Troubleshooting

The site shows but nothing can be edited. midcode fell back to your dev script. Open the dev server log: the line before the second start says why. Usual causes are a config that reads something only your script sets, or a version of Vite midcode’s config can’t load. “Send report” sends the log and how the project is set up.

“Vite is not in node_modules. Install the dependencies.” The project has no dev script either. Install with the button on the card.

A style doesn’t show. Check that midcode.css is imported in your entry file.

More in Troubleshooting. In a workspace, see Monorepos.