<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jack Lee</title>
    <description>The latest articles on DEV Community by Jack Lee (@linb).</description>
    <link>https://dev.to/linb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F167185%2Fa03d48fd-2235-4fef-aa0a-9f5a2b9ea784.jpeg</url>
      <title>DEV Community: Jack Lee</title>
      <link>https://dev.to/linb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/linb"/>
    <language>en</language>
    <item>
      <title>Your sx prop is already a data structure, and almost no tool treats it that way</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 28 Sep 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/linb/your-sx-prop-is-already-a-data-structure-and-almost-no-tool-treats-it-that-way-229j</link>
      <guid>https://dev.to/linb/your-sx-prop-is-already-a-data-structure-and-almost-no-tool-treats-it-that-way-229j</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://blog.crossui.com/2026/09/shadcn-says-the-code-is-yours-so-why-cant-anything-edit-it" rel="noopener noreferrer"&gt;why shadcn is hard to edit visually&lt;/a&gt;, and somewhere in the middle I made a claim in passing: shadcn's styling is a string, MUI's &lt;code&gt;sx&lt;/code&gt; is data, and those two need different editors.&lt;/p&gt;

&lt;p&gt;A few people asked the obvious follow-up. Fine — what does the data side actually look like?&lt;/p&gt;

&lt;p&gt;This is that answer. It's less dramatic than the shadcn one, because nothing here was blocked on a rendering trick. It's more useful, though, because the interesting decisions are all about where to stop.&lt;/p&gt;

&lt;p&gt;Put the two side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p-4 hover:bg-accent md:p-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;sx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;:hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bgcolor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@media (min-width:900px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same design intent. The first is a token list that happens to have conventions baked into the token names. The second has actual shape: keys, nesting, values you can address by path.&lt;/p&gt;

&lt;p&gt;A string has to be parsed before you know what's in it. An object already knows. Which sounds like it makes the editor easy — and the tree part genuinely is easy. The hard parts moved somewhere else: deciding which keys deserve to be structure, and putting a change back in the file without touching anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nesting is a tree, so the UI is a tree
&lt;/h2&gt;

&lt;p&gt;The rule for what becomes a branch is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit → expandable branch, recurse. Miss → leaf, render a control.&lt;/p&gt;

&lt;p&gt;That's the whole thing, and it covers more than you'd expect. &lt;code&gt;&amp;amp;:hover&lt;/code&gt;, &lt;code&gt;@media (min-width:900px)&lt;/code&gt;, &lt;code&gt;&amp;amp; &amp;gt; *&lt;/code&gt;, &lt;code&gt;&amp;amp;::placeholder&lt;/code&gt;, &lt;code&gt;&amp;amp; .MuiButton-root&lt;/code&gt; — all of them become tree nodes for free, because CSS-in-JS conventions are prefix conventions.&lt;/p&gt;

&lt;p&gt;Seventeen of them ship with human labels: &lt;code&gt;&amp;amp;:hover&lt;/code&gt; → Hover, &lt;code&gt;@media (min-width:900px)&lt;/code&gt; → MD, &lt;code&gt;&amp;amp; &amp;gt; *&lt;/code&gt; → Direct Children. Anything not in that list still nests correctly, just without a friendly name. The rule is general; the labels are a courtesy.&lt;/p&gt;

&lt;p&gt;One decision in there is worth pulling out, because it's the kind of thing that only shows up once you've used the panel on real code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;isBranch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isExpression&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isResponsive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasChildren&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;isSelector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expressions and responsive values win over selectors.&lt;/strong&gt; If a key looks structural but its value is one of those two, it stays a leaf.&lt;/p&gt;

&lt;p&gt;That isn't a house style. It's how MUI's own &lt;code&gt;sx&lt;/code&gt; processor dispatches. When it walks an &lt;code&gt;sx&lt;/code&gt; object and hits a nested object, it asks whether the keys are breakpoints before it treats the thing as a nested style block — roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;hasBreakpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;resolveResponsive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;recurseAsNestedStyles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A breakpoint object is a &lt;em&gt;value spread across viewports&lt;/em&gt;. A selector object is a &lt;em&gt;scope containing more styles&lt;/em&gt;. They're written with identical syntax and mean entirely different things, so the styling engine has to disambiguate before it can do anything, and it disambiguates on the keys.&lt;/p&gt;

&lt;p&gt;The panel is drawing that same decision. &lt;code&gt;{ xs: 2, md: 3 }&lt;/code&gt; renders as one responsive control rather than a two-child subtree because that's what MUI is going to do with it. An expression stays whole for the same reason: MUI calls it with the theme and uses the result as a value, so the panel treats it as a value too.&lt;/p&gt;

&lt;p&gt;Which is also the practical answer. When someone writes a responsive object or drops in an arrow function, they're holding it in their head as one thing. Exploding it into a subtree turns a single decision into rows you have to keep in sync — and now the panel's model of the file disagrees with the framework's.&lt;/p&gt;

&lt;p&gt;Keep that in mind for the write-back section. The same instinct runs through it: touch as little as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tree's edges have to follow the real semantics
&lt;/h2&gt;

&lt;p&gt;Breakpoints in the panel aren't hardcoded. They come from the same &lt;code&gt;VIEWPORTS&lt;/code&gt; config the canvas uses for device sizes — MUI's breakpoint keys, with a representative device width picked for each: xs 375, sm 600, md 900, lg 1200, xl 1536.&lt;/p&gt;

&lt;p&gt;(Those widths are canvas device widths, not breakpoint thresholds. sm through xl happen to line up with MUI's defaults; &lt;code&gt;xs&lt;/code&gt; doesn't, because MUI's &lt;code&gt;xs&lt;/code&gt; floor is 0 and you can't render a 0px canvas.)&lt;/p&gt;

&lt;p&gt;The nice consequence is that the breakpoint you click in the style panel and the device you switch to on the canvas are the same object. Not two lists that have to be kept in sync by whoever remembers.&lt;/p&gt;

&lt;p&gt;Promoting a value to responsive fills &lt;strong&gt;all five&lt;/strong&gt; breakpoints with the current value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;{ xs: 2, md: 3 }&lt;/code&gt;. You differentiate afterward, by hand, at whichever breakpoint you actually care about.&lt;/p&gt;

&lt;p&gt;I've been asked why it doesn't write a minimal object. Because the panel is showing you five rows, and three of them would be blank while still rendering something — MUI cascades responsive objects upward, so the value is real even when the key is absent. Blank rows that aren't blank in the output are a bad place to start editing from. Filling all five means promotion is a visual no-op: nothing changes on screen, you just gained five editable slots. Then you change one.&lt;/p&gt;

&lt;p&gt;And when you do change one, only that one gets written. Editing the &lt;code&gt;md&lt;/code&gt; slot produces a patch carrying &lt;code&gt;dirtyPath: ['md']&lt;/code&gt;, which becomes &lt;code&gt;['p', 'md']&lt;/code&gt; by the time it reaches the writer. The other four are never re-emitted. (That matters more than it sounds. It's the next section.)&lt;/p&gt;

&lt;p&gt;Here's the part I think is actually worth the post, and it took me a while to see why it wasn't a limitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same-looking value gets different treatment depending on which path it's written on.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;sx={{ ... }}&lt;/code&gt;, any property can be promoted — including custom CSS keys you invented. &lt;code&gt;sx&lt;/code&gt; is MUI's own styling channel and it accepts breakpoint objects wherever a value goes. (On any component that processes &lt;code&gt;sx&lt;/code&gt;, which is every MUI component. A plain &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; doesn't have an &lt;code&gt;sx&lt;/code&gt; prop to begin with.)&lt;/p&gt;

&lt;p&gt;Written as a top-level system prop, it can't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* fine */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;   &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* not a thing */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;Box&lt;/code&gt;, &lt;code&gt;Stack&lt;/code&gt;, and &lt;code&gt;Grid&lt;/code&gt; support system props as top-level breakpoint objects. Typography's system props were deprecated in v6. So the panel offers promotion on those three and withholds it elsewhere — on the top-level path only.&lt;/p&gt;

&lt;p&gt;And under either path, a value that came from a spread can't be promoted at all, because there's nowhere unambiguous to write the result.&lt;/p&gt;

&lt;p&gt;That's three different answers for what looks, in the inspector, like the same number:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;where the value lives&lt;/th&gt;
&lt;th&gt;promotable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inside &lt;code&gt;sx={{ ... }}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;any property, on any component that takes &lt;code&gt;sx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;top-level system prop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Box&lt;/code&gt; / &lt;code&gt;Stack&lt;/code&gt; / &lt;code&gt;Grid&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything from a spread&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the panel flattened those into one rule, one of two things goes wrong. Offer promotion everywhere and you get a control that produces code MUI ignores — the panel looks right, the render doesn't change, and you lose twenty minutes deciding whether you misunderstood breakpoints or found a bug. Withhold it everywhere outside &lt;code&gt;Box&lt;/code&gt;/&lt;code&gt;Stack&lt;/code&gt;/&lt;code&gt;Grid&lt;/code&gt; and you've disabled a feature that works fine, on the path where it works fine.&lt;/p&gt;

&lt;p&gt;So the panel's edges follow MUI's semantics rather than a single convenient fiction about them.&lt;/p&gt;

&lt;p&gt;That's the concrete version of the argument I was making last week. "Does this accept a breakpoint object here?" is knowledge about the styling system, it differs by path within the same component, and it's only actionable if the styles are data you can reason about. &lt;strong&gt;A tool that edits &lt;code&gt;className&lt;/code&gt; as a string has no way to know the difference, and no place to put the distinction even if it did.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The three-second version, if you want to check it: put &lt;code&gt;p={{ xs: 1 }}&lt;/code&gt; on a &lt;code&gt;Box&lt;/code&gt; and on a &lt;code&gt;Typography&lt;/code&gt;, then look at the same spacing value written inside &lt;code&gt;sx&lt;/code&gt; on both. Three of those four offer promotion. On a free account the affordance is a gold lock rather than the control — responsive promotion is a paid feature — but a lock and an absence are different states, and telling them apart is the whole exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing back: no reserialize
&lt;/h2&gt;

&lt;p&gt;Most people assume a visual editor works like this: read the file, parse to AST, mutate the AST, print it back out.&lt;/p&gt;

&lt;p&gt;That assumption has a well-known answer already — recast and ts-morph have preserved original formatting for unmodified nodes for over a decade. So this isn't a story about a tool that mangles your file versus one that doesn't. That problem is solved.&lt;/p&gt;

&lt;p&gt;The difference here is where the ranges live. &lt;strong&gt;The IR carries them.&lt;/strong&gt; Every value node knows its own start and end offset in the source, so an edit doesn't need a print-then-reconcile pass at all. An edit is a path plus a new value, and the path resolves to a byte range:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every IR value node carries &lt;code&gt;range_v&lt;/code&gt; — where that value starts and ends in the source.&lt;/li&gt;
&lt;li&gt;An edit produces a &lt;code&gt;dirtyPath&lt;/code&gt; — &lt;code&gt;['p', 'md']&lt;/code&gt; — naming the one thing that changed.&lt;/li&gt;
&lt;li&gt;Write-back replaces that range. One splice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Which means your formatting, your comments, your quote style, that arrow function three properties down, the blank line you put there on purpose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They aren't preserved. They're untouched.&lt;/strong&gt; We never had them in hand, so there's nothing to get wrong.&lt;/p&gt;

&lt;p&gt;There's a second thing worth mentioning here, because it surprised me when I traced it. The theme editor and the &lt;code&gt;sx&lt;/code&gt; editor are the same writer. Three commands form one cascade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;updateJSObject  &amp;gt;  updateStyle  &amp;gt;  updateProp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;updateJSObject&lt;/code&gt; — the one the MUI theme editor emits — wraps itself in a synthetic &lt;code&gt;style&lt;/code&gt;, then rewrites its own command type and falls through to &lt;code&gt;updateStyle&lt;/code&gt;, which does the same thing again and falls through to &lt;code&gt;updateProp&lt;/code&gt;. Editing &lt;code&gt;createTheme()&lt;/code&gt;'s palette and editing an inline &lt;code&gt;sx&lt;/code&gt; value converge on the same byte-range splice by the time they hit the file.&lt;/p&gt;

&lt;p&gt;That 1310-line theme editor isn't a second implementation. It's a second entrance to one hallway.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part that answers last month's post
&lt;/h3&gt;

&lt;p&gt;I ended the shadcn piece on a problem I didn't have an answer for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;border-b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-red-500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buttonVariants&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="p"&gt;}))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set padding to 6 from a panel, and something has to decide which literal to edit. The base string? The conditional? The variant definition every other button on the page also uses? Sometimes there's no correct answer, only a policy.&lt;/p&gt;

&lt;p&gt;On the &lt;code&gt;sx&lt;/code&gt; side — &lt;strong&gt;when &lt;code&gt;sx&lt;/code&gt; is an inline object literal&lt;/strong&gt; — the question doesn't come up. Not because we solved it. Because objects have paths. &lt;code&gt;sx.p.md&lt;/code&gt; names exactly one span of bytes in exactly one file, and the panel doesn't have to choose anything.&lt;/p&gt;

&lt;p&gt;The qualifier is load-bearing, so let me be exact about it. Two shapes don't get that guarantee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;sx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sharedSx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;            &lt;span class="c1"&gt;// the path isn't in this JSX attribute at all&lt;/span&gt;
&lt;span class="nx"&gt;sx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;   &lt;span class="c1"&gt;// whatever base contributes lives somewhere else&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is the same class of problem as the shadcn one, arrived at from the other direction. The second I'd rather not characterize than characterize wrong.&lt;/p&gt;

&lt;p&gt;But that's the real difference between the two sides, and it's narrower than "objects good, strings bad": &lt;strong&gt;an inline object literal gives you an addressable path; string concatenation never does, no matter how it's composed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it refuses, and how
&lt;/h2&gt;

&lt;p&gt;Plenty of &lt;code&gt;sx&lt;/code&gt; values can't be turned into controls. That refusal happens when the panel reads, not when it writes.&lt;/p&gt;

&lt;p&gt;The IR has exactly three value shapes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;shape&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;th&gt;UI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ value }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a plain value&lt;/td&gt;
&lt;td&gt;visual control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ code }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an expression, source text kept verbatim&lt;/td&gt;
&lt;td&gt;expression editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ object }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a branch&lt;/td&gt;
&lt;td&gt;recurse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus &lt;code&gt;{ code, object }&lt;/code&gt; — source text &lt;em&gt;and&lt;/em&gt; parsed result, both retained.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;sx={{ p: theme =&amp;gt; theme.spacing(2) }}&lt;/code&gt; becomes &lt;code&gt;{ code: "theme =&amp;gt; theme.spacing(2)" }&lt;/code&gt;, stays a leaf, and gets a code editor instead of a slider. If the entire &lt;code&gt;sx&lt;/code&gt; prop is an expression the panel switches itself to advanced mode. Properties outside the Basic set say so in as many words: "This style is not available in Basic mode."&lt;/p&gt;

&lt;p&gt;None of that is silent degradation. The boundary is stated: here's a control, here's why there isn't one, here's your original text back. A structured editor that guesses wrong rewrites your source incorrectly. One that drops to text doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being straight about the gaps
&lt;/h2&gt;

&lt;p&gt;Three, and the third is the one that bothers me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsive promotion is a paid feature.&lt;/strong&gt; Free accounts get a gold lock and an upgrade prompt. I'd rather you learn that here than from the lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the top-level system-prop path it's &lt;code&gt;Box&lt;/code&gt;/&lt;code&gt;Stack&lt;/code&gt;/&lt;code&gt;Grid&lt;/code&gt; only&lt;/strong&gt; — explained above, and I think that's correct rather than partial, but it does mean the affordance is absent in places where a casual read would expect it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your custom theme tokens don't appear in the sx autocomplete.&lt;/strong&gt; The theme editor edits your project's real &lt;code&gt;createTheme()&lt;/code&gt;. The &lt;code&gt;sx&lt;/code&gt; suggestion list is a hardcoded table of MUI's default keys. &lt;code&gt;palette.primary.main&lt;/code&gt; is in it. &lt;code&gt;palette.brand.main&lt;/code&gt;, which you defined, is not.&lt;/p&gt;

&lt;p&gt;That last one should look familiar if you read the shadcn post. I wrote there that your &lt;code&gt;tailwind.config&lt;/code&gt; tokens are fed to the engine but never reach the inspector. This is the identical seam in a completely different styling system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rendering pipeline gets your project's configuration. The editing surface doesn't.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two style systems with nothing in common, the same unconnected wire in both. That's not two oversights. That's one missing idea — project configuration isn't a first-class input to the panel yet — and finding it twice is how I know it's structural rather than a todo someone forgot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual difference
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sx&lt;/code&gt; can be a tree because MUI's styling happens to be a data structure. shadcn's happens to be a string. Neither is a design failure; they're different bets, and both are reasonable.&lt;/p&gt;

&lt;p&gt;But they hand a tool completely different jobs. One asks you to understand structure. The other asks you to render it correctly before you can understand anything at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Change a value inside a string, and you have to decide which part of the string to change.&lt;br&gt;
Change a value inside an object, and the path already told you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything hard about visual editing lives on one side of that sentence or the other.&lt;/p&gt;

&lt;p&gt;If you work in MUI: &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;Studio has a free tier&lt;/a&gt;. Open a template, find an &lt;code&gt;sx&lt;/code&gt; prop with a &lt;code&gt;&amp;amp;:hover&lt;/code&gt; in it, and tell me where the tree gets it wrong. The write-back path is the part I'd most like to hear about breaking.&lt;/p&gt;

</description>
      <category>mui</category>
      <category>react</category>
      <category>sx</category>
    </item>
    <item>
      <title>shadcn/ui says the code is yours. So why can't anything edit it?</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/linb/shadcnui-says-the-code-is-yours-so-why-cant-anything-edit-it-2dc7</link>
      <guid>https://dev.to/linb/shadcnui-says-the-code-is-yours-so-why-cant-anything-edit-it-2dc7</guid>
      <description>&lt;p&gt;&lt;em&gt;~9 min read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;shadcn/ui's whole pitch is that it isn't a dependency. You run the CLI, the component lands in &lt;code&gt;components/ui/button.tsx&lt;/code&gt;, and it's yours. No package to upgrade, no wrapper to fight, no &lt;code&gt;!important&lt;/code&gt; war with someone else's stylesheet.&lt;/p&gt;

&lt;p&gt;Which raises a question nobody asks out loud: if the code is mine, why is hand-editing the only way to change it?&lt;/p&gt;

&lt;p&gt;Not a rhetorical complaint. There's a real technical answer, and it isn't the one you'd guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual editors assume a prop table. shadcn doesn't have one.
&lt;/h2&gt;

&lt;p&gt;Every visual React editor I've looked at makes the same bet: components arrive from a package, and that package has a documented, stable prop surface. MUI is the ideal case. &lt;code&gt;&amp;lt;Button variant="contained" size="large" color="primary"&amp;gt;&lt;/code&gt; — three enums with known values. A tool can read the type, render three dropdowns, write back a string. It works because the component is a black box with a labelled control panel bolted to the front.&lt;/p&gt;

&lt;p&gt;Now open a shadcn button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buttonVariants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cva&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline-flex items-center justify-center rounded-md text-sm font-medium ...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-primary text-primary-foreground hover:bg-primary/90&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;destructive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-destructive text-destructive-foreground ...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;border border-input bg-background hover:bg-accent ...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h-10 px-4 py-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h-9 px-3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h-11 px-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;defaultVariants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no prop table. There's a variant map — written in your repo, in plain TypeScript, in a file you own.&lt;/p&gt;

&lt;p&gt;The standard read is that this is worse for tooling: no types to introspect from a package, no docs to scrape, everyone's copy drifts. And it's true that you can't point a package-shaped tool at it.&lt;/p&gt;

&lt;p&gt;I think the opposite conclusion is the right one. &lt;strong&gt;A CVA map is strictly more information than a prop table&lt;/strong&gt;, and it's information you already have on disk.&lt;/p&gt;

&lt;p&gt;A prop table tells you &lt;code&gt;variant&lt;/code&gt; accepts &lt;code&gt;"contained"&lt;/code&gt;. It does not tell you what &lt;code&gt;"contained"&lt;/code&gt; does. That's compiled into the library. To find out, you read the docs, or you guess, or you try it.&lt;/p&gt;

&lt;p&gt;The CVA map tells you &lt;code&gt;variant: "destructive"&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; &lt;code&gt;bg-destructive text-destructive-foreground&lt;/code&gt;. The mapping is right there, as data, in source you control. Nothing is hidden, because nothing was ever packaged.&lt;/p&gt;

&lt;p&gt;So the argument is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Structured visual editing needs to know what a control does, not just that it exists.&lt;br&gt;
With a package component, that knowledge lives in the library's compiled internals.&lt;br&gt;
With shadcn, it lives in your repo as a literal object.&lt;br&gt;
&lt;strong&gt;shadcn should be the easiest component library to edit visually, not the hardest.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, the thing I can't find anywhere is structured variant editing driven by &lt;em&gt;your&lt;/em&gt; CVA map, in &lt;em&gt;your&lt;/em&gt; repo. Visual editors for Tailwind exist, and theme editors for shadcn exist — but a theme editor rewrites CSS variables, which is a different job: it changes what &lt;code&gt;bg-primary&lt;/code&gt; resolves to, not which variant a button is using or what that variant is made of. The obstacle to the second one turns out to sit a layer below variants.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obstacle: shadcn's styling is class names, and class names need a build step
&lt;/h2&gt;

&lt;p&gt;MUI styles at runtime. &lt;code&gt;sx={{ p: 2 }}&lt;/code&gt; becomes CSS while the component renders — nothing precomputed, nothing to scan. MUI in a browser with no build step is plenty of work, but none of it is styling work: it's module resolution, keeping emotion and the theme as true singletons, and shipping a coherent set of package versions (we vendor eight MUI-related builds across three version tiers for exactly that reason). We &lt;a href="https://blog.crossui.com/2026/08/no-install-no-dev-server-vibe-coding-era" rel="noopener noreferrer"&gt;wrote about that side here&lt;/a&gt;. Once the modules resolve, the styles take care of themselves.&lt;/p&gt;

&lt;p&gt;Tailwind works the other way around. It scans your source files, sees which utility class names appear as text, and generates exactly those rules. No scan, no CSS. And "scanning source files" is a build step.&lt;/p&gt;

&lt;p&gt;So for a tool that renders a real project in the browser with no install and no dev server, shadcn presents a wall that MUI never did. You can compile &lt;code&gt;button.tsx&lt;/code&gt; perfectly and get an unstyled button. The class names are all present in the DOM and mean nothing, because nothing generated the rules.&lt;/p&gt;

&lt;p&gt;That's the actual reason visual tooling stops at the door. Not variants. Styling.&lt;/p&gt;

&lt;p&gt;There's a sharper version of the problem, too. Tailwind's scanner reads source text. But shadcn components don't have class names in source text — they have a function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buttonVariants&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no string &lt;code&gt;"bg-destructive"&lt;/code&gt; anywhere in that line. It's assembled at runtime from the CVA map, the incoming props, and whatever the caller passed.&lt;/p&gt;

&lt;p&gt;In a normal build this is handled, and handled well: the variant map file gets scanned too, the literals live there, and the extractor sees them. (Worth saying plainly, since it gets garbled a lot: Tailwind's rule is don't &lt;em&gt;construct&lt;/em&gt; class strings like &lt;code&gt;`text-${color}-500`&lt;/code&gt;. CVA variant maps hold complete literals. shadcn is the recommended pattern, not a workaround, and it needs no safelist.)&lt;/p&gt;

&lt;p&gt;In a browser with no build step, there is no scanning pass at all — so there is nothing to handle it. That's what makes this version of the problem sharper rather than the same problem again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did instead: collect classes from the DOM, not from the source
&lt;/h2&gt;

&lt;p&gt;The fix is to stop scanning source entirely.&lt;/p&gt;

&lt;p&gt;Studio runs Tailwind's real browser engine — the official one, not a reimplementation. The version is picked from what the project declares: v3 projects get Tailwind's Play CDN engine, vendored in the app rather than fetched from the CDN; v4 and up get &lt;code&gt;@tailwindcss/browser&lt;/code&gt;, likewise vendored. A remote fetch happens only when no vendored build matches the declared major — a v5 project, say. Your &lt;code&gt;tailwind.config&lt;/code&gt; is fed to the engine, so your theme extensions are the ones in effect.&lt;/p&gt;

&lt;p&gt;The obvious question here is why not run Tailwind's own extractor in the browser and scan the source after all. We measured what that costs: one render already issues somewhere between 40 and 340 file reads on the host side, and on a large template a single read runs 5–8 ms with no throughput gain from concurrency. Scanning the repo means reading every file instead of the ones that render. And after paying for it you still miss class names that come from runtime data. Collecting from the DOM isn't the lazy option — it trades "read the whole repo" for "read the result once."&lt;/p&gt;

&lt;p&gt;Running the engine gets you halfway. Then the interesting problem shows up.&lt;/p&gt;

&lt;p&gt;The engine is document-scoped and only observes light DOM — it calls &lt;code&gt;observe(document.documentElement)&lt;/code&gt; and never touches shadow roots. Studio's design mode renders the template inside a shadow root, for isolation. So the engine looks at the document, sees none of your component's class names, and generates nothing. Correct engine, correct config, zero output.&lt;/p&gt;

&lt;p&gt;What bridges it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After render, walk the shadow DOM and collect every class token actually present on actual elements.&lt;/li&gt;
&lt;li&gt;Write those tokens into a hidden sink element in the light DOM, where the engine can see them.&lt;/li&gt;
&lt;li&gt;The engine generates rules for exactly the utilities in use.&lt;/li&gt;
&lt;li&gt;Adopt the resulting stylesheet back into the shadow root.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;MutationObserver&lt;/code&gt; on the shadow root — subtree, &lt;code&gt;attributeFilter: ['class']&lt;/code&gt; — catches everything that appears later: route changes, lazy content, anything conditional.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One detail worth flagging because it cost real time: the sink must be inserted with native &lt;code&gt;appendChild&lt;/code&gt;. The canvas patches &lt;code&gt;document.body.appendChild&lt;/code&gt; to redirect portal content into the shadow root, and going through the patched version buries the sink inside the shadow root — where the engine can't see it. The bridge then fails silently, which is the worst kind.&lt;/p&gt;

&lt;p&gt;The sink is a real node in the light DOM, and it keeps filling as you use the app — switch routes and the classes the new page needs show up in it, without a reload.&lt;/p&gt;

&lt;p&gt;There's a second gotcha like the first one. It's not a shadcn thing — it comes from other templates we render — but it's the same failure shape. Tailwind v3 templates built on Next often set &lt;code&gt;important: '#__next'&lt;/code&gt; in their config, scoping every utility under the app's root ID. That ID doesn't exist in the canvas, so every utility silently fails to match. The config's selector gets redirected to the live render root — and the replacement has to also be an ID selector, because the &lt;code&gt;(1,0,0)&lt;/code&gt; specificity is doing load-bearing work against the template's own CSS.&lt;/p&gt;

&lt;p&gt;The payoff for doing it this way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because collection happens from the DOM instead of the source, any class name assembled at runtime works.&lt;/strong&gt; &lt;code&gt;cn()&lt;/code&gt;, &lt;code&gt;clsx&lt;/code&gt;, &lt;code&gt;twMerge&lt;/code&gt;, CVA, a ternary, a lookup table, a class name built from a prop by string concatenation — the engine never has to understand any of it. By the time collection runs, the composition already happened and the result is sitting in a &lt;code&gt;class&lt;/code&gt; attribute. shadcn-admin renders correctly not because we special-cased shadcn, but because this approach is indifferent to how the string got there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limit:&lt;/strong&gt; only classes that have actually rendered get generated. A real Tailwind build scans source, so it emits both branches of &lt;code&gt;cond ? 'bg-red-500' : 'bg-blue-500'&lt;/code&gt; even though only one can be on screen. Here the untaken branch has no CSS until it renders. The &lt;code&gt;MutationObserver&lt;/code&gt; fills it in when state flips — but that means &lt;strong&gt;the frame where it flips can be unstyled&lt;/strong&gt;. In practice you see it as a brief flash on the first toggle of a variant you haven't hit yet.&lt;/p&gt;

&lt;p&gt;It cuts the other way too, which is why I'd call it a tradeoff and not a deficit. Take &lt;code&gt;statusColors[res.status]&lt;/code&gt;, where the key arrives from an API response. No complete utility literal exists anywhere in the source, so a scanning build generates nothing for it — this is the case safelists exist to paper over. DOM collection doesn't distinguish it from anything else; by the time we look, the class is on the element. Source scanning gets you branches that haven't run. DOM collection gets you class names that were never written down. You pick which one you'd rather have, and in a browser the choice is mostly made for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the argument
&lt;/h2&gt;

&lt;p&gt;So: shadcn-admin renders. A template built entirely on class names, CVA variants, and &lt;code&gt;cn()&lt;/code&gt; composition, running with no install and no dev server, with the real Tailwind engine and your real config.&lt;/p&gt;

&lt;p&gt;Which is worth something specific, and it isn't "visual editing."&lt;/p&gt;

&lt;p&gt;Think about the last time you opened an unfamiliar admin template to decide whether to use it. You cloned it, installed it, waited, ran it, clicked around for four minutes, and closed it. Most of that time was the machine's, not yours. Now think about the last time you wanted to show a teammate what a component looked like in three states, or check whether a template's dark mode was actually finished, or figure out where a card's spacing came from before quoting a change. Same tax, every time, for a question that takes seconds to answer once something is on screen.&lt;/p&gt;

&lt;p&gt;That tax is the thing that's gone. A shadcn project renders in seconds with its real classes, and then everything Studio does that isn't styling-specific works on it: &lt;a href="https://blog.crossui.com/2026/08/two-way-is-not-the-same-as-symmetric" rel="noopener noreferrer"&gt;selection that goes both ways between code and canvas&lt;/a&gt;, &lt;a href="https://blog.crossui.com/2026/08/drill-down-all-the-way" rel="noopener noreferrer"&gt;drilling into a component defined five files away&lt;/a&gt;, &lt;a href="https://blog.crossui.com/2026/08/what-breaks-if-i-delete-this-file" rel="noopener noreferrer"&gt;seeing what breaks if you delete a file&lt;/a&gt;. Those operate on your code, not your styles, so they never cared which styling system you picked. They just needed the thing to render first.&lt;/p&gt;

&lt;p&gt;What's still missing is the styling side of the inspector: &lt;code&gt;className&lt;/code&gt; is edited as a string like any other prop, nothing reads your CVA map to build variant controls, and your &lt;code&gt;tailwind.config&lt;/code&gt; tokens reach the engine but not the panel. The MUI side has a theme editor and a structured &lt;code&gt;sx&lt;/code&gt; tree; the Tailwind side has none of that yet. I'd rather say so than let a demo imply otherwise.&lt;/p&gt;

&lt;p&gt;But that ordering is the actual point, and it isn't obvious from outside:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;shadcn's styling is class names, not props.&lt;br&gt;
So correctly rendering runtime-composed class names is the &lt;strong&gt;prerequisite&lt;/strong&gt; for editing them in any structured way.&lt;br&gt;
That prerequisite is genuinely hard in a browser, because the engine only sees light DOM and design-mode rendering happens in a shadow root.&lt;br&gt;
The class bridge solves it by collecting from the DOM rather than the source.&lt;br&gt;
&lt;strong&gt;The prerequisite is now in place.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What sits on top of it is a smaller problem than what's underneath, but not a trivial one, and I don't want to wave at it. A CVA map is a literal object in a file you own; reading it and rendering a variant selector is ordinary work. Writing the change back is not. A class name can arrive from three composition sites at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;border-b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-red-500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buttonVariants&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="p"&gt;}))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set padding to 6 in a panel, and something has to decide which literal to edit — the base string, the conditional, or the variant definition that every other button on the page also uses. Sometimes there's no correct answer, only a policy. That's the next real problem, and it's hard in a different way than this one was.&lt;/p&gt;

&lt;p&gt;If you'd asked me before starting which half would be hard, I'd have guessed wrong.&lt;/p&gt;

&lt;p&gt;If you work in shadcn and have opinions about what that control surface should look like — variant dropdowns driven by the CVA map, spacing controls that rewrite utilities rather than inline styles, a rule for which literal a write-back should land in — that's a conversation I'd like to have before building it rather than after. &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;Studio has a free tier&lt;/a&gt;; point it at a shadcn project and tell me where it's wrong.&lt;/p&gt;

</description>
      <category>react</category>
      <category>shadcn</category>
      <category>tailwindcss</category>
      <category>cva</category>
    </item>
    <item>
      <title>Every React instructor has drawn this box diagram. Let's go further</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 14 Sep 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/linb/every-react-instructor-has-drawn-this-box-diagram-lets-go-further-3ch6</link>
      <guid>https://dev.to/linb/every-react-instructor-has-drawn-this-box-diagram-lets-go-further-3ch6</guid>
      <description>&lt;p&gt;You finish teaching props. You drew the box diagram, you gave examples. And they got it — you ask a question in class, they answer it fine.&lt;/p&gt;

&lt;p&gt;Next week, in the homework, the same student asks: "how do I know where this total comes from?"&lt;/p&gt;

&lt;p&gt;He didn't forget what you said. He knows what props are. He just can't call on it inside real code.&lt;/p&gt;

&lt;p&gt;The box diagram got him to "I understand." It doesn't reach "I can use it." That part was never in range of an explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I want to say first
&lt;/h2&gt;

&lt;p&gt;Your teaching is fine. They really do understand.&lt;/p&gt;

&lt;p&gt;The problem is the next step. &lt;strong&gt;There's a distance between understanding and using, and that distance doesn't close with a better explanation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;props, context, &lt;code&gt;.map()&lt;/code&gt;, conditional rendering, module structure — these are relational, not factual. "useState returns an array" is a fact. Say it once, done. "This line of JSX corresponds to that thing on screen" is a mapping. Mappings only get built by matching them up, over and over.&lt;/p&gt;

&lt;p&gt;You can explain a mapping beautifully. They'll understand it. But understanding a mapping and being able to call on it instantly are two different things.&lt;/p&gt;

&lt;p&gt;This isn't about teaching skill. Explanation has a ceiling, and the ceiling is at "understand." Getting past it takes reps from the student — and whether they get reps depends on whether they have something they can poke at.&lt;/p&gt;

&lt;p&gt;So: four places this shows up every term. In all four, the students understood you. They still got stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  One: your first class gets eaten by environment setup
&lt;/h2&gt;

&lt;p&gt;Thirty people. The lesson plan says "get a React project running."&lt;/p&gt;

&lt;p&gt;What actually happens: five have the wrong node version. Three hit npm permission errors on Windows. Two are behind a corporate network that won't install packages. One installed nvm into the wrong shell config. You spend forty minutes doing tech support and the last twenty rushing through half your material.&lt;/p&gt;

&lt;p&gt;What it actually costs you:&lt;/p&gt;

&lt;p&gt;Your class time goes to something that has nothing to do with React. And you pay it again with every new cohort.&lt;/p&gt;

&lt;p&gt;You end up maintaining a setup guide doc. Every time node, npm, or vite moves, you update it. Unpaid, forever.&lt;/p&gt;

&lt;p&gt;You can't assign "go run this at home," because half the class gets stuck and then your weekend is answering setup questions.&lt;/p&gt;

&lt;p&gt;And the quiet one: &lt;strong&gt;you filtered people out in week one, on a criterion that has nothing to do with React.&lt;/strong&gt; The ones who dropped weren't bad at React. They just couldn't get an environment running yet. But they'll read that failure as "I'm not cut out for this."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes it:&lt;/strong&gt; opening a real project costs about as much as opening a webpage. No install, no dev server — the compile happens in the browser, so there's nothing on their machine to go wrong. Which means the goal of your first class can actually be "read the structure of a real project" instead of "get thirty laptops to agree with each other." (&lt;a href="https://blog.crossui.com/2026/08/no-install-no-dev-server-vibe-coding-era" rel="noopener noreferrer"&gt;how that works&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Someone always asks: so they never learn to set up an environment? They do. But setup is a standalone skill you can teach in week five, on purpose, with your attention on it. Putting it in week one as a gate is just a sequencing mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two: they understood props. They still can't use props.
&lt;/h2&gt;

&lt;p&gt;A student can read &lt;code&gt;&amp;lt;AnalyticsWidgetSummary total={714000} color="warning" /&amp;gt;&lt;/code&gt; and tell you what every part does. He can define props correctly.&lt;/p&gt;

&lt;p&gt;Ask him what happens if you change &lt;code&gt;total&lt;/code&gt; — he pauses. Ask where &lt;code&gt;color="warning"&lt;/code&gt; is actually defined — he has no direction.&lt;/p&gt;

&lt;p&gt;He's not confused about props. The mapping just isn't reflexive yet.&lt;/p&gt;

&lt;p&gt;Here's why explanation tops out here, and I think this is the real point of this whole post:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A diagram is you doing the mapping for them.&lt;/strong&gt; They see the result, they understand that it holds, but they never walked it themselves.&lt;/p&gt;

&lt;p&gt;It's the gap between reading a translation and being able to translate. Passive understanding and active recall run on different tracks. The first doesn't automatically become the second.&lt;/p&gt;

&lt;p&gt;And there's a counterintuitive bit: &lt;strong&gt;the clearer you explain it, the easier it is for them to stop at "I get it"&lt;/strong&gt; — because the quality of your explanation saved them from having to build the mapping themselves.&lt;/p&gt;

&lt;p&gt;So investing more in making the explanation better returns less and less. Not because the method is wrong. Because that road ends there.&lt;/p&gt;

&lt;p&gt;Which is why you keep building fancier analogies for the same concept and the results don't scale with the effort. Your part is done. The missing part isn't on your side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes it:&lt;/strong&gt; click code, the canvas highlights. Click the canvas, the code highlights — the exact JSX node, not the general area. That's not impressive technology; it's that it turns one thing you said into forty things they did. (&lt;a href="https://blog.crossui.com/2026/08/two-way-is-not-the-same-as-symmetric" rel="noopener noreferrer"&gt;the mechanics of that&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something you can use next week:&lt;/strong&gt; before you define props abstractly, have them do twenty clicks. Let them notice on their own that this text corresponds to that pixel. Give the concept its name after the operation, not as setup for it.&lt;/p&gt;

&lt;p&gt;And the obvious objection: won't they get dependent on the tool and never learn to find files? Depends on whether the tool shows the path. If it silently navigates for them and they never look, sure, that's a crutch. If it shows the component chain it walked and which file each layer lives in, then they're seeing real structure over and over until it stops being mysterious. That's training wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three: three concepts, three lessons, everyone half-gets each one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.map()&lt;/code&gt;.&lt;/strong&gt; Student asks how to change the third card. You say you're changing the template, not the third card. He understands the sentence. But his eyes see five separate cards. Accepted intellectually, not built as intuition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional rendering.&lt;/strong&gt; He writes &lt;code&gt;isVip ? A : B&lt;/code&gt;, then spends twenty minutes debugging branch B. His test data always goes down A. He's debugging code that isn't running, and he doesn't know it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HOCs.&lt;/strong&gt; He sees &lt;code&gt;withAuth(withTheme(Card))&lt;/code&gt; and asks what the component is. There's no single answer — it depends which layer you mean. You can say that clearly. You can't make him walk each layer himself on a whiteboard.&lt;/p&gt;

&lt;p&gt;Here's the useful part: &lt;strong&gt;those three look like three different teaching problems. They're one.&lt;/strong&gt; What's missing isn't three pieces of knowledge. It's one piece of meta-knowledge — the rendered result and the source structure aren't one-to-one, and I need to know which layer I'm looking at.&lt;/p&gt;

&lt;p&gt;What it costs you: you treated them as three topics, built three explanations, spent three lessons. All three explanations were correct. They all understood. But the conversion to "can use it" was low on all three, because the missing piece was the same operation, not three explanations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.map()&lt;/code&gt;: click the second card, the code selects the map expression — because there genuinely is no "second card" in the source — then drill one step further into the template inside it. They watch five things come from one line. Reverse it: cursor on the map expression, all five cards light up at once.&lt;/p&gt;

&lt;p&gt;Conditional rendering: put the cursor on the branch that isn't currently rendering, and the canvas renders that branch on its own. No state changes, no fake test data. That student debugging for twenty minutes figures out what he's doing in two seconds.&lt;/p&gt;

&lt;p&gt;HOCs: climb the layers one at a time, definite answer at each. "What is this component" becomes a question you can answer layer by layer. (&lt;a href="https://blog.crossui.com/2026/09/drill-down-all-the-way" rel="noopener noreferrer"&gt;more on how the layers are defined&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something you can use next week:&lt;/strong&gt; collapse three lessons into one, called "layers." Let them drill up and down first, bump into the concept themselves, then name it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four: they don't know what to be careful with
&lt;/h2&gt;

&lt;p&gt;Students go one of two ways. Either they won't touch anything (afraid of breaking it), or they'll touch anything (no idea what it's connected to). Both make homework hard to grade — one turns in nothing, the other turns in collateral damage.&lt;/p&gt;

&lt;p&gt;Why explanation can't quite reach this: importance isn't a static property. It's a relationship — how many things depend on this. You can accurately say "some files are more critical," and they'll agree, and it won't help them when they open an unfamiliar repo. They need a specific answer about a specific file.&lt;/p&gt;

&lt;p&gt;What they actually need isn't being told a file is important. It's seeing eleven references light up when they change one file. Specific, countable, located. That's when importance stops being an adjective.&lt;/p&gt;

&lt;p&gt;And here's the part I'd care most about if I were teaching: &lt;strong&gt;this instinct decides whether your students can handle an unfamiliar codebase after graduation.&lt;/strong&gt; The confusion in someone's first two weeks is almost never "I don't know React." It's "I don't know where anything lives in this repo." Someone who can trace an import chain and check inbound references can onboard anywhere. Someone who only knows hooks can onboard onto the one repo they were taught on. Which of those you're producing decides how rough their first job is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes it:&lt;/strong&gt; inbound edges give that signal a shape you can point at, and they can practice it on file after file — including the references that arrive through a barrel, which a plain editor search doesn't catch. There's an even better classroom move: &lt;strong&gt;break an edge on purpose.&lt;/strong&gt; Rename an export, watch which part of the graph goes red. Targeted breakage teaches faster than a correct example. (&lt;a href="https://blog.crossui.com/2026/09/what-breaks-if-i-delete-this-file" rel="noopener noreferrer"&gt;what building that graph involves&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Worth mentioning: one of the most stubborn beginner misconceptions is that an import path tells you where the code lives. Jump through three barrels in one hop to the real definition and that misconception dies immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule you can run your syllabus through
&lt;/h2&gt;

&lt;p&gt;The four pains look unrelated — a psychological gate, a visual mapping, structural layers, relational reasoning. Underneath they're the same thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If a concept is fundamentally a relationship or a mapping, explanation gets them to "I understand." The rest of the way to "I can use it" only happens by doing it ten times themselves.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So, a test you can apply to your own outline:&lt;/p&gt;

&lt;p&gt;Can students use this correctly right after understanding it? Yes → factual. Explanation is the whole solution.&lt;/p&gt;

&lt;p&gt;Or do they still need a few tries to get comfortable? → relational. Your explanation already did its job. What's missing is somewhere for them to try.&lt;/p&gt;

&lt;p&gt;Most of the hard parts of React are the second kind. Which is why a genuinely good instructor still ends up feeling like students "know it but can't quite use it." That stretch was never in range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things you could change next week
&lt;/h2&gt;

&lt;p&gt;Make the goal of class one "open a real project without being scared of it." Move environment setup to week five and teach it properly.&lt;/p&gt;

&lt;p&gt;Before defining props, twenty clicks. Name the concept after.&lt;/p&gt;

&lt;p&gt;Merge &lt;code&gt;.map()&lt;/code&gt;, conditional rendering, and HOCs into one lesson called layers. Operate first, name second.&lt;/p&gt;

&lt;p&gt;Before teaching context, have them delete a provider in a real project and see what breaks and where. Then explain why it's needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not replacing your explanation. Adding the part after it.
&lt;/h2&gt;

&lt;p&gt;If you've taught React for a few years and students land in the same four spots every time — understood it, can't quite use it — that isn't a quality problem with your teaching. The box diagram did its job. It got them to "I understand."&lt;/p&gt;

&lt;p&gt;The rest of that distance doesn't need a better diagram. It needs the student to have something they can try ten times themselves.&lt;/p&gt;

&lt;p&gt;The four things above — opening a real project with no setup, clicking between code and canvas, drilling through layers, seeing what depends on what — are all things we built into &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;CrossUI Studio&lt;/a&gt;, mostly for other reasons. Each one links out to its own writeup above if you want the details. Free for classroom use; if you're teaching a cohort and want it set up, get in touch.&lt;/p&gt;

</description>
      <category>react</category>
      <category>bootstrap</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vibe coding made writing cheap. It made reading expensive.</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:20:52 +0000</pubDate>
      <link>https://dev.to/linb/vibe-coding-made-writing-cheap-it-made-reading-expensive-5bc9</link>
      <guid>https://dev.to/linb/vibe-coding-made-writing-cheap-it-made-reading-expensive-5bc9</guid>
      <description>&lt;p&gt;A junior I know shipped a change last month. Cursor wrote it. He ran it. Looked fine. Opened the PR.&lt;/p&gt;

&lt;p&gt;Senior sent it back in five minutes: "this touches three things we don't have tests for."&lt;/p&gt;

&lt;p&gt;He had nothing to say. He didn't write the code. It looked right. He genuinely didn't know what he'd missed — or where you'd even start looking.&lt;/p&gt;

&lt;p&gt;He's not dumb. Nobody ever showed him what sits between "looks right" and "I checked."&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth first
&lt;/h2&gt;

&lt;p&gt;People keep saying AI made writing code free.&lt;/p&gt;

&lt;p&gt;It didn't. Tokens cost real money. Run agents in a loop on a decent-sized repo for a week and go look at your bill. Nobody who's done that thinks generation is free.&lt;/p&gt;

&lt;p&gt;What AI actually did is narrower than the hype. It made &lt;em&gt;producing a plausible diff&lt;/em&gt; fast. It did exactly nothing for the hard part — knowing whether that diff is safe.&lt;/p&gt;

&lt;p&gt;So the cost didn't vanish. It moved. Writing got cheaper. Reading got brutal, because now there's ten times more of it and none of it is yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody teaches reading
&lt;/h2&gt;

&lt;p&gt;Think about how we train people. Syntax. Hooks. Build a project. All production.&lt;/p&gt;

&lt;p&gt;Then they get a job where most of the day is reviewing changes they didn't write, and we act surprised when they can't.&lt;/p&gt;

&lt;p&gt;Most people file "can spot a bad change" under experience. Years of it. Can't teach it, you just earn it.&lt;/p&gt;

&lt;p&gt;I don't buy that. I've watched enough seniors do it to see it's not magic. It's four moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locate.&lt;/strong&gt; Which element on screen does this actually affect? Not roughly. Exactly which JSX node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace.&lt;/strong&gt; Is this a shared definition, or one call site? Shared means everything using it. Call site means just here. This one question decides whether the next move matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius.&lt;/strong&gt; If shared — who uses it? How many? Any references hiding behind a barrel or an &lt;code&gt;export *&lt;/code&gt;, where your editor's search just doesn't look?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it.&lt;/strong&gt; Does it render what you expected? Including the branch that isn't on screen — the other half of that ternary.&lt;/p&gt;

&lt;p&gt;Four moves. All teachable. None taught.&lt;/p&gt;

&lt;h2&gt;
  
  
  "We have tools for this"
&lt;/h2&gt;

&lt;p&gt;Do we? Let's check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; catches type mismatches. But AI's signature move is code that types perfectly and sits in exactly the wrong place. Add a legal optional prop to a shared component. All green. Three of five call sites now render a UI element that shouldn't be there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; only cover what someone already thought about. The danger with an AI change is that it landed on a call site nobody thought about. Which almost certainly has no coverage — that's the state of UI testing everywhere, not one lazy team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storybook, visual regression.&lt;/strong&gt; These tell you something changed. Never why. Alarms after the fact, and only for components someone bothered to write a story for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And then the diff view.&lt;/strong&gt; This is the real problem, and it hides in plain sight.&lt;/p&gt;

&lt;p&gt;A diff shows you text. It shows you nothing about position or reach. Three lines feels safe. Three hundred feels scary. And that feeling has zero connection to what either one actually breaks.&lt;/p&gt;

&lt;p&gt;Sit with that for a second, because it's the whole thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diff size and blast radius are unrelated. Human alarm scales with diff size anyway.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A three-line change can hit eleven files. A three-hundred-line refactor can be perfectly contained. Your gut has it backwards, and vibe coding means you now get to be wrong about this several times a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Fine, better models will fix it"
&lt;/h2&gt;

&lt;p&gt;Probably not. And not for the reason people assume.&lt;/p&gt;

&lt;p&gt;The model sees a window. A few open files, some conversation. From inside that window everything looks isolated. Almost nothing in a real repo is isolated.&lt;/p&gt;

&lt;p&gt;Look at what AI actually gets wrong. Same three patterns, forever:&lt;/p&gt;

&lt;p&gt;Thinks it's editing one instance. Actually editing the shared definition. Five other places quietly change.&lt;/p&gt;

&lt;p&gt;Imports through a path that isn't what it thought, because two barrels sat in between.&lt;/p&gt;

&lt;p&gt;Edits a branch that your case doesn't even go through, so the change never renders at all.&lt;/p&gt;

&lt;p&gt;None of those are syntax problems. They're all &lt;em&gt;where does this line sit in the system&lt;/em&gt; problems. Position. And position is not something a language model has good intuition for. A bigger context window is not a map.&lt;/p&gt;

&lt;p&gt;Someone always replies: so give the model the graph. Fair, and we do. It helps.&lt;/p&gt;

&lt;p&gt;But two things survive. First, shipping a change is a responsibility question, not an information question — when the AI says "safe," someone still has to be able to check that, or you're trusting a system that bears no consequences. Second, the map has to exist before you can hand it to anyone. Build it first. Then argue about who reads it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why good engineers skip the checks anyway
&lt;/h2&gt;

&lt;p&gt;Here's the part that took me a while to see.&lt;/p&gt;

&lt;p&gt;The four moves aren't hard to understand. Everyone nods along. And then they don't do them.&lt;/p&gt;

&lt;p&gt;Not laziness. Cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locating&lt;/strong&gt; means eyeballing a wall of JSX hunting for which part of the UI this touches. Inside a &lt;code&gt;.map()&lt;/code&gt;, inside a conditional, in a component from another file — that hunt is half your review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius&lt;/strong&gt; means three greps, two of which return false hits from a string match, and none of which catch the reference coming in through a re-export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running it&lt;/strong&gt; means switching to a terminal, maybe installing whatever the AI just pulled in, waiting for a build, waiting for a dev server. Two or three minutes to verify a two-second change. So people read it, decide it looks fine, and ship. That's not a discipline failure. That's an entirely rational response to an absurd ratio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer awareness&lt;/strong&gt; means rebuilding a component's whole structure in your head. At 6pm. On the fourth PR of the day.&lt;/p&gt;

&lt;p&gt;Which is why a better PR template doesn't fix this. A template tells people what to check. It does nothing about what checking costs — or whether the answer you get back is even right.&lt;/p&gt;

&lt;h2&gt;
  
  
  So we made the checks cheap
&lt;/h2&gt;

&lt;p&gt;But cost was only half of it. The other half is worse.&lt;/p&gt;

&lt;p&gt;Even if you're willing to spend the half hour, some of these questions your tools cannot answer correctly.&lt;/p&gt;

&lt;p&gt;Run a Find All References on a component in a real template. It returns what it indexed. It does not return the reference that arrived through an &lt;code&gt;export *&lt;/code&gt; in a barrel file — there's no textual match to find, and the indexer didn't resolve the chain. You get a list. The list looks complete. It isn't, and nothing tells you that.&lt;/p&gt;

&lt;p&gt;So when someone does the diligent thing — greps it, checks the callers, decides it's contained — they can be diligent and wrong at the same time. That's the failure mode that actually bothers me, more than the people who skip the check entirely. At least skipping feels like skipping.&lt;/p&gt;

&lt;p&gt;That's the reason CrossUI Studio exists. Not to run the same checks faster. To make a couple of them answerable at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locate is one click.&lt;/strong&gt; Click the thing on screen, land on the exact JSX node that drew it — through the &lt;code&gt;.map()&lt;/code&gt;, through the conditional, through a component defined in a different file. Works backwards too: cursor on a line, and everything it draws lights up on the canvas. Put the cursor inside a &lt;code&gt;.map()&lt;/code&gt; and watch eight cards light up at once. That's the whole "one line, many elements" idea, no explanation needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace is shown, not reconstructed.&lt;/strong&gt; Click again and you climb: this expression, then the component call, then the call site that passed the props, then the file the component actually lives in. "Shared definition or one call site" stops being a thing you work out in your head. It's just on screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius is a resolved graph, not a text search.&lt;/strong&gt; We resolve imports the way a bundler would — through barrels, through path aliases, through &lt;code&gt;export *&lt;/code&gt;. So "who imports this" returns the references a string search structurally can't reach. Fast is a side effect. Complete is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running it is instant.&lt;/strong&gt; No install, no bundler, no dev server. Open a real repo, it renders in the browser. And that branch that isn't on screen? Put your cursor on it and we render it for you. No faking state, no editing test data to see what a VIP sees.&lt;/p&gt;

&lt;p&gt;Two of those make a slow check fast. Two of them make a question answerable that wasn't. The second kind is the part I'd care about if I were choosing a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that looks like in practice
&lt;/h2&gt;

&lt;p&gt;You ask AI for a discount badge on the product card. It adds a conditional inside &lt;code&gt;&amp;lt;ProductCard&amp;gt;&lt;/code&gt;. Clean diff. Runs. No errors.&lt;/p&gt;

&lt;p&gt;Click the badge. You land inside a &lt;code&gt;.map()&lt;/code&gt; — so this isn't one card, it's every card in the list.&lt;/p&gt;

&lt;p&gt;Climb up one layer. &lt;code&gt;&amp;lt;ProductCard&amp;gt;&lt;/code&gt; is a shared definition. This page doesn't own it.&lt;/p&gt;

&lt;p&gt;Check who imports it. Three places: cart, favorites, search results. Search results has no discount field at all.&lt;/p&gt;

&lt;p&gt;Open search results, cursor on the new branch, render it. There it is. Empty badge, alignment broken.&lt;/p&gt;

&lt;p&gt;Under a minute. No build. Without those four moves that diff merges clean and you hear about it from a user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The team version of this problem
&lt;/h2&gt;

&lt;p&gt;One more thing, because individual judgment isn't a process.&lt;/p&gt;

&lt;p&gt;When half your team runs these checks and half doesn't, whether a bad PR gets caught depends on who got assigned. That's worse than a uniformly weak team, because it looks like something is working.&lt;/p&gt;

&lt;p&gt;And it caps how hard you can lean on AI. Without this, you've got two options: throttle AI usage, or accept an incident rate you can't predict. Neither is a strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable version for anyone teaching this
&lt;/h2&gt;

&lt;p&gt;Same problem, one step earlier.&lt;/p&gt;

&lt;p&gt;If a junior's day is mostly reviewing code they didn't write, then "hand-write a TodoList" is training the wrong muscle.&lt;/p&gt;

&lt;p&gt;Better assignment: here's an AI-generated diff. It runs fine. Something in it is wrong. Find the real blast radius and show your work.&lt;/p&gt;

&lt;p&gt;Build the material from the failure patterns above — bait diffs, tiny in lines, huge in reach. Train people out of trusting line count.&lt;/p&gt;

&lt;p&gt;Not saying skip learning to write code. Someone who's never hand-written a &lt;code&gt;.map()&lt;/code&gt; or been burned by conditional rendering can't judge either. Writing is the floor judgment stands on. It just isn't the whole building. (More on the teaching side in a separate post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves us
&lt;/h2&gt;

&lt;p&gt;The bottleneck moved. Writing is cheap now. Reading is expensive — and part of it isn't just expensive, it's guesswork wearing the clothes of diligence.&lt;/p&gt;

&lt;p&gt;That's the piece I'd want closed before leaning harder on AI. Not because review takes too long. Because "I checked" should mean something.&lt;/p&gt;

&lt;p&gt;If you want to try the four moves on something real: &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;CrossUI Studio&lt;/a&gt; opens an actual React repo in the browser, no install. Click something and see how far down it goes.&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>vibecoding</category>
      <category>development</category>
    </item>
    <item>
      <title>Drill down. All the way — even when the real code is five files away</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/linb/drill-down-all-the-way-even-when-the-real-code-is-five-files-away-3o3a</link>
      <guid>https://dev.to/linb/drill-down-all-the-way-even-when-the-real-code-is-five-files-away-3o3a</guid>
      <description>&lt;p&gt;You click a card on the dashboard. Looks like nothing — one component, right there in the file you're already looking at.&lt;/p&gt;

&lt;p&gt;It isn't. It's one of several produced by a &lt;code&gt;.map()&lt;/code&gt;. It's the branch of a conditional that happens to be on screen — the other branch is one state change away, and invisible right now. What actually draws it is a wrapper, with the real component a layer or two underneath. And that real component lives in a different file, reached through a re-export. Four walls between the pixel you clicked and the code that made it: a loop, an invisible branch, a wrapper, a re-export.&lt;/p&gt;

&lt;p&gt;Most tools stop at the first wall. Land you in the wrong place, or just the nearest place, and call it done. That's the actual pain: you click, you get &lt;em&gt;somewhere&lt;/em&gt;, but not &lt;em&gt;there&lt;/em&gt;. You still have to go hunting.&lt;/p&gt;

&lt;p&gt;We built drill-down to keep going. Through the &lt;code&gt;.map()&lt;/code&gt;, through the wrapper, through the re-export, all the way to the file where the thing is actually defined — and back up again, one layer at a time, whenever you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why stopping at the first wall is the default failure mode
&lt;/h2&gt;

&lt;p&gt;The screen you're looking at is a flat tree of pixels. The source that produced it is a nested tree of expressions, conditionals, and loops, spread across however many files someone decided to spread it across. These two trees don't line up file-for-file, wall-for-wall.&lt;/p&gt;

&lt;p&gt;So a click can't just be "find the matching thing." It has to be a path — DOM node, then the JSX expression that drew it, then the component call, then the component's actual definition, then the file that definition lives in, then the call site above that passed the props in. Stopping one layer too early is the single most common way a "click to find it" feature quietly fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four walls that stop a shallow tool cold
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;.map()&lt;/code&gt; wall.&lt;/strong&gt; One line of JSX produces five cards. Click any of them, you land on that one line — we don't pretend to know "which iteration" you clicked, that's runtime data, not source structure, faking it just makes the tool unpredictable. What we do instead: land on the map expression, then let you drill one more level down, into the actual template structure sitting inside it. Two honest stops instead of a guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The invisible-branch wall.&lt;/strong&gt; &lt;code&gt;{isVip ? &amp;lt;GoldBadge /&amp;gt; : &amp;lt;SilverBadge /&amp;gt;}&lt;/code&gt; — only one branch is on screen right now. Click into the one that isn't, and a shallow tool just says "nothing to show." We render that branch on its own instead, like flipping the switch for a second, so you actually see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The re-export wall.&lt;/strong&gt; The component you clicked isn't defined where you are. It's imported, maybe through a barrel, maybe through two. A tool that stops here drops you at the import line and calls it a day. Drilling through means landing on the real definition, without losing track of what props the original call site was passing — otherwise you land in a strange file with no idea how you got there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wrapper wall.&lt;/strong&gt; &lt;code&gt;withAuth(withTheme(Card))&lt;/code&gt;. Click the rendered thing — is "this component" the innermost &lt;code&gt;Card&lt;/code&gt;, or something a wrapper is doing? A shallow tool picks one and hopes. Drilling means you can move through every layer of wrapping, one click at a time, and see what each one is actually responsible for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that keeps the drill-down predictable
&lt;/h2&gt;

&lt;p&gt;One rule, applied every time: land somewhere definite first, then give an explicit way to go deeper or pull back — never a silent guess.&lt;/p&gt;

&lt;p&gt;First click: the smallest meaningful JSX unit closest to what you clicked. Not the outer wrapper, not the raw DOM leaf.&lt;/p&gt;

&lt;p&gt;Click again, or use an explicit gesture, and you move up the chain — component call, then the call site that passed props in, then the file the component actually lives in. This is the same chain behind the breadcrumb trail from our earlier walkthroughs — here we're talking about the rule driving it, not the visual.&lt;/p&gt;

&lt;p&gt;This rule only got solid after running it against a few hundred real templates. It wasn't right on a whiteboard the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drilling the other direction, from code into canvas
&lt;/h2&gt;

&lt;p&gt;Cursor on a &lt;code&gt;.map()&lt;/code&gt; expression: every one of the N rendered results lights up on canvas, not just the first.&lt;/p&gt;

&lt;p&gt;Cursor on a HOC's definition: every rendered instance using that wrapper lights up — could be scattered across totally different screens.&lt;/p&gt;

&lt;p&gt;Cursor on an inactive branch: canvas renders that branch on its own so you can actually see it, instead of a dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nastiest case we could find
&lt;/h2&gt;

&lt;p&gt;shadcn-admin, the left sidebar — &lt;code&gt;components/layout/nav-group.tsx&lt;/code&gt;. You click one nav item. On screen it's just a button.&lt;/p&gt;

&lt;p&gt;It isn't one thing. It's one of N produced by &lt;code&gt;items.map(...)&lt;/code&gt;, and inside that map a three-way branch decides what the item even is: a plain &lt;code&gt;&amp;lt;SidebarMenuLink&amp;gt;&lt;/code&gt; when it has no sub-items, a &lt;code&gt;&amp;lt;SidebarMenuCollapsedDropdown&amp;gt;&lt;/code&gt; when the sidebar is collapsed, or a &lt;code&gt;&amp;lt;SidebarMenuCollapsible&amp;gt;&lt;/code&gt; otherwise — so two of the three are off screen right now, in whatever state you're not currently in. The button you actually see is &lt;code&gt;&amp;lt;SidebarMenuButton asChild&amp;gt;&lt;/code&gt;, a Radix Slot wrapper that merges its props onto its child, and its real definition isn't in this file at all — it's in &lt;code&gt;@/components/ui/sidebar&lt;/code&gt;, behind a barrel export.&lt;/p&gt;

&lt;p&gt;Click it. First stop: the closest JSX layer. Drill up: you're inside the &lt;code&gt;items.map(...)&lt;/code&gt; that generates the whole menu. Flip to the branch that isn't showing — the collapsed-sidebar dropdown — and the canvas renders just that branch on its own, without actually collapsing anything. Peel the &lt;code&gt;asChild&lt;/code&gt; Slot to see what it's merging onto. Jump to the real &lt;code&gt;SidebarMenuButton&lt;/code&gt;: a different file, through the barrel.&lt;/p&gt;

&lt;p&gt;Four walls — a &lt;code&gt;.map()&lt;/code&gt;, an invisible branch, a Slot wrapper, a re-exported definition — one click, and you get through all of them. (Every name above is real: &lt;code&gt;nav-group.tsx&lt;/code&gt;, &lt;code&gt;SidebarMenuLink&lt;/code&gt;/&lt;code&gt;SidebarMenuCollapsible&lt;/code&gt;/&lt;code&gt;SidebarMenuCollapsedDropdown&lt;/code&gt;, and &lt;code&gt;SidebarMenuButton&lt;/code&gt; from &lt;code&gt;@/components/ui/sidebar&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a real pain point, not a nice-to-have
&lt;/h2&gt;

&lt;p&gt;Every "click to inspect" feature that exists eventually hits one of these four walls and just stops — usually silently, so you don't even realize you're in the wrong place until you've wasted five minutes editing the wrong file. That's the actual cost: not "this feature is missing," but "I trusted where it took me and it was wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for teaching React
&lt;/h2&gt;

&lt;p&gt;Put a cursor inside a &lt;code&gt;.map()&lt;/code&gt;, watch eight elements light up at once — that teaches list rendering better than any explanation of the &lt;code&gt;key&lt;/code&gt; prop ever has, and it only works because the tool knows exactly how many things to light up.&lt;/p&gt;

&lt;p&gt;HOCs and wrapped components are one of the hardest things for beginners to reason about — "what even is this component" doesn't have one right answer. Drilling up through the wrapper stack, one layer at a time, and seeing what each one is actually doing, beats defining "higher-order component" on a slide.&lt;/p&gt;

&lt;p&gt;Being able to see both branches of a conditional side by side — what a VIP sees, what everyone else sees — without touching state or faking test data is something a lot of people never get straight even years into writing React.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for reviewing AI's diffs
&lt;/h2&gt;

&lt;p&gt;The common workflow now: AI hands you a diff, you review it. Real problem — if that diff touches one line inside a &lt;code&gt;.map()&lt;/code&gt;, you genuinely can't tell from the text alone whether it changes every item in that list or just one special case. Layer ambiguity is a live blind spot in code review, and it's exactly the kind of thing that slips through when someone's skimming a PR at the end of the day.&lt;/p&gt;

&lt;p&gt;With a real notion of layer, an AI-generated change can be labeled by what it actually touches — a shared component definition (hits every call site) versus a single call site's props (hits just this one). That distinction is the whole ballgame for judging whether a change is safe, and most diff views today just don't carry it.&lt;/p&gt;

&lt;p&gt;Push it further — if an AI agent itself can sense which layer a change lands on, it avoids a common failure mode: thinking it edited one instance when it actually touched the shared definition, quietly breaking five other places that use that component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions for any tool claiming to handle this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Click one of several elements generated by a &lt;code&gt;.map()&lt;/code&gt; on the canvas — does the code correctly select the map expression that generated it? Can you drill one step further into the actual template? What happens with nested maps?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click a component wrapped in a HOC — can you climb up / down, one layer at a time, and see what each wrapper actually does?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Put your cursor on a conditional branch that isn't currently rendering — does the canvas actually render that branch for you, or does it just apologize that there's nothing to show?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>reactjsdevelopment</category>
      <category>ux</category>
      <category>uxdesign</category>
    </item>
    <item>
      <title>No npm install. No dev server. A faster way to iterate on React UIs?</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 24 Aug 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/linb/no-npm-install-no-dev-server-a-faster-way-to-iterate-on-react-uis-173a</link>
      <guid>https://dev.to/linb/no-npm-install-no-dev-server-a-faster-way-to-iterate-on-react-uis-173a</guid>
      <description>&lt;p&gt;You've done this a hundred times. Clone a template. npm install. Wait. Maybe a peer dependency fight. Fix the node version. Install again. Start the dev server. Wait for the first compile. Now you can finally look at the thing.&lt;/p&gt;

&lt;p&gt;Nobody complains about this anymore. We just accept it as the cost of looking at code. But if all you wanted was to glance at a component or tweak one prop, that whole ritual is wildly out of proportion to the task.&lt;/p&gt;

&lt;p&gt;So — why does "look at this code" require an entire toolchain as a prerequisite?&lt;/p&gt;

&lt;h2&gt;
  
  
  That whole ritual was invented, not required
&lt;/h2&gt;

&lt;p&gt;Browsers have been able to run JS forever. Running was never the hard part. Compiling is the hard part — JSX isn't valid JS, TypeScript annotations aren't valid JS, both need translating first.&lt;/p&gt;

&lt;p&gt;The standard answer puts that translation on your machine, in your terminal, through a bundler running on Node, and it hands the browser a finished file afterward. That's a historical accident, not a law of nature. Browser-side compilers just weren't mature when this pattern got set. Node was there first.&lt;/p&gt;

&lt;p&gt;If the compiling itself moves into the browser, the whole chain in between just goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting a compiler in the browser is harder than it sounds
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parsing TSX isn't free.&lt;/strong&gt; A real template is hundreds of files. Full type-checking on the main thread would lock up the UI. You have to decide what's actually needed to render — mostly that means syntax transforms and type erasure, not a full type-check on every keystroke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no filesystem to resolve against.&lt;/strong&gt; Node's module resolution assumes a real disk, a real node_modules, package.json exports fields. In a browser you're simulating all of that in memory — path aliases, workspace protocols, conditional exports, the works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do the packages even come from?&lt;/strong&gt; A template depends on real npm packages — MUI, shadcn, whatever. You can't ask the user to install first. Either resolve real package versions off a CDN, or pre-bundle the common ones — and match whatever's actually pinned in that template's package.json, or the render won't match the real project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some plugins assume they're running in Node.&lt;/strong&gt; Build-time babel plugins, emotion's compile-time optimizations — a fair number of templates lean on these. You either reimplement the effect in the browser or find a runtime equivalent. Neither is free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recompiling everything on every keystroke would be miserable.&lt;/strong&gt; You need module-level caching and invalidation — only the file that changed, and whatever depends on it, gets rebuilt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source maps have to survive the whole trip.&lt;/strong&gt; Errors, breakpoints, and any node-level mapping have to point back to your actual source line, not some transformed mess. That has to hold end to end, not just at one stage of the pipeline.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's just what a real React template looks like once you actually try to run it without Node standing in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we gave up to get here
&lt;/h2&gt;

&lt;p&gt;Being upfront about the tradeoffs, because a piece claiming none is a little suspect.&lt;/p&gt;

&lt;p&gt;Not every build-time plugin gets covered — projects leaning heavily on custom webpack/vite codegen plugins are only partially supported right now. Big monorepos have a real compile cost on first load, incremental after that, but not instant the very first time. And this isn't a replacement for a production build — production still needs a real build tuned for tree-shaking and performance. This is a tool for looking and editing, not a deploy pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the floor the other two pieces stand on
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://blog.crossui.com/2026/08/two-way-is-not-the-same-as-symmetric" rel="noopener noreferrer"&gt;SCD(Symmetric Collaborative Development)&lt;/a&gt; selection needs position info injected at compile time, and a live index kept in sync with it. If compiling happens in some black-box bundler on your machine, there's no place to slot that in. Owning the compiler means owning every step of it, so the index can live inside the pipeline instead of bolted on after.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.crossui.com/2026/08/what-breaks-if-i-delete-this-file" rel="noopener noreferrer"&gt;The dependency graph&lt;/a&gt; needs imports resolved live, as you edit — not from some static analysis script run once, offline, drifting out of sync the moment you touch a file. A browser-side compiler means the graph updates incrementally, with you, instead of "run the script again."&lt;/p&gt;

&lt;p&gt;Neither of the other two pillars gets to be real-time without this one underneath it. It's the least visible of the three, and the one everything else depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like, side by side
&lt;/h2&gt;

&lt;p&gt;Old way: clone, npm install (thirty seconds to several minutes depending on the day, longer if peer deps fight you), sort out the node version, start the dev server, wait for the first compile.&lt;/p&gt;

&lt;p&gt;New way: open the file.&lt;/p&gt;

&lt;p&gt;The gap isn't "faster." It's a different category of thing. One is running a project. The other is opening a file. Your brain never has to switch into "this is an engineering task" mode just to look at a card component.&lt;/p&gt;

&lt;p&gt;We've already shown this holding up on real templates — Material Kit React, shadcn-admin — where "open it and it renders" isn't a demo trick, it's just what happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for teaching React
&lt;/h2&gt;

&lt;p&gt;Environment setup is where most tutorials lose people. A lot of beginners hit an npm install error before they've written a single line of real React, and they walk away — and that has nothing to do with whether they could've learned the material.&lt;/p&gt;

&lt;p&gt;Zero-infrastructure flattens the gap between "open a real template" and "open a CodePen." A student can touch actual production-shaped code in the first session instead of waiting out a whole week of environment setup first.&lt;/p&gt;

&lt;p&gt;For instructors it kills a specific nightmare: half the room stuck on a different Node version, a different OS quirk, a different peer dependency error, each one eating fifteen minutes of class time that had nothing to do with React.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for pairing with AI
&lt;/h2&gt;

&lt;p&gt;The part of AI pairing nobody talks about is the loop after the generation — you get a diff, and now you have to actually run it to see if it's right. The old loop: switch to a terminal, maybe install something new the AI just pulled in, wait for the build, wait for the dev server, then look. Call it two or three minutes per round trip, easily.&lt;/p&gt;

&lt;p&gt;Cut the compile step out and that loop collapses. The AI finishes, you open it, it's already rendered — no waiting stage in between. That turns human-AI back-and-forth into something closer to real-time instead of something metered out in build-times. If the AI keeps getting faster and the human verification step doesn't, all that generation speed just evaporates into the wait.&lt;/p&gt;

&lt;p&gt;There's a second-order effect too — an AI agent itself can look at a render in this environment and self-check (screenshot, check for errors) without spinning up a whole build chain inside its own sandbox first. This isn't just saving a human's coffee break. It's infrastructure for the "AI writes code, then checks its own screenshot" loop that's already how a lot of people work now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions for any tool claiming to skip the build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open a template you've never seen. Do you have to run a single command line command before you see it rendered?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Change one file, one line. Does that mean waiting for a full recompile, or does it just... update?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When something breaks, does the stack trace point at your actual line, or at some scrambled, transformed mess three layers removed from what you wrote?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What breaks if I delete this file? React tooling can't answer that.</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:20:24 +0000</pubDate>
      <link>https://dev.to/linb/what-breaks-if-i-delete-this-file-react-tooling-cant-answer-that-1eom</link>
      <guid>https://dev.to/linb/what-breaks-if-i-delete-this-file-react-tooling-cant-answer-that-1eom</guid>
      <description>&lt;p&gt;You want to delete a file.&lt;/p&gt;

&lt;p&gt;A quick search shows three direct imports. That's easy enough to see.&lt;/p&gt;

&lt;p&gt;The harder question is what those three files are used by.&lt;/p&gt;

&lt;p&gt;One may be a layout dependency. Another may be part of navigation. A third may feed a page that doesn't look related at first glance.&lt;/p&gt;

&lt;p&gt;So the real question isn't "Who imports this file?"&lt;/p&gt;

&lt;p&gt;It's "What breaks if I remove it?"&lt;/p&gt;

&lt;p&gt;Most tools don't actually keep a dependency graph around as something you can ask questions of. They keep a list of files and a pile of text search. That's not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A component tree is not a dependency graph
&lt;/h2&gt;

&lt;p&gt;People mix these up a lot, so worth separating.&lt;/p&gt;

&lt;p&gt;Component tree: who renders who, at runtime. Parent to child. React DevTools shows you this one.&lt;/p&gt;

&lt;p&gt;Dependency graph: who imports who, at compile time. Directed, sometimes has cycles, crosses files and packages freely.&lt;/p&gt;

&lt;p&gt;They're not the same shape. A provider might be a distant ancestor in the component tree, but in the dependency graph it's just one edge coming off main.tsx. You debug with the component tree. But when you're actually changing code, the thing you're navigating is the dependency graph — and that one's never had good tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real graph actually looks like
&lt;/h2&gt;

&lt;p&gt;Here's why building this is annoying, not fun-annoying, actually annoying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Barrel chains.&lt;/strong&gt; &lt;code&gt;@/components&lt;/code&gt; → &lt;code&gt;components/index.ts&lt;/code&gt; → &lt;code&gt;chart/index.ts&lt;/code&gt; → &lt;code&gt;chart-widget.tsx&lt;/code&gt;. Three re-exports before you hit real code. Do you keep those middle hops as nodes in the graph? Keep them and it's noisy. Collapse them and you lose the truth of what's happening. We keep the edges, mark them transitive, and let you collapse the view when you don't care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path aliases.&lt;/strong&gt; &lt;code&gt;@/&lt;/code&gt;, &lt;code&gt;~/&lt;/code&gt;, whatever a monorepo's workspace protocol wants to call itself this week. The resolver has to actually understand the build config to draw the right edge, and the build config itself is sometimes three files stacked on each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;export * ambiguity.&lt;/strong&gt; Same symbol name coming in from two barrels, and which one wins depends on order. The graph has to remember that order or it's just wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic imports.&lt;/strong&gt; &lt;code&gt;React.lazy(() =&amp;gt; import('./Page'))&lt;/code&gt;. Static analysis can see it, but it's an edge that only activates at runtime. Should probably be a dashed line, not a solid one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type-only edges.&lt;/strong&gt; &lt;code&gt;import type&lt;/code&gt; doesn't exist at runtime. Delete that file and your app still runs, but tsc breaks. Two different kinds of edges, same graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cycles.&lt;/strong&gt; Real repos have them constantly, plenty of them harmless. The graph has to represent that, not fall over.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's just Tuesday in a real repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the graph answer questions
&lt;/h2&gt;

&lt;p&gt;The point isn't "look, we drew a graph." The point is what you can ask it:&lt;/p&gt;

&lt;p&gt;Resolve-through — give me the file this import actually lands on, skip every barrel in between, one hop.&lt;/p&gt;

&lt;p&gt;Inbound edges — who references me? If I change this file, what screens does it touch? This is the actual answer to that opening question.&lt;/p&gt;

&lt;p&gt;Reachability from entry — can you get here from main.tsx at all? If not, it's dead code, or only a test imports it.&lt;/p&gt;

&lt;p&gt;Shortest path between two nodes — how does this page even use that util? The path itself is the explanation.&lt;/p&gt;

&lt;p&gt;Cut vertices — which files are chokepoints, where one change ripples everywhere? That's an objective basis for refactor priority, not someone's gut feeling.&lt;/p&gt;

&lt;p&gt;None of this is a visualization trick. It's turning engineering instinct into something you can actually ask and get an answer to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph is never complete, and that's fine
&lt;/h2&gt;

&lt;p&gt;Real repos always have holes — a missing peer dep, an optional import, an alias nobody set up, a file that flat out doesn't exist anymore.&lt;/p&gt;

&lt;p&gt;Two ways to handle that. Throw out the whole graph the second one edge fails to resolve, which is what most tools do. Or mark that edge unresolved and keep building the rest of the graph around it. We do the second. An incomplete graph you can still navigate beats a complete-or-nothing graph every time. (We wrote about the mechanics of that specific move — bypassing a broken import instead of stopping — in an earlier post, if you want the deep dive on that one piece.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Graph plus symmetric editing
&lt;/h2&gt;

&lt;p&gt;This is where the two pieces click together, and it's the part that's genuinely hard to copy.&lt;/p&gt;

&lt;p&gt;Symmetric editing gives you precision inside one node — this thing on screen maps to exactly this AST node. The dependency graph gives you reach across nodes — this AST node's position in the whole repo's topology.&lt;/p&gt;

&lt;p&gt;Put together: click a card on screen, and you don't just land on its JSX. You get its full path back to the entry point — through the route, the view, past three barrels, to the real file — plus who else depends on it. A precise map with no way to zoom out is just a precise island. A zoomed-out map with no precision is just a picture. You need both to actually navigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more now that AI writes half the code, not less
&lt;/h2&gt;

&lt;p&gt;The instinct is that once AI writes your code, you stop needing to understand structure. It's actually the opposite.&lt;/p&gt;

&lt;p&gt;The mistake AI tools make most often is a graph mistake — importing something that doesn't exist, or not noticing a util is already used in five places and changing it breaks all five. The model sees a local window of tokens. It doesn't see the topology.&lt;/p&gt;

&lt;p&gt;The part of the job that's left for a human is exactly "is this change safe on the graph" — inbound edges, reachability, cut vertices. That's precisely what a limited context window can't hold. So a navigable dependency graph isn't a nostalgia tool for people who refuse to use AI. It might be the one navigation skill a human still has to own.&lt;/p&gt;

&lt;p&gt;And the gap in speed is not subtle. Working it out by hand means opening tabs, running greps, manually throwing out the false hits from a string search — that's minutes, and it's still sometimes wrong. A precomputed graph answers the same question, barrels and all, in milliseconds. Minutes versus milliseconds, and the minutes might be a wrong answer too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real walkthrough, with a sting in it
&lt;/h2&gt;

&lt;p&gt;Say you're in Material Kit React and decide that &lt;strong&gt;&lt;code&gt;logo.ts&lt;/code&gt;&lt;/strong&gt; is no longer needed. Maybe the product has moved to a different branding component, or the logo is simply being removed from the application.&lt;/p&gt;

&lt;p&gt;At first, it looks like a trivial deletion.&lt;/p&gt;

&lt;p&gt;You check the graph and find three direct consumers: &lt;strong&gt;&lt;code&gt;layout&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;nav&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;not-found-view&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's already useful. But the interesting part starts when you follow the &lt;strong&gt;upstream edges&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;layout&lt;/code&gt; isn't the end of the story. It is itself used by higher-level application components. Follow that edge and you can see where the layout enters the application structure. &lt;code&gt;nav&lt;/code&gt; leads into another chain of consumers. &lt;code&gt;not-found-view&lt;/code&gt; comes through yet another path, eventually connecting back to the routing layer.&lt;/p&gt;

&lt;p&gt;So instead of seeing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logo.ts
├── layout
├── nav
└── not-found-view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;you can keep walking:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     &amp;lt;higher-level page/router&amp;gt;
                 │
         ┌───────┴───────┐
         │               │
   layout.tsx        nav.tsx
   (import)           (import)
         │               │
         └──────┐ ┌──────┘
                ▼ ▼
              logo.ts
                  ▲
                  │
             (import)
        not-found-view.tsx
                  │
      &amp;lt;higher-level page/router&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important question is no longer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who imports &lt;code&gt;logo.ts&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What parts of the application are sitting above those consumers?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction matters when you're deleting something.&lt;/p&gt;

&lt;p&gt;A direct-reference search can tell you that three files need attention. It doesn't necessarily give you the context needed to understand &lt;strong&gt;where those three files participate in the application&lt;/strong&gt;. You can easily remove the component, clean up the obvious imports, and still miss an affected path higher in the tree.&lt;/p&gt;

&lt;p&gt;And that's where seemingly harmless deletions become expensive.&lt;/p&gt;

&lt;p&gt;A shared component can sit low in the dependency graph while its consumers sit inside completely different application paths. One may affect the main layout. Another may affect navigation. Another may only appear on an error route that nobody happened to test locally.&lt;/p&gt;

&lt;p&gt;Without the graph, the usual workflow is familiar: search references, make the change, run what you normally run, and assume the result is complete. The missing dependency may not surface until a particular route is opened, a rarely used state is rendered, or another developer hits the code path you didn't know existed.&lt;/p&gt;

&lt;p&gt;With the graph, you can inspect those upstream paths &lt;strong&gt;before deleting the file&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can see that &lt;code&gt;logo.ts&lt;/code&gt; is not really an isolated file. It's a small node sitting underneath several higher-level parts of the application. That gives you the information needed to decide whether the deletion is actually safe, which consumers need to be changed, and how far the impact extends.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3DR8kVUC4hA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The file may take five seconds to delete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding what that deletion means is the real job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the value of the graph: not merely finding references, but making the &lt;strong&gt;dependency footprint of a change visible before the change is made&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this means if you're teaching React
&lt;/h2&gt;

&lt;p&gt;Courses teach the component tree. The job is navigating the dependency graph.&lt;/p&gt;

&lt;p&gt;Someone who can trace an import chain can onboard onto any codebase. Someone who only knows hooks can only onboard onto the one they were taught on.&lt;/p&gt;

&lt;p&gt;The difference is simple: &lt;strong&gt;the component tree teaches you how the UI is assembled; the dependency graph teaches you how the codebase is connected.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That matters the moment something stops working.&lt;/p&gt;

&lt;p&gt;A student sees a blank screen. Instead of guessing which component is broken, they can start from the component they were working on and follow its dependency chain. One import leads to another, then another, until the broken dependency becomes visible.&lt;/p&gt;

&lt;p&gt;The graph turns:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Something is broken.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This dependency is breaking this path, which affects these components.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more useful debugging skill.&lt;/p&gt;

&lt;p&gt;It also changes how students learn unfamiliar codebases. They don't have to understand the entire project first. They can start from something they recognize — a page, a component, an import — and navigate outward through the graph. Each edge answers a concrete question: &lt;strong&gt;where does this come from, and what depends on it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is particularly valuable when the application itself can't help you. A broken import can prevent the UI from rendering, but it doesn't erase the source-level dependency graph. You can still trace the chain, locate the failure, and understand what is happening even when the canvas is dead.&lt;/p&gt;

&lt;p&gt;And you can deliberately turn that into a lesson.&lt;/p&gt;

&lt;p&gt;Rename an export in a &lt;strong&gt;"Shadcn Admin"&lt;/strong&gt; template. Break an import. Watch the affected part of the graph change. Then trace the path back to the change that caused it.&lt;/p&gt;

&lt;p&gt;Now students aren't just being shown a correct React example. They're learning how to &lt;strong&gt;investigate a system when their change causes something else to fail&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the real value.&lt;/p&gt;

&lt;p&gt;The goal isn't to teach students another React API. It's to give them a way to enter an unfamiliar codebase, understand its structure, trace problems, and keep working when the obvious surface — the running application — stops telling them what is wrong.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Oesi3dvXHoQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;And barrel traversal made visible, in one click, fixes one of the most persistent beginner misunderstandings there is: that an import path tells you where the code actually lives. It usually doesn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three questions for any tool claiming to handle this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Can it jump through a barrel in one hop and land on the real definition?&lt;/li&gt;
&lt;li&gt;Can it tell you who references a file, including references that came in through a re-export?&lt;/li&gt;
&lt;li&gt;And when an import can't resolve, does it give up on the whole graph, or keep working with everything it can still see?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The ultimate debugging experience:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Can you hover over any file to see its import/export flow in real-time?&lt;/li&gt;
&lt;li&gt;Does it trace a crash back to the exact broken file in the import chain?&lt;/li&gt;
&lt;li&gt;Can you define custom interceptors to bypass failing modules and keep your app rendering?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worth testing on whatever you're using now. We're not going to tell you what it'll find.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this graph looks like on a real template
&lt;/h2&gt;

&lt;p&gt;We ran this against a real, popular template, not a toy repo. Because the full dependency map is massive, &lt;strong&gt;click the image below to download the high-resolution version&lt;/strong&gt; directly.&lt;/p&gt;

&lt;p&gt;Material Kit React dependency graph &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxaucbqvjtzbm9zcnba1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxaucbqvjtzbm9zcnba1t.png" alt=" " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://blog.crossui.com/images/graphs/material-kit-react-full.png" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;blog.crossui.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Shadcn Admin dependency graph&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzq8rvejn5pk0qwtqnqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzq8rvejn5pk0qwtqnqe.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://blog.crossui.com/images/graphs/shadcn-admin-full.png" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;blog.crossui.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>architecture</category>
      <category>react</category>
      <category>softwareengineering</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Two-way is not the same as symmetric</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:02:00 +0000</pubDate>
      <link>https://dev.to/linb/two-way-is-not-the-same-as-symmetric-362l</link>
      <guid>https://dev.to/linb/two-way-is-not-the-same-as-symmetric-362l</guid>
      <description>&lt;p&gt;Every visual editor for React says it's "&lt;strong&gt;two-way.&lt;/strong&gt;" Click something, see the code. Change the code, see the canvas update. Sounds symmetric. It's usually not.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export-once.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime-overlay.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview-only.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "symmetric" actually has to mean
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selection has to go both directions, cleanly.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editing has to go both directions too, with equal power.&lt;/strong&gt; 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."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's one truth, not two.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pieces underneath
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is actually hard to build
&lt;/h2&gt;

&lt;p&gt;If it were easy everyone would have shipped it already. Here's where it gets messy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DOM doesn't remember where it came from.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One line of code, many things on screen.&lt;/strong&gt; A &lt;code&gt;.map()&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some code draws nothing at all, right now.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing on screen might be defined three files away.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Formatting and comments can't get mangled.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency graph is sometimes broken, and that's fine.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;None of these are exotic edge cases. They're just... what a real React app looks like on a random Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  What symmetry buys you
&lt;/h2&gt;

&lt;p&gt;Once selection and editing are actually symmetric, some things stop being separate features and just become normal.&lt;/p&gt;

&lt;p&gt;Point at a card buried inside a &lt;code&gt;.map()&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking one, for real
&lt;/h2&gt;

&lt;p&gt;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: &lt;code&gt;main.tsx&lt;/code&gt; → &lt;code&gt;DashboardPage&lt;/code&gt; → &lt;code&gt;OverviewAnalyticsView&lt;/code&gt; → &lt;code&gt;AnalyticsWidgetSummary&lt;/code&gt;, and that last one is sitting inside a &lt;code&gt;.map()&lt;/code&gt; over a list of stat cards.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   &amp;lt;AnalyticsWidgetSummary
     title="Weekly sales"
     percent={2.6}
&lt;span class="gd"&gt;-    total={714000}
&lt;/span&gt;&lt;span class="gi"&gt;+    total={928000}
&lt;/span&gt;     ...
   /&amp;gt;
   &amp;lt;AnalyticsWidgetSummary
     title="New users"
     percent={-0.1}
     total={1352831}
&lt;span class="gd"&gt;-    color="secondary"
&lt;/span&gt;&lt;span class="gi"&gt;+    color="success"
&lt;/span&gt;     ...
   /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No reformatting. No touched imports. The other cards stay untouched. Real template. Real component. Real source.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/x3Y8Jhz8wCI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters if you're teaching React, not just shipping it
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;.map()&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick test for any tool claiming "two-way"
&lt;/h2&gt;

&lt;p&gt;Three questions, works on anything, not just us:&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>react</category>
      <category>reactjsdevelopment</category>
      <category>cst</category>
      <category>development</category>
    </item>
    <item>
      <title>What if students could edit a real React app before they can even write one?</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/linb/what-if-students-could-edit-a-real-react-app-before-they-can-even-write-one-13n2</link>
      <guid>https://dev.to/linb/what-if-students-could-edit-a-real-react-app-before-they-can-even-write-one-13n2</guid>
      <description>&lt;p&gt;Most React courses teach in the same order. Syntax first. Then hooks. Then a small app. Then, somewhere near the end — if there's time — how a real project is actually organized.&lt;/p&gt;

&lt;p&gt;I understand why. You can't explain a provider tree to someone who doesn't know what a component is. The order makes sense on paper.&lt;/p&gt;

&lt;p&gt;But there's a gap in the middle that nobody really covers, and I think it's the reason so many people finish a course and still feel like they can't read real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading code and understanding code are different things
&lt;/h2&gt;

&lt;p&gt;You can stare at this for an hour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsWidgetSummary&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Weekly sales"&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;714000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"warning"&lt;/span&gt;
  &lt;span class="na"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you'll "understand" it. Title is a string. Total is a number. Color is a string that's probably one of a few allowed values. Chart is an object with a series array.&lt;/p&gt;

&lt;p&gt;Now: what does &lt;code&gt;color="warning"&lt;/code&gt; actually change? Where does "warning" come from? Is it defined in this file? No. In the component? No. It's in a theme object, three directories away, that gets injected through a provider wrapped around the entire app in a file the student has never opened.&lt;/p&gt;

&lt;p&gt;None of that is visible from reading the line. You only learn it by changing the value and watching what happens — or better, by changing the &lt;em&gt;theme&lt;/em&gt; and watching six unrelated components shift at once. That's the moment the concept lands. Not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandboxes solve the setup problem, not the structure problem
&lt;/h2&gt;

&lt;p&gt;The usual answer here is CodeSandbox or StackBlitz, and they're genuinely good tools. Setup friction goes to zero, the student clicks a link and there's running code. Great.&lt;/p&gt;

&lt;p&gt;But look at what's actually in most educational sandboxes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One file, maybe three&lt;/li&gt;
&lt;li&gt;No routing&lt;/li&gt;
&lt;li&gt;No theme provider&lt;/li&gt;
&lt;li&gt;No barrel exports&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;features/&lt;/code&gt; folder, no &lt;code&gt;layouts/&lt;/code&gt;, no shared component library&lt;/li&gt;
&lt;li&gt;State that lives exactly where you'd expect it to live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a curated environment. It's useful for isolating one concept — and that's what it's for — but it's the opposite of what the student will face at their first job. Real apps are five providers deep before anything renders. Components import from &lt;code&gt;@/components&lt;/code&gt; which re-exports from somewhere else which re-exports from somewhere else. Half the props come from a context you can't see in the file you're looking at.&lt;/p&gt;

&lt;p&gt;So we teach on tidy code and then hand people messy code and act surprised when the transition is rough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing link: code and canvas, pointing at each other
&lt;/h2&gt;

&lt;p&gt;There's one more thing sandboxes structurally can't do, and I think it matters more for learning than anything else on this list.&lt;/p&gt;

&lt;p&gt;In a sandbox, the editor and the preview are two separate worlds. You have code on the left, a rendered iframe on the right, and nothing connects them. If a student sees a card on screen and wants to know which JSX made it, they have to guess: search for the text, scroll, open three files, guess again. If they change a line of code, they see &lt;em&gt;something&lt;/em&gt; change on the right, but they have to work out for themselves which change was theirs.&lt;/p&gt;

&lt;p&gt;That guessing step is where beginners lose the thread. It's a translation tax paid on every single interaction.&lt;/p&gt;

&lt;p&gt;Now flip it. Selection is shared in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Click a JSX node in the code, the matching element lights up on the canvas.&lt;/strong&gt; "This is what that line draws." No searching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click an element or prop on the canvas, the exact JSX node or prop gets selected and highlighted in the code.&lt;/strong&gt; “That’s the line that made this — and the property that controls it.” No guessing..&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And editing works both ways too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type in the code, the canvas updates live.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change a prop on the canvas, and the corresponding code updates — the changed prop briefly highlights.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds small. It isn't. What it does is collapse the gap between &lt;em&gt;symbol&lt;/em&gt; and &lt;em&gt;thing&lt;/em&gt;. Beginners spend enormous energy holding an unstable mental map between "this text" and "that rectangle on screen." When the tool holds the map for them, that energy goes back into the actual concept.&lt;/p&gt;

&lt;p&gt;It's the same reason browser devtools were such a leap for learning CSS. Before devtools you edited a stylesheet and reloaded and squinted. After devtools, you clicked the element and &lt;em&gt;saw&lt;/em&gt; which rules applied. Nobody argues devtools made people worse at CSS. It just removed the guessing.&lt;/p&gt;

&lt;p&gt;React never really got that moment. The component tree is a runtime construct, the JSX is source text, and the two have historically lived in separate windows with no line between them. Two-way selection is that line.&lt;/p&gt;

&lt;p&gt;And this genuinely is not something CodeSandbox or StackBlitz can bolt on. They run your code in an iframe — they can host it and hot-reload it, but they don't have a live mapping from the rendered DOM node back to the exact AST node that produced it, especially once that node came out of a &lt;code&gt;.map()&lt;/code&gt;, or a ternary, or a component defined in a different file. That mapping is the hard part, and it's the part that turns "here's your code running" into "here's your code, and here's what each piece of it is."&lt;/p&gt;

&lt;p&gt;For a student, the practical effect is that the feedback loop drops from minutes to zero. Click, see, change, see. Repeat forty times in a session. That's not a nicer UI — that's a different learning rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dependency graph is the thing you're actually teaching
&lt;/h2&gt;

&lt;p&gt;Here's the part I think gets underrated. When a student says "I can't find where this comes from," they're not asking a syntax question. They're asking a &lt;em&gt;graph&lt;/em&gt; question.&lt;/p&gt;

&lt;p&gt;Every React codebase is a dependency graph. Files import files, barrels re-export barrels, a component pulls a hook that pulls a context that's provided six levels up. The rendered UI is just one projection of that graph. Most of the confusion beginners have — "where does this prop come from," "why did changing that file break this screen," "which of these five &lt;code&gt;index.ts&lt;/code&gt; files is the real one" — is them trying to traverse a graph they can't see.&lt;/p&gt;

&lt;p&gt;Courses teach the tree (component hierarchy). They mostly skip the graph (module dependencies). But the graph is what you navigate every day at work.&lt;/p&gt;

&lt;p&gt;So the exercise gets better if you make the graph visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace one import chain end to end.&lt;/strong&gt; &lt;code&gt;AnalyticsWidgetSummary&lt;/code&gt; → &lt;code&gt;@/components/chart&lt;/code&gt; → &lt;code&gt;chart/index.ts&lt;/code&gt; → &lt;code&gt;chart-widget.tsx&lt;/code&gt;. Three re-exports to get to real code. That's not unusual; that's Tuesday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask "what breaks if I delete this file?"&lt;/strong&gt; and then actually look at the answer. Inbound edges are a concept you can feel, not just define.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show a broken edge.&lt;/strong&gt; Rename an export and watch which parts of the graph go red. Beginners learn far more from a targeted break than from a working example.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a practical angle too: real templates often have edges that don't resolve — a missing peer dep, an optional import, a path alias nobody configured. Traditionally that's a hard stop; nothing renders, the student is dead in the water in minute three. It doesn't have to be. If the tool can bypass an unresolved import and keep rendering the rest of the graph, a broken edge becomes a lesson instead of a blocker. (We wrote about the mechanics of that separately, in &lt;a href="https://blog.crossui.com/2026/06/dont-just-find-the-broken-import-bypass-it" rel="noopener noreferrer"&gt;Don't just find the broken import. Bypass it.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;A student who can read a dependency graph can onboard onto any codebase. A student who only knows hooks can onboard onto the one they were taught.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that's usually a diagram
&lt;/h2&gt;

&lt;p&gt;Every React curriculum I've seen has a moment where the instructor draws a box, puts smaller boxes inside it, and says "so the provider wraps everything, and the components underneath can read from it."&lt;/p&gt;

&lt;p&gt;The diagram is correct. It's also inert. It doesn't respond when you poke it. Nobody has ever learned what a provider does by looking at a rectangle.&lt;/p&gt;

&lt;p&gt;What if instead of the diagram, the student opened the actual app — a real, shipped, slightly messy admin template — clicked into a card buried four levels deep inside a &lt;code&gt;.map()&lt;/code&gt; inside a conditional inside a layout component, and changed one prop? And saw both the rendered result and the exact source diff at the same time?&lt;/p&gt;

&lt;p&gt;That's not a replacement for the explanation. It's the thing the explanation is &lt;em&gt;about&lt;/em&gt;, made touchable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like in practice
&lt;/h2&gt;

&lt;p&gt;Concretely, here's the exercise I keep imagining for week one, before the student has written a single line of React themselves:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Open a real template.&lt;/strong&gt; Not a teaching example — something that actually shipped. Material Kit React, shadcn-admin, whatever. Entry file, straight from disk. No &lt;code&gt;npm install&lt;/code&gt;, no dev server, no "wait for it to build."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Find one thing on screen.&lt;/strong&gt; "See that revenue card in the top left? Click it." The corresponding JSX highlights in the code — they didn't search for it, they pointed at it. Then walk back up: &lt;code&gt;main.tsx&lt;/code&gt; → &lt;code&gt;DashboardPage&lt;/code&gt; → &lt;code&gt;OverviewAnalyticsView&lt;/code&gt; → &lt;code&gt;AnalyticsWidgetSummary&lt;/code&gt;. Four hops. The student just learned what a component tree is, by walking one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.5. Go the other direction.&lt;/strong&gt; Put the cursor on a random JSX node in the code and watch the canvas highlight what it draws. Do it on a node inside a &lt;code&gt;.map()&lt;/code&gt; so they see one line of code producing eight things on screen. That single observation explains lists better than any lecture on &lt;code&gt;key&lt;/code&gt; props.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Change one prop — from both sides.&lt;/strong&gt; Edit &lt;code&gt;total={714000}&lt;/code&gt; → &lt;code&gt;total={928000}&lt;/code&gt; in the code, watch the number change. Then change the next card's value on the canvas instead, and watch the one-line diff appear in the source. Same operation, two directions. That's props, and that's also the first time most students really believe the UI &lt;em&gt;is&lt;/em&gt; the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.5. Follow one import out of the file.&lt;/strong&gt; Where does &lt;code&gt;AnalyticsWidgetSummary&lt;/code&gt; actually live? Not the import line — the real file, past the barrels. Two or three hops through the dependency graph. Do it once by hand so they know what the tool is doing for them later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Change something in the theme.&lt;/strong&gt; Watch six other components move. That's context. No diagram required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Break something on purpose.&lt;/strong&gt; Pass a string where a number goes. See what React does. Fix it.&lt;/p&gt;

&lt;p&gt;None of that requires the student to know how to write React yet. It requires them to know how to &lt;em&gt;read&lt;/em&gt; it and &lt;em&gt;poke&lt;/em&gt; it — which is honestly what most junior work is anyway. You're rarely writing a new app from scratch. You're finding the thing in someone else's codebase and changing it without breaking three other things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The objection I keep coming back to
&lt;/h2&gt;

&lt;p&gt;The obvious pushback: isn't this just teaching people to be dependent on a visual tool? If they can't find the file by hand, have they actually learned anything?&lt;/p&gt;

&lt;p&gt;I think that's a fair worry, and it depends entirely on how it's used. If the tool does the navigation &lt;em&gt;for&lt;/em&gt; them and they never look at the path, no, they haven't learned much. If the tool shows them the path — literally, the breadcrumb of components it walked through, and the file each one lives in — then it's closer to training wheels than a crutch. They see the structure repeatedly, in a real codebase, until it stops being mysterious.&lt;/p&gt;

&lt;p&gt;The other objection: real templates are messy, and messy is confusing for beginners. Also fair. But they're going to hit messy eventually, and I'd rather they hit it with an instructor in the room than alone on day three of a new job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm thinking about this at all
&lt;/h2&gt;

&lt;p&gt;I build a tool that does the "open a real React app with no build step and edit it" part — &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;CrossUI Studio&lt;/a&gt;. It wasn't built for education. It was built because I got tired of running &lt;code&gt;npm install&lt;/code&gt; on a template just to see whether the card component was worth using.&lt;/p&gt;

&lt;p&gt;But the education angle keeps coming up. People teaching React tell me the hardest part isn't syntax — it's the gap between "I can write a component" and "I can find my way around a codebase someone else wrote." That gap is not a knowledge problem. You can't lecture it away. It's a familiarity problem, and familiarity comes from time spent poking at real things.&lt;/p&gt;

&lt;p&gt;So the question I actually want to ask: &lt;strong&gt;has anyone teaching React tried starting with a real app instead of a curated one?&lt;/strong&gt; Bootcamps, university courses, even internal onboarding for new hires. Did it work? Did students find it empowering, or just overwhelming? Is there a reason this is a bad idea that I'm not seeing from where I sit?&lt;/p&gt;

&lt;p&gt;Genuinely curious. I'm on the tool side of this, not the teaching side, and I'd rather hear from people who've actually stood in front of a room of beginners.&lt;/p&gt;




&lt;p&gt;If you want to try the mechanic yourself: &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;Guest mode&lt;/a&gt; opens instantly, no account. Point it at a template, click something, and see how far down it goes.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>learning</category>
      <category>react</category>
    </item>
    <item>
      <title>Preview and edit material-kit-react without a build step</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Thu, 23 Jul 2026 18:52:02 +0000</pubDate>
      <link>https://dev.to/linb/preview-and-edit-material-kit-react-without-a-build-step-1hc9</link>
      <guid>https://dev.to/linb/preview-and-edit-material-kit-react-without-a-build-step-1hc9</guid>
      <description>&lt;p&gt;&lt;em&gt;~7 min read · Tutorial&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I look at a lot of MUI admin templates. &lt;code&gt;material-kit-react&lt;/code&gt; from the minimals people is one I keep going back to. Clean, typed, and the folder structure makes sense. Repo: &lt;a href="https://github.com/minimal-ui-kit/material-kit-react" rel="noopener noreferrer"&gt;minimal-ui-kit/material-kit-react&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But every time I just want to &lt;em&gt;see&lt;/em&gt; it, or change one color to check something, it's the same ritual. &lt;code&gt;git clone&lt;/code&gt;, &lt;code&gt;npm install&lt;/code&gt;, wait, &lt;code&gt;npm run dev&lt;/code&gt;, wait more, tab over to localhost. Few minutes gone, a few hundred MB of &lt;code&gt;node_modules&lt;/code&gt; on disk. All that to look at a dashboard.&lt;/p&gt;

&lt;p&gt;So this time I skipped the build. Opened the folder in &lt;a href="https://stuido.crossui.com/app" rel="noopener noreferrer"&gt;CrossUI Studio&lt;/a&gt;, rendered &lt;code&gt;src/main.tsx&lt;/code&gt; directly. No install, no Vite, no localhost. Below is what I did, including the bits that made me stop and think.&lt;/p&gt;

&lt;p&gt;One honest note first. This does not replace your dev server. You still need the real thing for tests, prod builds, actual feature work. It's good for the look-and-tweak loop. Evaluating a template, recoloring something, showing a client. The stuff where booting the whole toolchain costs more than the task itself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Quick note&lt;/strong&gt;: local folder support requires a Pro account. To test it out, use the code in the original blog for a free upgrade. No credit card required, available while it lasts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your browser does not support video. Watch on &lt;a href="https://youtu.be/Oesi3dvXHoQ" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Clone to local disk (don't open it straight from GitHub)
&lt;/h2&gt;

&lt;p&gt;Studio can mount a GitHub repo directly. For a small repo that's the nicest path. For this one I cloned to disk first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/minimal-ui-kit/material-kit-react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is boring. &lt;code&gt;src/&lt;/code&gt; alone is ~130 files, ~245 in the whole project, spread over &lt;code&gt;sections/&lt;/code&gt;, &lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;layouts/&lt;/code&gt;, &lt;code&gt;theme/&lt;/code&gt;, &lt;code&gt;routes/&lt;/code&gt;. Opening a project means the tool has to pull the files it touches. Over the GitHub API, on demand, that's a lot of small requests. It works, just not snappy, and you can hit the rate limit if you poke around. A local folder is only the filesystem, so it's instant. For a template this size, local wins.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;npm install&lt;/code&gt; here. I only cloned the source. The whole point is to not build.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Open the folder — and let it generate a project setting
&lt;/h2&gt;

&lt;p&gt;In Studio: open a &lt;strong&gt;Local Folder&lt;/strong&gt;, pick the cloned &lt;code&gt;material-kit-react&lt;/code&gt; directory. It reads the tree. Nothing installs, nothing runs, files stay on my disk. That last part matters if you don't love uploading a client's codebase somewhere just to look at it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-0.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First time in, Studio sees there's no project setting file and pops a prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This project has no CrossUI Studio setting file. We recommend auto-scanning the project to generate one first, then editing it by hand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-1.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hit &lt;strong&gt;Scan &amp;amp; generate&lt;/strong&gt;. It goes through the project's own config files and writes a setting file at the root. After that, the imports the template uses everywhere just resolve, without Vite in the loop. I didn't have to tell it anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-2.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can hand-edit it after. The Project Settings dialog opens right on the generated file. But the auto one was enough for everything below. Couple of seconds, still zero &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Open &lt;code&gt;src/main.tsx&lt;/code&gt; and hit preview
&lt;/h2&gt;

&lt;p&gt;Close the Project Settings dialog and Studio opens the entry file for you. No need to go hunting in the file tree.&lt;/p&gt;

&lt;p&gt;Here's the file you normally can't "just render":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBrowserRouter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;errorElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;routesSection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StrictMode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RouterProvider&lt;/span&gt; &lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;StrictMode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a Vite entry. &lt;code&gt;createBrowserRouter&lt;/code&gt; + &lt;code&gt;RouterProvider&lt;/code&gt;, and the whole app lives inside &lt;code&gt;&amp;lt;App&amp;gt;&lt;/code&gt; (the theme provider) and &lt;code&gt;routesSection&lt;/code&gt; (the routes). Render this file in isolation the naive way and it blows up. No router context, no theme, no &lt;code&gt;#root&lt;/code&gt; the way the app wants it.&lt;/p&gt;

&lt;p&gt;It rendered anyway. I configured nothing. Opened &lt;code&gt;main.tsx&lt;/code&gt;, hit preview, and the dashboard showed up on the canvas with the MUI theme and all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-21.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-21.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now return to the Design Mode. From what I can tell it picks the providers up from the entry files, so a template that does something weird at bootstrap probably needs you to point it at the right one by hand. For material-kit it just worked, and I'd guess it's because &lt;code&gt;app.tsx&lt;/code&gt; is this plain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AppProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* github fab */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean entry files matter a lot here. More on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Drill down — the providers follow you down (this is the real part)
&lt;/h2&gt;

&lt;p&gt;The dashboard at &lt;code&gt;/&lt;/code&gt; isn't one component. It's a lazy-loaded stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main.tsx  (RouterProvider defined here + the detected ThemeProvider)
  └─ routesSection              (src/routes/sections.tsx)
       └─ DashboardPage          (src/pages/dashboard.tsx, lazy)
            └─ OverviewAnalyticsView   (src/sections/overview/view)
                 └─ AnalyticsWidgetSummary ×4   (src/sections/overview, the stat cards)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-3.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ctrl+click on the canvas walks you DOWN this tree, one file at a time. The nice surprise: &lt;strong&gt;the provider wrapping follows you down by itself.&lt;/strong&gt; Open the Render Decorations panel at any level and you can see the two wrappers listed, &lt;code&gt;RouterProvider&lt;/code&gt; and &lt;code&gt;ThemeProvider&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On &lt;code&gt;main.tsx&lt;/code&gt; it lists both but &lt;em&gt;auto-skips&lt;/em&gt; &lt;code&gt;RouterProvider&lt;/code&gt;. Makes sense, that provider lives in this very file, so wrapping again would double it. It only applies the theme.&lt;/li&gt;
&lt;li&gt;On &lt;code&gt;sections.tsx&lt;/code&gt;, then &lt;code&gt;dashboard.tsx&lt;/code&gt;, then below, both are on (the panel shows "2"). So router + theme context is there the whole way down. I never had to hand-fix a "missing provider" on the way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's usually the painful part of rendering a deep file in isolation, and it was handled for free. What was left on the way to the file I wanted were two small navigation detours. Neither is a crash. They're just what real code looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detour 1 — &lt;code&gt;sections.tsx&lt;/code&gt; opens on the wrong JSX block (&lt;code&gt;renderFallback&lt;/code&gt;).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ctrl+click into the router file and it lands on &lt;code&gt;renderFallback&lt;/code&gt;, the Suspense spinner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/sections.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderFallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt; &lt;span class="na"&gt;sx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1 1 auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alignItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the canvas goes almost empty. One file can hold several JSX blocks and this spinner is just the first one. Up top there's a block navigator (the breadcrumb dropdown) listing them all. I jumped to &lt;code&gt;routesSection&lt;/code&gt; and its children:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routesSection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteObject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="cm"&gt;/* &amp;lt;DashboardLayout&amp;gt;&amp;lt;Suspense&amp;gt;&amp;lt;Outlet/&amp;gt;&amp;lt;/Suspense&amp;gt;&amp;lt;/DashboardLayout&amp;gt; */&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DashboardPage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserPage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductsPage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BlogPage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// sign-in, 404 ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-4.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Picked &lt;code&gt;{ index: true, element: &amp;lt;DashboardPage /&amp;gt; }&lt;/code&gt;, the &lt;code&gt;/&lt;/code&gt; route, and the whole dashboard rendered. Theme already riding along from the auto-wrapper. Finding &lt;code&gt;DashboardPage&lt;/code&gt; next to &lt;code&gt;UserPage&lt;/code&gt; / &lt;code&gt;ProductsPage&lt;/code&gt; took one glance. The routes read like the URL map.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-5.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lesson: when a file has more than one JSX block, don't trust the first thing it shows. Use the block navigator to land on the piece you care about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detour 2 — &lt;code&gt;index.ts&lt;/code&gt; is a barrel, not a component.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;DashboardPage&lt;/code&gt; I kept drilling and hit this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/sections/overview/view/index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./overview-analytics-view.tsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-6.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NO-TARGET-JSX: No target jsx block found in the file&lt;/code&gt;. Which is correct. It's a re-export, there's no JSX to render. &lt;code&gt;sections/&lt;/code&gt; uses these short barrels everywhere so imports stay tidy (&lt;code&gt;from 'src/sections/overview/view'&lt;/code&gt; instead of the full path). Click &lt;code&gt;overview-analytics-view.tsx&lt;/code&gt; in the file list to open the source file. &lt;code&gt;overview-analytics-view.tsx&lt;/code&gt; renders on arrival, theme already applied, and this is the file I actually edit (next section).&lt;/p&gt;

&lt;p&gt;So the two things that made me stop weren't errors. The providers rode down on their own. One was a file with multiple JSX blocks (use the navigator), the other a barrel re-export (click through). Half-second detours, and both say more about how the template is wired than about the tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-7.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(The block navigator above the editor also jumps to any level directly, if clicking through gets old.)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Change something
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;overview-analytics-view.tsx&lt;/code&gt; is where the four stat cards get their props, and it reads easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsWidgetSummary&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Weekly sales"&lt;/span&gt;
  &lt;span class="na"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;2.6&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;714000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// color defaults to primary&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small edits, both from the canvas and the inspector. I changed the "Weekly sales" card's &lt;code&gt;color&lt;/code&gt; to &lt;code&gt;warning&lt;/code&gt; and pushed &lt;code&gt;total&lt;/code&gt; up. Then flipped the "New users" card from &lt;code&gt;secondary&lt;/code&gt; to &lt;code&gt;success&lt;/code&gt;. The diff that lands is exactly that and nothing more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   &amp;lt;AnalyticsWidgetSummary
     title="Weekly sales"
     percent={2.6}
&lt;span class="gd"&gt;-    total={714000}
&lt;/span&gt;&lt;span class="gi"&gt;+    total={928000}
+    color="warning"
&lt;/span&gt;     ...
   /&amp;gt;
   &amp;lt;AnalyticsWidgetSummary
     title="New users"
     percent={-0.1}
     total={1352831}
&lt;span class="gd"&gt;-    color="secondary"
&lt;/span&gt;&lt;span class="gi"&gt;+    color="success"
&lt;/span&gt;     ...
   /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-7.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No reformatting, no touched imports, the other cards byte-identical. This is the bit I care about most in these tools. I want to see it write the edit the way I'd have typed it, not re-emit the whole file from some internal model. Here it's a surgical prop change.&lt;/p&gt;

&lt;p&gt;If you want a bigger change, the theme is the place. &lt;code&gt;src/theme/&lt;/code&gt; is where the palette gets created, and changing the primary color there recolors buttons, nav, active states, everywhere. Same loop, wider blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Back to the whole app — just hit the browser Back button
&lt;/h2&gt;

&lt;p&gt;Small thing I didn't expect to like this much. I didn't reopen &lt;code&gt;main.tsx&lt;/code&gt; from the file tree to get out. Every drill-down step was real navigation, the URL changed each time I stepped in, so the &lt;strong&gt;browser Back button walks straight back out&lt;/strong&gt;. View → page → route → &lt;code&gt;main.tsx&lt;/code&gt;. Forward and back behave like any web app. Back a few times and I'm looking at the full dashboard again with my edit baked in. The "Weekly sales" card now amber inside the real layout, not only in the isolated view where I changed it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Fpreview-edit-tpl-without-build-8.png" alt="CrossUI Studio — visual IDE for React and MUI" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That round trip, edit deep in a view then Back all the way out to see it in the whole app, is normally a reload plus a mental context switch. Here it's the same canvas and the Back button.&lt;/p&gt;

&lt;p&gt;Total time from &lt;code&gt;git clone&lt;/code&gt; to "amber card in the running dashboard": a couple of minutes, most of it the clone. Zero of it &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the friction landed where it did (a note for template authors)
&lt;/h2&gt;

&lt;p&gt;It wasn't quite zero friction. Two small navigation detours on the way down. But both were in predictable places, a multi-block router file and a barrel re-export, and each was a one-click fix. A good part of why it was that predictable is the template itself, not the tool. &lt;code&gt;material-kit-react&lt;/code&gt; is put together in a way that cooperates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The entry (&lt;code&gt;main.tsx&lt;/code&gt; / &lt;code&gt;app.tsx&lt;/code&gt;) is straightforward. Providers are right there, not hidden behind three layers of indirection. That's probably why &lt;code&gt;RouterProvider&lt;/code&gt; and &lt;code&gt;ThemeProvider&lt;/code&gt; got picked up and stayed applied at every level I drilled into. Theme context just rode along, no manual provider fixing on the way down.&lt;/li&gt;
&lt;li&gt;Routes are &lt;code&gt;lazy()&lt;/code&gt; and grouped in &lt;code&gt;routesSection&lt;/code&gt;, so the block navigator reads like the URL structure. Spotting &lt;code&gt;DashboardPage&lt;/code&gt; next to &lt;code&gt;UserPage&lt;/code&gt; / &lt;code&gt;ProductsPage&lt;/code&gt; takes one glance.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sections/&lt;/code&gt; are split per feature behind short barrel &lt;code&gt;index.ts&lt;/code&gt; files. Keeps imports tidy in the real app, and when drilling you just click the one &lt;code&gt;export *&lt;/code&gt; line. A predictable pattern beats a clever one.&lt;/li&gt;
&lt;li&gt;The cards take &lt;strong&gt;plain props&lt;/strong&gt;. &lt;code&gt;AnalyticsWidgetSummary&lt;/code&gt; is just &lt;code&gt;title/total/percent/color/chart&lt;/code&gt;, fed by the view. So editing one from the view is a data change, not a backend call, and the diff stays a single line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A messier template would be harder to preview this way, tool or no tool. Providers assembled dynamically, one 2000-line page, sections that only work when fed real fetched data. So good template structure is a big part of what makes "open it without building it" realistic. Hats off to &lt;strong&gt;material-kit-react&lt;/strong&gt; there, the structure is doing real work.&lt;/p&gt;

&lt;p&gt;The practical upshot, if you make or sell templates. A chunk of the friction for someone &lt;em&gt;evaluating&lt;/em&gt; your template is the clone-install-run tax before they see anything. Being previewable and tweakable straight from source cuts that tax a lot. Worth thinking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not use it for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Running the test suite or the real Vite build. This is preview + edit, not CI.&lt;/li&gt;
&lt;li&gt;Anything that needs live backend data to render at all. You can mock it for a drill-down, but that's a different workflow.&lt;/li&gt;
&lt;li&gt;A final answer on runtime behavior. It renders structure from the source. It's not watching your app actually execute end to end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For "open this template, change a couple of things, see it in context", which is most of what I do with admin kits before committing to one, skipping the build was just less ceremony.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Quick note&lt;/strong&gt;: local folder support requires a Pro account. To test it out, use the code in the original blog for a free upgrade. No credit card required, available while it lasts.&lt;/p&gt;

</description>
      <category>react</category>
      <category>reactjsdevelopment</category>
      <category>mui</category>
      <category>development</category>
    </item>
    <item>
      <title>Editing React components that never rendered</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 13 Jul 2026 14:33:47 +0000</pubDate>
      <link>https://dev.to/linb/editing-react-components-that-never-rendered-3dol</link>
      <guid>https://dev.to/linb/editing-react-components-that-never-rendered-3dol</guid>
      <description>&lt;p&gt;&lt;em&gt;~7 min read · Engineering&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are only two ways a tool can learn the shape of your React code. Which one you pick decides the single most important thing about the tool: whether it's available in the exact moment you need it most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to know a codebase
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Runtime introspection&lt;/strong&gt; watches a running app. React DevTools, profilers, error overlays — they read the live fiber tree after the app mounts. This is the richer of the two: they know real prop values, real state, what actually rendered. But all of that is conditional on one thing — the app has to successfully run first. The moment a render throws, they go dark. Nothing mounted, so there's nothing to inspect. And a module that failed to initialize is invisible to them by definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static AST analysis&lt;/strong&gt; reads source. It parses each file into an abstract syntax tree and reads the structure directly — every JSX node, every import, every prop — without executing anything. It's poorer in one sense: it can't tell you what a variable held at 2:04pm, because nothing ran. But it has a property no runtime tool can match: it works on code that has never once rendered cleanly.&lt;/p&gt;

&lt;p&gt;Most developer tooling is built on the first model. We built CrossUI Studio — a visual editor for React — on the second, on purpose. This post is about why that constraint turned out to be the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment tooling abandons you
&lt;/h2&gt;

&lt;p&gt;Here's the scenario that shaped the decision. A component renders blank. The stack trace points at the component, but the real failure is five imports down — a dynamic import that didn't resolve, a peer dependency that got bumped, a circular reference that's &lt;code&gt;undefined&lt;/code&gt; on first access. The component is innocent; the thing it transitively pulls in is the culprit.&lt;/p&gt;

&lt;p&gt;This is exactly when you most need to understand your code's structure — which file imports what, where the break is, what the surrounding code looks like. And it's exactly when every runtime tool has gone dark, because the app didn't render. DevTools shows nothing. The profiler has no render to profile. The error overlay gives you a line number but not the shape of the code around it. They all need the app to run first, and the entire problem is that it won't.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The one moment you most need to see your code's structure is the one moment a runtime tool refuses to produce it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A static AST tool has no such dependency. It parsed the files when you opened them; it can draw the whole import graph, point at the broken module, and show you the code around it — with the app fully crashed. A broken page isn't a dead end. It's the entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing what you can't run
&lt;/h2&gt;

&lt;p&gt;The same property unlocks something stranger: editing a component that isn't rendering, in a file you never opened.&lt;/p&gt;

&lt;p&gt;Think about what a visual editor normally needs to render a &lt;code&gt;&amp;lt;Card&amp;gt;&lt;/code&gt; that lives inside a &lt;code&gt;.map()&lt;/code&gt;. To put that Card on a canvas, it has to know what &lt;code&gt;item&lt;/code&gt; is — and &lt;code&gt;item&lt;/code&gt; only exists at runtime, inside the loop, when real data flows through. A runtime-based editor simply can't render that Card in isolation; there's no live &lt;code&gt;item&lt;/code&gt; to feed it. So most visual editors quietly refuse to go inside a &lt;code&gt;.map()&lt;/code&gt; at all.&lt;/p&gt;

&lt;p&gt;Working from the AST, the problem reframes. We don't need a running loop; we need the &lt;em&gt;node&lt;/em&gt;. Parse the file, find the JSX inside the map callback, and render that subtree in isolation. The one thing missing is the value of &lt;code&gt;item&lt;/code&gt; — so we ask you for it (a small mock in a panel) rather than requiring the app to produce it. The Card renders, live, with real theming, standing on its own.&lt;/p&gt;

&lt;p&gt;And because the unit of understanding is the AST node rather than the mounted component, drill-down doesn't stop at the file boundary. A child component imported from another file is just another edge in the graph. Follow the import, parse that file, render its node — you're editing a component defined somewhere you never opened, in an app that may never have rendered as a whole.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of the constraint
&lt;/h2&gt;

&lt;p&gt;None of this is free, and pretending otherwise would be dishonest. Static analysis buys unconditional availability by giving up runtime knowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully dynamic imports are unresolvable.&lt;/strong&gt; &lt;code&gt;import('./pages/' + name)&lt;/code&gt; with a runtime-computed path can't be resolved statically. We mark the edge rather than guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We see structure, not values.&lt;/strong&gt; The AST tells you &lt;code&gt;AIPanel&lt;/code&gt; reaches &lt;code&gt;TreeSitter&lt;/code&gt;; it can't tell you what the parser returned on a given render. Mock data stands in for real data; it doesn't replay it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exotic resolution needs config.&lt;/strong&gt; Non-standard bundler aliases or monorepo path magic resolve best when we can read your tsconfig/jsconfig. Without it, some edges fall back to raw specifiers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are real boundaries. But notice none of them touch the core promise: the tool is &lt;em&gt;there&lt;/em&gt; when you're stuck, because it never needed your code to run in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is one idea, not many
&lt;/h2&gt;

&lt;p&gt;The crash-surviving dependency graph, the edit-inside-a-map drill-down, the cross-file navigation, the one-line diffs — from the outside they look like separate features. They're the same decision seen from different angles: &lt;strong&gt;treat parsed source as the source of truth, and make every surface a view over that AST.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The canvas is a view over the AST. The prop inspector is a view over the AST. The dependency graph is a view over the AST across files. None of them depend on a successful render, because none of them are built on the runtime. That's a harder thing to build than hooking the fiber tree — and it's exactly what keeps the tooling alive in the moments the runtime, and everything built on it, goes dark.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;Try it&lt;/a&gt; — open a project, break a deep import on purpose, and watch the structure still render.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About CrossUI Studio&lt;/strong&gt; — A visual IDE for React &amp;amp; MUI. Code and canvas stay in two-way sync on the same AST: edit code and the canvas updates live; click an element on the canvas, edit its props visually, and the code changes with a surgical one-line diff. No build, no localhost — it runs in the browser, works on your real Git repo or a local folder, with no vendor lock-in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;Try it free → studio.crossui.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>development</category>
    </item>
    <item>
      <title>Edit inside a .map(), a ternary, even a component in another file</title>
      <dc:creator>Jack Lee</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:07:34 +0000</pubDate>
      <link>https://dev.to/linb/edit-inside-a-map-a-ternary-even-a-component-in-another-file-2e7</link>
      <guid>https://dev.to/linb/edit-inside-a-map-a-ternary-even-a-component-in-another-file-2e7</guid>
      <description>&lt;p&gt;&lt;em&gt;~7 min read · Tutorial&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the wall almost every visual editor hits. You can edit the outer JSX just fine. But the thing you actually need to change is a &lt;code&gt;Card&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; a &lt;code&gt;.map()&lt;/code&gt;, and the tool goes quiet — because to render that &lt;code&gt;Card&lt;/code&gt;, it needs to know what &lt;code&gt;item&lt;/code&gt; is, and &lt;code&gt;item&lt;/code&gt; only exists at runtime, inside the callback.&lt;/p&gt;

&lt;p&gt;So you drop back to hand-editing. Which is fine, except the whole reason you opened a visual editor was to not do that.&lt;/p&gt;

&lt;p&gt;CrossUI Studio's Layer Drill-down Engine is built for exactly this. You can isolate and edit &lt;em&gt;any&lt;/em&gt; node at &lt;em&gt;any&lt;/em&gt; depth — including the ones that live behind a runtime boundary. And drill-down doesn't stop at the file edge: &lt;strong&gt;Ctrl+click a child component and Studio follows it into the file where it's defined&lt;/strong&gt;, keeping the canvas live the whole way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The depth problem, concretely
&lt;/h2&gt;

&lt;p&gt;Take a list that every React app has some version of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;elevation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;featured&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h6"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Chip&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to visually tweak that &lt;code&gt;Card&lt;/code&gt; — bump the padding, change the &lt;code&gt;Chip&lt;/code&gt; size, adjust the &lt;code&gt;Typography&lt;/code&gt; variant. But a normal visual editor can only see the &lt;code&gt;items.map(...)&lt;/code&gt; expression. It can't render the inside, because &lt;code&gt;item&lt;/code&gt; is undefined until the loop runs. Same story for a ternary (&lt;code&gt;cond ? &amp;lt;A/&amp;gt; : &amp;lt;B/&amp;gt;&lt;/code&gt;) and for &lt;code&gt;children&lt;/code&gt; passed into a slot.&lt;/p&gt;

&lt;p&gt;That runtime boundary is where most tools stop. Studio treats it as just another layer to step into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Drill into the iteration
&lt;/h2&gt;

&lt;p&gt;There are three ways to drill into a node, and they work whether the target is in the current file or in another one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ctrl+click on the canvas or in the component tree.&lt;/strong&gt; Hover a drillable sub-component or expression and an action prompt appears; hold &lt;code&gt;Ctrl&lt;/code&gt; to highlight the target with a mask, then click to step into it. Or just click the corresponding node in the left component tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSX block selector / breadcrumb.&lt;/strong&gt; Use the block selector or breadcrumb above the editor to see the hierarchy and jump to any level — handy when the tree is dense and you'd rather pick than click through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor focus (in-file).&lt;/strong&gt; In the code editor, place your cursor inside a JSX block and Studio promotes it to the canvas focus. Editor and canvas point at the same AST, so moving in one moves the other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drill into the &lt;code&gt;.map()&lt;/code&gt; and Studio renders an &lt;strong&gt;isolated canvas for the inner block&lt;/strong&gt; — one iteration of your list, standing on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-infile.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-infile.png" alt="Ctrl+click the .map() to drill into a single list item" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-infile2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-infile2.png" alt="Ctrl+click the .map() to drill into a single list item" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the shot above, Ctrl+click on the &lt;code&gt;.map()&lt;/code&gt; drills into &lt;code&gt;ProjectBoard['map-0-item']&lt;/code&gt; — the first item of the list, isolated on the canvas while the full &lt;code&gt;items.map(...)&lt;/code&gt; stays put in the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supply the missing context: the Test Data Injector
&lt;/h3&gt;

&lt;p&gt;An isolated list item has a problem: it lost the context its parent gave it. &lt;code&gt;item&lt;/code&gt; is undefined now — there's no loop feeding it. So Studio gives you a &lt;strong&gt;Test Data Injector&lt;/strong&gt;: a panel where you supply temporary mock data (JSON, strings, booleans) for any undefined props or variables at the current level. Fill in &lt;code&gt;item&lt;/code&gt; and the isolated &lt;code&gt;Card&lt;/code&gt; renders realistically, without the full app around it.&lt;/p&gt;

&lt;p&gt;One thing that saves guesswork: &lt;strong&gt;hover any unassigned variable and the code editor highlights its exact location in the source&lt;/strong&gt;, so you can see how &lt;code&gt;item&lt;/code&gt; is used before you decide what to mock.&lt;/p&gt;

&lt;p&gt;Now the &lt;code&gt;Card&lt;/code&gt; is live. Not a screenshot — the actual component, rendered with real MUI theming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Edit the inner node
&lt;/h2&gt;

&lt;p&gt;With the inner block isolated, editing is the normal Studio loop. Click the &lt;code&gt;Chip&lt;/code&gt;, change &lt;code&gt;size&lt;/code&gt; from &lt;code&gt;small&lt;/code&gt; to &lt;code&gt;medium&lt;/code&gt;. Select the &lt;code&gt;Card&lt;/code&gt;, promote its padding to a responsive &lt;code&gt;{ xs: 2, md: 3 }&lt;/code&gt;. Each change writes back into the callback body as a precise AST patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   {items.map((item) =&amp;gt; (
&lt;span class="gd"&gt;-    &amp;lt;Card key={item.id} elevation={item.featured ? 3 : 1}&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+    &amp;lt;Card key={item.id} elevation={item.featured ? 3 : 1} sx={{ p: { xs: 2, md: 3 } }}&amp;gt;
&lt;/span&gt;       &amp;lt;CardContent&amp;gt;
         &amp;lt;Typography variant="h6"&amp;gt;{item.title}&amp;lt;/Typography&amp;gt;
&lt;span class="gd"&gt;-        &amp;lt;Chip label={item.tag} size="small" /&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+        &amp;lt;Chip label={item.tag} size="medium" /&amp;gt;
&lt;/span&gt;       &amp;lt;/CardContent&amp;gt;
     &amp;lt;/Card&amp;gt;
   ))}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.map()&lt;/code&gt; wrapper, the &lt;code&gt;key&lt;/code&gt;, the ternary on &lt;code&gt;elevation&lt;/code&gt; — all untouched. Your mock data never touches the file; it lived only in the drill-down session. What lands in the diff is exactly the two properties you changed.&lt;/p&gt;

&lt;p&gt;The same mechanism works for conditional rendering (drill into the &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; side of a ternary, or a &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; branch, and edit it in isolation), for controlled sub-components that depend on external props or state, and for render-slot &lt;code&gt;children&lt;/code&gt;. Any depth your tree actually has, you can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Cross the file boundary
&lt;/h2&gt;

&lt;p&gt;Real components don't live in one file — a page composes children that are &lt;code&gt;import&lt;/code&gt;ed from all over &lt;code&gt;src/&lt;/code&gt;. A file-bound editor stops when it hits one of those imported children: you have to go find the file yourself, open it, and lose the canvas.&lt;/p&gt;

&lt;p&gt;Studio doesn't stop there. &lt;strong&gt;Ctrl+click a child component and Studio drills straight into the file where it's defined&lt;/strong&gt;, and keeps rendering it live on the canvas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-crossfile.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-crossfile.png" alt="[Ctrl+click a child component to drill into the file where it's defined" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-crossfile2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.crossui.com%2Fimages%2Flayer-drilldown-crossfile2.png" alt="[Ctrl+click a child component to drill into the file where it's defined" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the shot above, the active file is &lt;code&gt;ShadcnDashboard.jsx&lt;/code&gt;. Its component tree lists the children it composes — &lt;code&gt;KpiCard&lt;/code&gt;, &lt;code&gt;RevenueChart&lt;/code&gt;, &lt;code&gt;OrdersTable&lt;/code&gt;, &lt;code&gt;Sidebar&lt;/code&gt;, &lt;code&gt;Header&lt;/code&gt;, and so on. &lt;code&gt;Sidebar&lt;/code&gt; isn't defined in this file; it's &lt;code&gt;import&lt;/code&gt;ed from &lt;code&gt;./components/Sidebar.jsx&lt;/code&gt; (line 16). Ctrl+click it, and Studio follows the import, opens the real definition, and drops you onto its canvas — the dashboard's sidebar, rendered live — with no manual file hunting and no losing your place. From there you can keep drilling: into that file's own children, into a &lt;code&gt;.map()&lt;/code&gt; inside it, into a component &lt;em&gt;it&lt;/em&gt; imports from somewhere else.&lt;/p&gt;

&lt;p&gt;The same three drill-down methods from Step 1 apply here — Ctrl+click and the JSX block selector both cross file boundaries (cursor focus is the one that's in-file only, since it needs the code open in front of you). The effect is that the component tree stops being one-file-at-a-time. You navigate your UI the way it's actually structured — as a graph of components across files — while the canvas stays live at every hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it can do this
&lt;/h2&gt;

&lt;p&gt;Same reason the rest of Studio works the way it does: everything is a view over the AST, not over a running app.&lt;/p&gt;

&lt;p&gt;Drilling into a &lt;code&gt;.map()&lt;/code&gt; is an AST operation — Studio finds the callback body node and renders it with an injected scope. Following a child component across files is &lt;em&gt;also&lt;/em&gt; an AST operation — it resolves the &lt;code&gt;import&lt;/code&gt;, parses the target file, locates the exported component, and renders that node. The runtime never has to succeed for any of this; Studio is reading structure, not observing execution. That's why it can isolate a node five levels deep, in a file you haven't opened, whether or not the full app renders.&lt;/p&gt;

&lt;p&gt;If you want the architecture behind that, the &lt;a href="https://blog.crossui.com/2026/06/bypass-the-broken-import" rel="noopener noreferrer"&gt;dependency graph post&lt;/a&gt; covers how static AST parsing survives things a runtime tool can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The fastest way to feel this is on a real project with real nesting — a dashboard with a list of cards, a page that composes imported sections.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a project (Playground, Git repo, or a local folder).&lt;/li&gt;
&lt;li&gt;Find a &lt;code&gt;.map()&lt;/code&gt; or a ternary, drill in, use the Test Data Injector to mock the missing variable, and edit the inner node.&lt;/li&gt;
&lt;li&gt;Find an imported child component in the tree, &lt;strong&gt;Ctrl+click&lt;/strong&gt; it, and watch Studio follow it into its own file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guest mode, no account: &lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;studio.crossui.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About CrossUI Studio&lt;/strong&gt; — A visual IDE for React &amp;amp; MUI. Code and canvas stay in two-way sync on the same AST: edit code and the canvas updates live; click an element on the canvas, edit its props visually, and the code changes with a surgical one-line diff. No build, no localhost — it runs in the browser, works on your real Git repo or a local folder, with no vendor lock-in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://studio.crossui.com" rel="noopener noreferrer"&gt;Try it free → studio.crossui.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>mui</category>
      <category>development</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
