2
3 Comments

Two-way is not the same as symmetric

Every visual editor for React says it's "two-way." Click something, see the code. Change the code, see the canvas update. Sounds symmetric. It's usually not.

Look closer at most tools and you'll find one side is doing real work and the other side is just watching. Design tool exports JSX once, and if you touch the code after that, good luck getting back in. Visual panel changes a prop, but it's writing to some in-memory state or a JSON config file, not your actual source. Sandbox previews your code fine, but try clicking on the rendered output to jump back into the file — nothing happens, it doesn't even know what produced that pixel.

Two-way, but not symmetric. There's always a first-class side and a side that's along for the ride. You can sort most of these into three buckets.

Export-once. Design tool spits out JSX one time. Nice for the first commit. Then someone edits the code by hand — which happens on day two, always — and the tool has no way back in. Round trip is a one-way trip wearing a costume.

Runtime-overlay. The visual panel changes something, but it's changing an in-memory prop or a config JSON, not the file. Refresh the page and it's gone, or it lives forever in a second file that now has to stay in sync with the real one by hand. Your code isn't the truth here. Some other file is.

Preview-only. CodeSandbox, StackBlitz, plain Vite HMR. Great at showing you the code running. Click on the rendered thing though — nothing happens. It doesn't know what produced that pixel, because nobody ever built that link.

We built something different for CrossUI Studio. We call it Symmetric Collaborative Development, SCD for short. The idea is simple to say and annoying to build: code and canvas are two views of the same AST. Neither one is the source of truth that the other has to catch up to. Both can read, both can write, and both write to the exact same place.

What "symmetric" actually has to mean

Marketing copy loves the word "two-way." I want to make it testable instead. Three things have to be true, or it's not symmetric, it's just two-way-ish.

Selection has to go both directions, cleanly. Click any element on the canvas, it resolves to exactly one AST node. Click any JSX node in the code, every rendered thing it's responsible for lights up on the canvas. Not "usually works." Both directions, every time.

Editing has to go both directions too, with equal power. If you can change a prop from the code, you can change it from the canvas. If you can restructure something from the canvas, that same change is expressible as a code edit. No "oh that one's canvas-only" or "sorry, edit the code for that."

There's one truth, not two. No hidden config layer, no shadow state that only the visual panel knows about. Every edit, from either side, lands as a real patch to the actual source file. What you get out is a diff you could commit, not an export you have to translate.

Most tools fail at least one of these. Usually the third one — they'll do fine selection and okay editing, but underneath it's writing to some intermediate format that isn't your code.

The pieces underneath

Mechanically, this runs on three things working together. An in-browser TypeScript/JSX compiler, so there's no build step standing between a keystroke and a render. A runtime layer that walks the live component tree as it actually renders, not just the static file tree. And an AST patcher that turns any edit, from either side, into one small diff instead of a full-file rewrite.

None of these three alone gets you symmetry. The compiler without the AST patcher just gives you fast previews. The runtime tree without the compiler gives you introspection but no live editing. It's the three stacked that make either side able to write.

Why this is actually hard to build

If it were easy everyone would have shipped it already. Here's where it gets messy.

The DOM doesn't remember where it came from. By the time React renders something to the screen, the structural info from your source is gone. You need to inject position info at compile time and keep a live index mapping rendered stuff back to AST nodes, and keep that index correct as things re-render.

One line of code, many things on screen. A .map() produces N elements from one JSX node. Click the third card — do you select the JSX expression, or "the third iteration of it"? Both are valid answers depending on what the user is trying to do, and the tool has to pick sensibly.

Some code draws nothing at all, right now. A ternary's other branch exists in the source but isn't on screen. If someone clicks that branch in the code, what does the canvas even show? There's no pixel to point at.

The thing on screen might be defined three files away. A component gets imported through two barrel re-exports before you reach its actual definition. The mapping has to survive that, and still land you on the real file, not a re-export shim.

Formatting and comments can't get mangled. Every patch needs to be small and clean, or nobody's going to want the diff in their PR. This alone rules out "just regenerate the file from a new AST" as a strategy — you have to patch, not rewrite.

The dependency graph is sometimes broken, and that's fine. Real repos have imports that don't resolve — missing packages, aliases nobody configured, whatever. If one broken edge kills the whole feature, you've built something too fragile to use on a real codebase. So it has to keep working around the parts it can't resolve.

None of these are exotic edge cases. They're just... what a real React app looks like on a random Tuesday.

What symmetry buys you

Once selection and editing are actually symmetric, some things stop being separate features and just become normal.

Point at a card buried inside a .map(), inside a conditional, inside a component from another file — and land exactly on the JSX that made it, in one click, no searching. Change a prop from the canvas side, and the diff you get is one clean line, same as if you'd typed it. Change the code, and the canvas updates without you needing to guess which part of the screen you just touched.

It also means you're never locked out. You can start from the canvas because you're thinking visually, switch to code because you need precision on a value, switch back because you want to see six components move together after a theme change. Nothing about that flow required an export step or a "sync now" button.

Walking one, for real

Take a real template, not a toy one — Material Kit React, the MUI admin kit. Click a revenue card on the dashboard. It's not a top-level thing — it's the fourth hop down: main.tsxDashboardPageOverviewAnalyticsViewAnalyticsWidgetSummary, and that last one is sitting inside a .map() over a list of stat cards.

One click, and the exact JSX for that card is selected in the code, not the whole map block, not the parent — that one card. Change total={714000} to total={928000} in the code. Canvas updated.

Now go the other way. Select the "New users" card on the canvas. In the Inspector, change its color from the default "secondary" to "success". Code updated. The change lands exactly where you'd expect:

   <AnalyticsWidgetSummary
     title="Weekly sales"
     percent={2.6}
-    total={714000}
+    total={928000}
     ...
   />
   <AnalyticsWidgetSummary
     title="New users"
     percent={-0.1}
     total={1352831}
-    color="secondary"
+    color="success"
     ...
   />

No reformatting. No touched imports. The other cards stay untouched. Real template. Real component. Real source.

Why this matters if you're teaching React, not just shipping it

A side effect I didn't expect going into this: it's a genuinely good teaching tool, maybe better than it is a shipping tool.

Beginners lose a lot of energy just holding the map between "this text" and "that rectangle on screen" in their head. Symmetric selection hands that map to the tool instead. Put a cursor inside a .map(), watch eight things light up at once on the canvas — that explains list rendering better than any slide ever has. Change a theme token and watch six unrelated components move, and context stops being a diagram on a whiteboard and starts being a thing you just saw happen.

It's the same shift devtools gave CSS. Before devtools: edit stylesheet, reload, squint, guess. After: click the element, see exactly which rule applies. Nobody thinks devtools made people worse at CSS. React never really got that moment for itself. This is closer to it.

A quick test for any tool claiming "two-way"

Three questions, works on anything, not just us:

Can I go from canvas to code and code to canvas, always, not just sometimes? Can I do the same kinds of edits from either side? And when I'm done, do I get a diff I can commit — or a format I have to translate first?

If the answer to any of those is "well, mostly," it's two-way. Symmetric is a stronger bar, and it's the one that actually matters once you're editing a real app instead of a demo.

on August 9, 2026
  1. 1

    The interesting positioning risk is that “symmetric” may be a technically precise distinction that still requires the buyer to understand the underlying architecture before they can value it. Your three-part test makes the difference obvious to a developer, but a first-time visitor may still ask, “Why does this change my workflow?” The stronger commercial story seems to be the outcome: no export step, no shadow state, and no mental translation between what you see and what you edit. I’d be curious whether leading with that workflow consequence before introducing SCD makes the differentiation click faster.

  2. 1

    The “two-way but not symmetric” distinction is really sharp. The .map() example especially makes the problem obvious — that’s where a lot of visual/code tools seem to hit the limits of what they actually understand.

Trending on Indie Hackers
I Just Discovered My Analytics Numbers Are Mostly Fake. Here Is Why. User Avatar 96 comments Co-founders suck… User Avatar 82 comments I built an AI that finds the right product for your customers User Avatar 41 comments I built a tool to find people already talking about problems your product solves User Avatar 34 comments Solo-built Pistly for months. Launching on PH this week and I still don't know if the market wants it. User Avatar 34 comments The easiest version of generation history was probably the least useful one User Avatar 32 comments