<?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: Oleksandr Kostrov</title>
    <description>The latest articles on DEV Community by Oleksandr Kostrov (@oleks_samurai).</description>
    <link>https://dev.to/oleks_samurai</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%2F1912676%2Fd329e3bc-1204-4d42-845b-5ecb7a247ba4.jpg</url>
      <title>DEV Community: Oleksandr Kostrov</title>
      <link>https://dev.to/oleks_samurai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oleks_samurai"/>
    <language>en</language>
    <item>
      <title>DRY is not for UI</title>
      <dc:creator>Oleksandr Kostrov</dc:creator>
      <pubDate>Wed, 17 Jun 2026 19:36:22 +0000</pubDate>
      <link>https://dev.to/oleks_samurai/dry-is-not-for-ui-9eb</link>
      <guid>https://dev.to/oleks_samurai/dry-is-not-for-ui-9eb</guid>
      <description>&lt;p&gt;When we talk about UI, we almost always start by thinking about small components we can reuse across the frontend. And of course we can't reuse &lt;em&gt;every&lt;/em&gt; component. But where exactly does that border of reusability lie — and how do we decide which components are worth reusing and which aren't?&lt;/p&gt;

&lt;p&gt;To answer that, I'll first walk you through a problem we ran into on our product, and then show you how we solved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: one browsing experience, many pages
&lt;/h2&gt;

&lt;p&gt;On our product we show senders a list of products that can become a gift for a recipient, and they can browse that list with a variety of filters. There are two main types of product list: a plain list of products, and products grouped into collections for a specific occasion.&lt;/p&gt;

&lt;p&gt;This list shows up in a lot of places. A sender sees it on the main browsing page; when sending a single gift; when sending a collection of gifts; when automating sends for recurring occasions (birthday, anniversary, and so on); and when building their own collection. That's at least four different pages or modals where the user should get the &lt;em&gt;same&lt;/em&gt; product-browsing experience — search and filters included.&lt;/p&gt;

&lt;p&gt;Sooner or later, though, "the same experience" starts to crack. Design wants to add an element on one flow only. Filters need to behave differently depending on the page. A handler we share across all of them now has to return a different response depending on which request it's serving — same function, different output per page. Each of these little exceptions leaks a boolean flag into our reusable widget, and conditional branching and conditional rendering slowly take over and start defining the component's behavior.&lt;/p&gt;

&lt;p&gt;And then the most painful problem shows up: a change on one page breaks the tests, logic, or UI on a completely different page — one that, from a domain perspective, has nothing to do with it. That is exactly &lt;em&gt;not&lt;/em&gt; how we want to maintain our app.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where's the real boundary?
&lt;/h2&gt;

&lt;p&gt;At the same time, we couldn't get the idea of reusing page layouts across similar flows out of our heads. We wanted both: shared building blocks &lt;em&gt;and&lt;/em&gt; shared layouts, without the cross-page breakage.&lt;/p&gt;

&lt;p&gt;The thing that kept biting us wasn't reuse itself — it was &lt;em&gt;what&lt;/em&gt; we were reusing. Every painful case had the same shape: we were reusing a component that already carried a piece of logic inside it. So the rule we landed on is blunt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't reuse a component that has even one piece of logic baked into it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reusable components should be purely presentational. The components that put them to work — the ones that wire in data, side effects, and page-specific behavior — aren't things to reuse. They're &lt;em&gt;use-cases&lt;/em&gt; of the reusable UI. A page is a use-case. A modal is a use-case. They're allowed to differ, because differing is their whole job.&lt;/p&gt;

&lt;p&gt;The moment you start reusing presentational components that have logic in them, the problems from the previous section begin. That's the border.&lt;/p&gt;

&lt;p&gt;So we split the system in two: a library of pure, reusable UI components, and everything else built on top as use-cases. Here's how we ship that library.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ship our design system the shadcn way (without shipping shadcn)
&lt;/h2&gt;

&lt;p&gt;We borrowed two things from shadcn — and only two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The API shape.&lt;/strong&gt; Our components mirror shadcn/MUI prop conventions (&lt;code&gt;variant&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, CVA variant maps), so they feel familiar to any React developer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The distribution mechanism.&lt;/strong&gt; The shadcn CLI doesn't care whose components it installs — it just reads a JSON file that follows the &lt;a href="https://ui.shadcn.com/schema/registry-item.json" rel="noopener noreferrer"&gt;registry-item schema&lt;/a&gt;. That's a protocol, and anyone can implement it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under the hood, nothing is actually shadcn: our components are built on Base UI (not Radix), styled with Tailwind v4 + CVA against our own OKLCH design tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registry/acmeui/acme-button.tsx     ← component source (Base UI + CVA + tokens)
registry/registry-ui.ts                 ← manifest: name, deps, registryDependencies
        ↓  npm run build:registry
public/r/acme-button.json             ← self-contained installable unit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build script reads the manifest and emits one JSON file per component. The key trick: the entire TSX source is embedded as a string in &lt;code&gt;files[].content&lt;/code&gt;, alongside npm dependencies and &lt;code&gt;registryDependencies&lt;/code&gt; (other registry items, like our utils). The JSON is fully self-contained — it can be served as a static file, fetched from a URL, or read straight off disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing
&lt;/h3&gt;

&lt;p&gt;Our docs site (Next.js) serves &lt;code&gt;public/r/&lt;/code&gt; statically. A consumer app declares the registry once in its &lt;code&gt;components.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"registries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@acmeui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://acmeui.com/r/{name}.json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest add @acmeui/acme-button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI fetches the JSON, recursively resolves &lt;code&gt;registryDependencies&lt;/code&gt;, installs the npm deps, writes the source into the folder mapped by the consumer's &lt;code&gt;aliases.ui&lt;/code&gt;, and rewrites imports to match. A local path works too — handy for testing the artifact before publishing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest add ./apps/acmeui/public/r/acme-button.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  One twist on ownership
&lt;/h3&gt;

&lt;p&gt;Vanilla shadcn says: "copy it, it's yours." We flipped that. Installed files carry a &lt;code&gt;DO NOT MODIFY&lt;/code&gt; banner and are treated as managed artifacts. Changes happen in the design-system repo; consumers re-sync with &lt;code&gt;--overwrite&lt;/code&gt;. You get shadcn's zero-runtime, copy-the-source distribution — with a single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable layouts, not reusable logic
&lt;/h2&gt;

&lt;p&gt;On top of that, our design team can now build full page examples in Storybook using the components straight from the registry. That gives us reusable page layouts with code examples that are easy to copy, paste, and then wire your own logic into.&lt;/p&gt;

&lt;p&gt;Notice what we &lt;em&gt;didn't&lt;/em&gt; do: we didn't ship a "smart" page component with a flag for every flow. We shipped a layout you copy and own. Each page takes the shared presentational skeleton and becomes its own use-case — with its own logic, its own side effects, its own tests. A change in one flow can't reach into another, because there's nothing logical shared between them to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  DRY is good — until it breaks Single Responsibility
&lt;/h2&gt;

&lt;p&gt;So here's what I actually want to emphasize. DRY is a great principle to follow — but only when the components you're reusing aren't breaking another SOLID principle: &lt;strong&gt;S&lt;/strong&gt;, single responsibility. The moment a "reusable" component takes on logic &lt;em&gt;and&lt;/em&gt; presentation, it stops having a single responsibility, and from that point on following DRY becomes a headache for the maintainability, scalability, and predictability of the whole system.&lt;/p&gt;

&lt;p&gt;SOLID pays off when you follow all of it at once. Cherry-pick a couple of letters and ignore their neighbours in the SOLID family, and you get exactly the kind of mess I described above. DRY without S is one of those traps.&lt;/p&gt;

</description>
      <category>react</category>
      <category>frontend</category>
      <category>designsystem</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
