Two-way is not the same as symmetric - CrossUI Studio — Symmetric Visual IDE
Author
User<br>linb<br>Posts by this author<br>Posts by this author
~5 min read · Engineering
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...