<?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: Sergey</title>
    <description>The latest articles on DEV Community by Sergey (@bocharov).</description>
    <link>https://dev.to/bocharov</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%2F4100026%2Fcb8b318e-c99e-40d3-afb9-460033e0ea08.png</url>
      <title>DEV Community: Sergey</title>
      <link>https://dev.to/bocharov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bocharov"/>
    <language>en</language>
    <item>
      <title>You Don't Modularise a Frontend by Moving Files</title>
      <dc:creator>Sergey</dc:creator>
      <pubDate>Sat, 29 Aug 2026 10:23:25 +0000</pubDate>
      <link>https://dev.to/bocharov/you-dont-modularise-a-frontend-by-moving-files-c6o</link>
      <guid>https://dev.to/bocharov/you-dont-modularise-a-frontend-by-moving-files-c6o</guid>
      <description>&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%2F37wvs9za2t8hmmo0h7qb.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%2F37wvs9za2t8hmmo0h7qb.png" alt="You Don't Modularise a Frontend by Moving Files" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Notes from trying to give one React component a place it could actually call its own
&lt;/h3&gt;

&lt;p&gt;We wanted to split a large frontend into packages.&lt;/p&gt;

&lt;p&gt;This sounded, at first, like a file-moving problem.&lt;/p&gt;

&lt;p&gt;We had a component. Let's call it &lt;code&gt;WeatherCard&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It showed the current weather, allowed the user to save a location, sent analytics events, behaved slightly differently on the home page and in search results, participated in an experiment, and knew whether the application was running on mobile.&lt;/p&gt;

&lt;p&gt;Nothing particularly exotic.&lt;/p&gt;

&lt;p&gt;It lived here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  feed/
    cards/
      weather/
        WeatherCard.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We wanted it here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
  weather-card/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we moved it.&lt;/p&gt;

&lt;p&gt;That took an afternoon.&lt;/p&gt;

&lt;p&gt;Modularising it took considerably longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The files moved. The application came with them.
&lt;/h2&gt;

&lt;p&gt;The first package looked promising.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
  weather-card/
    WeatherCard.tsx
    WeatherIcon.tsx
    weather.css
    index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It had a &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It had tests.&lt;/p&gt;

&lt;p&gt;It even had a pleasantly small public export.&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="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WeatherCard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./WeatherCard&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;Then I looked at the imports.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSelector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTranslation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useExperiment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../experiments&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAnalytics&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;selectSavedLocations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../store&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;The package was physically separate and architecturally attached to almost everything around it.&lt;/p&gt;

&lt;p&gt;An import is at least honest about this. Package managers can see it. Build tools can see it. Static analysis can complain about it.&lt;/p&gt;

&lt;p&gt;Other dependencies are more discreet.&lt;/p&gt;

&lt;p&gt;Our component appeared to have a wonderfully small API:&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;WeatherCard&lt;/span&gt; &lt;span class="na"&gt;item&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="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;That was not really its API.&lt;/p&gt;

&lt;p&gt;Its inputs looked more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WeatherCard
    │
    ├── item
    ├── Redux store
    ├── router
    ├── translations
    ├── experiments
    ├── analytics
    ├── application type
    └── device configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one happened to be written in the function signature.&lt;/p&gt;

&lt;p&gt;The others arrived through the atmosphere.&lt;/p&gt;

&lt;p&gt;This wasn't an argument against Redux or React Context. Both had been useful. The problem appeared only when we tried to establish a boundary.&lt;/p&gt;

&lt;p&gt;Global context doesn't remove dependencies.&lt;/p&gt;

&lt;p&gt;It makes some of them easier not to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  An adapter helped
&lt;/h2&gt;

&lt;p&gt;We introduced an adapter.&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;function&lt;/span&gt; &lt;span class="nf"&gt;WeatherCardAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rawItem&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedLocations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectSavedLocations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTranslation&lt;/span&gt;&lt;span class="p"&gt;();&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;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useExperiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compact-weather-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analytics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnalytics&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;WeatherCard&lt;/span&gt;
      &lt;span class="na"&gt;rawItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rawItem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;savedLocations&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;savedLocations&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onOpenLocation&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WeatherCard&lt;/code&gt; no longer needed to know where these things came from.&lt;/p&gt;

&lt;p&gt;The application-facing dependencies accumulated in the adapter instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     │
     ▼
WeatherCardAdapter
     │
     ▼
 WeatherCard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was an immediate problem.&lt;/p&gt;

&lt;p&gt;The component interface became ugly.&lt;/p&gt;

&lt;h2&gt;
  
  
  We fixed the ugly interface
&lt;/h2&gt;

&lt;p&gt;The real component looked closer to this:&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;WeatherCard&lt;/span&gt;
  &lt;span class="na"&gt;rawItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rawItem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;savedLocations&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;savedLocations&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;translate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;compact&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onOpenLocation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onOpenLocation&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onSaveLocation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSaveLocation&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isMobile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;pageType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pageType&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;Nine props.&lt;/p&gt;

&lt;p&gt;We had taken a component with a neat API and turned it into this.&lt;/p&gt;

&lt;p&gt;So we did what seemed sensible.&lt;/p&gt;

&lt;p&gt;We introduced &lt;code&gt;WeatherCardContext&lt;/code&gt;.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;WeatherCardContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;savedLocations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SavedLocation&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Translate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;openLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;saveLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sendEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WeatherEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageType&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;The component became tidy again.&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;WeatherCard&lt;/span&gt;
  &lt;span class="na"&gt;rawItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rawItem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;weatherCardContext&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;Much better.&lt;/p&gt;

&lt;p&gt;For perhaps a week, I thought we had found the right abstraction.&lt;/p&gt;

&lt;p&gt;Then another card needed the same translation function.&lt;/p&gt;

&lt;p&gt;And the same navigation behaviour.&lt;/p&gt;

&lt;p&gt;But not the same analytics.&lt;/p&gt;

&lt;p&gt;A third card needed analytics and device information, but had a different interpretation of &lt;code&gt;pageType&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We started constructing context objects.&lt;/p&gt;

&lt;p&gt;Then helpers for constructing context objects.&lt;/p&gt;

&lt;p&gt;Then a common context from which card-specific contexts could be constructed.&lt;/p&gt;

&lt;p&gt;The dependency graph we had been trying to remove was slowly reappearing inside one object.&lt;/p&gt;

&lt;p&gt;Worse, code review had become harder.&lt;/p&gt;

&lt;p&gt;This:&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;WeatherCard&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;context&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;said almost nothing about what changing &lt;code&gt;WeatherCard&lt;/code&gt; might affect.&lt;/p&gt;

&lt;p&gt;We had successfully shortened the interface.&lt;/p&gt;

&lt;p&gt;We had also hidden it again.&lt;/p&gt;

&lt;p&gt;So we deleted &lt;code&gt;WeatherCardContext&lt;/code&gt; and put the ugly props back.&lt;/p&gt;

&lt;p&gt;That felt like going backwards.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;The long interface was information.&lt;/p&gt;

&lt;p&gt;Some props belonged to the weather card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rawItem
savedLocations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some were capabilities supplied by the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;onOpenLocation
onSaveLocation
onEvent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some described presentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And some looked suspicious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;isMobile
pageType
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why did a weather card need to know that it was on mobile?&lt;/p&gt;

&lt;p&gt;The answer was in the rendering code.&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMobile&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CompactTemperature&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;temperature&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;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TemperatureWithDetails&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;temperature&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;It didn't care about mobile.&lt;/p&gt;

&lt;p&gt;It cared about presentation.&lt;/p&gt;

&lt;p&gt;So:&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;isMobile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;became:&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;displayMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tiny change, but a useful one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;isMobile&lt;/code&gt; described the world outside the card.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;displayMode&lt;/code&gt; described the card.&lt;/p&gt;

&lt;p&gt;The ugly interface had shown us where to look.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sometimes an ugly interface is an X-ray.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Then we found the state
&lt;/h2&gt;

&lt;p&gt;Saving a location changed global Redux state.&lt;/p&gt;

&lt;p&gt;That seemed reasonable. The application needed to know which locations were saved.&lt;/p&gt;

&lt;p&gt;The card also stored whether its save animation was running in Redux.&lt;/p&gt;

&lt;p&gt;That seemed less reasonable.&lt;/p&gt;

&lt;p&gt;Then we found whether the tooltip had been dismissed.&lt;/p&gt;

&lt;p&gt;Also Redux.&lt;/p&gt;

&lt;p&gt;Whether the expanded forecast was open.&lt;/p&gt;

&lt;p&gt;Redux.&lt;/p&gt;

&lt;p&gt;Whether the user was currently hovering over the card.&lt;/p&gt;

&lt;p&gt;Thankfully, not Redux.&lt;/p&gt;

&lt;p&gt;We had apparently drawn a boundary somewhere. We just couldn't explain why it was there.&lt;/p&gt;

&lt;p&gt;Asking whether something belonged in Redux wasn't getting us very far, so we changed the question.&lt;/p&gt;

&lt;p&gt;Who owns this state?&lt;/p&gt;

&lt;p&gt;The expanded forecast existed only while the card existed.&lt;/p&gt;

&lt;p&gt;The save animation existed because the card was displaying it.&lt;/p&gt;

&lt;p&gt;The tooltip belonged to that interaction.&lt;/p&gt;

&lt;p&gt;They moved inside.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useWeatherCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;expanded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setExpanded&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;saving&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSaving&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tooltipVisible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTooltipVisible&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saved locations were different.&lt;/p&gt;

&lt;p&gt;Another card could save the same location. Navigation could show the number of saved locations. The information survived the lifetime of any individual &lt;code&gt;WeatherCard&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That stayed outside.&lt;/p&gt;

&lt;p&gt;This gave us a rule we could actually use:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If state needs to be observed outside a boundary, either the observer belongs inside the boundary or the state doesn't.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was a more useful question than choosing between Redux and &lt;code&gt;useState&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application knew what weather looked like
&lt;/h2&gt;

&lt;p&gt;Data was next.&lt;/p&gt;

&lt;p&gt;The API returned something like:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"london"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"units"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"celsius"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"forecast"&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;"alerts"&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;"provider"&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="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;The application parsed this response.&lt;/p&gt;

&lt;p&gt;The feed extracted fields.&lt;/p&gt;

&lt;p&gt;The card received the resulting object.&lt;/p&gt;

&lt;p&gt;This had always worked.&lt;/p&gt;

&lt;p&gt;It also meant that changing the server representation of a weather card could require changes in code that otherwise had nothing to do with weather.&lt;/p&gt;

&lt;p&gt;We tried making the data opaque to the application.&lt;/p&gt;

&lt;p&gt;The feed needed surprisingly little:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FeedItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rawItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;The rest could cross the boundary untouched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server
   │
   ▼
Feed
   │
   │ rawItem
   ▼
WeatherCard
   │
   ▼
Weather parser
   │
   ▼
Weather model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser moved into the package.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WeatherItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;celsius&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;fahrenheit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Forecast&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;This was one of the quieter changes in the migration.&lt;/p&gt;

&lt;p&gt;It removed no visible feature. It probably made no benchmark move.&lt;/p&gt;

&lt;p&gt;What changed was who had to understand the data.&lt;/p&gt;

&lt;p&gt;The feed stopped understanding weather.&lt;/p&gt;

&lt;p&gt;That was useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  A string that wasn't a string
&lt;/h2&gt;

&lt;p&gt;Packages gave us another tool: compilation boundaries.&lt;/p&gt;

&lt;p&gt;Suppose some application code accepted a location identifier.&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LocationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openWeather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LocationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later the contract changed. &lt;code&gt;openWeather&lt;/code&gt; could no longer accept any location identifier. It required the canonical location selected after resolving aliases.&lt;/p&gt;

&lt;p&gt;Unfortunately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LocationId = string
CanonicalLocationId = string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From TypeScript's point of view, nothing had happened.&lt;/p&gt;

&lt;p&gt;The compiler was correct.&lt;/p&gt;

&lt;p&gt;It was proving the wrong thing.&lt;/p&gt;

&lt;p&gt;We wanted consumers to acknowledge the semantic change, so we gave the new identifier a distinct type.&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CanonicalLocationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CanonicalLocationId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly the build failed in several places.&lt;/p&gt;

&lt;p&gt;Good.&lt;/p&gt;

&lt;p&gt;Each error represented a consumer whose assumption needed to be reconsidered.&lt;/p&gt;

&lt;p&gt;The branded type wasn't particularly sophisticated. What interested me was what the failure meant.&lt;/p&gt;

&lt;p&gt;We had turned part of an architectural migration into something the compiler could inspect.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A boundary described in documentation is an intention. A boundary that can fail the build has teeth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One experiment, three teams
&lt;/h2&gt;

&lt;p&gt;The compact layout had started as a single experiment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compact-feed-cards
      │
      ├── WeatherCard
      ├── NewsCard
      └── SportsCard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This had seemed like sensible reuse.&lt;/p&gt;

&lt;p&gt;The three cards were testing roughly the same visual idea, so one flag was simpler than three.&lt;/p&gt;

&lt;p&gt;Then the weather implementation was ready.&lt;/p&gt;

&lt;p&gt;The news implementation was nearly ready.&lt;/p&gt;

&lt;p&gt;Sports had discovered an interaction problem and needed another week.&lt;/p&gt;

&lt;p&gt;Nothing was broken. There was no technical incident. We simply couldn't finish the weather experiment without deciding what to do about sports.&lt;/p&gt;

&lt;p&gt;The options were not attractive.&lt;/p&gt;

&lt;p&gt;We could wait.&lt;/p&gt;

&lt;p&gt;We could add special-case logic around the shared experiment.&lt;/p&gt;

&lt;p&gt;We could coordinate a partial rollout between teams.&lt;/p&gt;

&lt;p&gt;Or we could admit that the experiment wasn't actually one thing.&lt;/p&gt;

&lt;p&gt;We split it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compact-weather-card
compact-news-card
compact-sports-card
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three flags where there had been one.&lt;/p&gt;

&lt;p&gt;Less DRY.&lt;/p&gt;

&lt;p&gt;The weather team shipped.&lt;/p&gt;

&lt;p&gt;A few days later news shipped.&lt;/p&gt;

&lt;p&gt;Sports changed its implementation and shipped after that.&lt;/p&gt;

&lt;p&gt;The interesting dependency had never appeared in the module graph. There was no import from weather to sports. There was no shared component.&lt;/p&gt;

&lt;p&gt;They shared a release decision.&lt;/p&gt;

&lt;p&gt;That was enough to couple them.&lt;/p&gt;

&lt;p&gt;After that I became less interested in counting dependencies and more interested in looking for things that had to change together.&lt;/p&gt;

&lt;p&gt;Sometimes that was an import.&lt;/p&gt;

&lt;p&gt;Sometimes state.&lt;/p&gt;

&lt;p&gt;Sometimes a server format.&lt;/p&gt;

&lt;p&gt;Sometimes an experiment.&lt;/p&gt;

&lt;p&gt;Sometimes a release process.&lt;/p&gt;

&lt;p&gt;They looked different in code.&lt;/p&gt;

&lt;p&gt;Operationally, they behaved rather similarly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture got worse
&lt;/h2&gt;

&lt;p&gt;After the first few migrations, our architecture diagram looked worse than when we started.&lt;/p&gt;

&lt;p&gt;Imagine a feed with sixty card types.&lt;/p&gt;

&lt;p&gt;We had migrated four.&lt;/p&gt;

&lt;p&gt;It now looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feed
 ├── LegacyNewsCard ───── Redux ── Context ── ...
 ├── LegacySportsCard ─── Redux ── Context ── ...
 ├── LegacyFinanceCard ── Redux ── Context ── ...
 │
 ├── WeatherCardAdapter
 │       │
 │       ▼
 │   WeatherCard
 │
 ├── TravelCardAdapter
 │       │
 │       ▼
 │   TravelCard
 │
 └── ...54 more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the migration there had been one architecture.&lt;/p&gt;

&lt;p&gt;Now there were two.&lt;/p&gt;

&lt;p&gt;We had more adapters, more package boundaries and more concepts to explain to somebody joining the project.&lt;/p&gt;

&lt;p&gt;Meanwhile product development continued.&lt;/p&gt;

&lt;p&gt;A legacy card would gain a feature while we were extracting another card. Occasionally somebody would add a new global dependency to a component we intended to migrate next.&lt;/p&gt;

&lt;p&gt;For a while this felt like evidence that the migration was too slow.&lt;/p&gt;

&lt;p&gt;The tempting response was to accelerate it.&lt;/p&gt;

&lt;p&gt;Take ten cards.&lt;/p&gt;

&lt;p&gt;Stop touching them for product work.&lt;/p&gt;

&lt;p&gt;Move them together.&lt;/p&gt;

&lt;p&gt;Clean up the intermediate abstractions afterwards.&lt;/p&gt;

&lt;p&gt;On paper this was much more efficient.&lt;/p&gt;

&lt;p&gt;It also meant that instead of learning whether the boundary worked from one card, we would learn after ten.&lt;/p&gt;

&lt;p&gt;So we kept the migration deliberately uneven.&lt;/p&gt;

&lt;p&gt;Four cards became six.&lt;/p&gt;

&lt;p&gt;Six became nine.&lt;/p&gt;

&lt;p&gt;The remaining cards stayed exactly as awkward as they had been before.&lt;/p&gt;

&lt;p&gt;This had an unexpected benefit.&lt;/p&gt;

&lt;p&gt;The new architecture had to earn its keep while surrounded by the old one.&lt;/p&gt;

&lt;p&gt;If extracting &lt;code&gt;WeatherCard&lt;/code&gt; required changing all sixty cards, the design was not useful yet.&lt;/p&gt;

&lt;p&gt;If the new package required every feed to adopt a new state model simultaneously, the design was not useful yet.&lt;/p&gt;

&lt;p&gt;If a parser could move only after the entire API response was redesigned, it was not useful yet.&lt;/p&gt;

&lt;p&gt;The constraints of incremental migration rejected quite a few elegant ideas for us.&lt;/p&gt;

&lt;p&gt;I am grateful for that now.&lt;/p&gt;

&lt;p&gt;An architecture that works only after the migration is finished has a bootstrapping problem.&lt;/p&gt;

&lt;p&gt;For a large, continuously changing system, the intermediate states are not a brief inconvenience between two architectures.&lt;/p&gt;

&lt;p&gt;They are where you live.&lt;/p&gt;

&lt;p&gt;Our migration would take months.&lt;/p&gt;

&lt;p&gt;For most of that time, the transitional architecture &lt;em&gt;was&lt;/em&gt; the architecture.&lt;/p&gt;

&lt;p&gt;That changed the question we asked about new abstractions.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this get us to the cleanest final design?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More often:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this make the next card cheaper to change without making the remaining fifty-one harder to live with?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That produced smaller steps.&lt;/p&gt;

&lt;p&gt;It also meant we could stop.&lt;/p&gt;

&lt;p&gt;Had priorities changed after twelve cards, we would still have had twelve cards with clearer ownership and forty-eight functioning legacy cards.&lt;/p&gt;

&lt;p&gt;There would have been no half-built platform waiting for the rest of the application to catch up.&lt;/p&gt;

&lt;p&gt;The destination still mattered.&lt;/p&gt;

&lt;p&gt;The ability to stop on the way mattered more than I had expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The card became boring
&lt;/h2&gt;

&lt;p&gt;Eventually &lt;code&gt;WeatherCard&lt;/code&gt; became surprisingly uninteresting.&lt;/p&gt;

&lt;p&gt;It received data.&lt;/p&gt;

&lt;p&gt;It parsed the data it owned.&lt;/p&gt;

&lt;p&gt;It managed its local interactions.&lt;/p&gt;

&lt;p&gt;It asked the application to perform things outside its boundary.&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;WeatherCard&lt;/span&gt;
  &lt;span class="na"&gt;rawItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rawItem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;displayMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"compact"&lt;/span&gt;
  &lt;span class="na"&gt;savedLocations&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;savedLocations&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onSaveLocation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;saveLocation&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onOpenLocation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openLocation&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sendWeatherEvent&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;It didn't know which state library the application used.&lt;/p&gt;

&lt;p&gt;It didn't know which router it used.&lt;/p&gt;

&lt;p&gt;It didn't know where experiments came from.&lt;/p&gt;

&lt;p&gt;It didn't know whether it was rendered in the main application, Storybook or a test harness.&lt;/p&gt;

&lt;p&gt;The adapter knew considerably more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   Application
                        │
       ┌────────────────┼────────────────┐
       │                │                │
     Router          State          Experiments
       │                │                │
       └────────────────┼────────────────┘
                        ▼
                WeatherCardAdapter
                        │
                        ▼
                   WeatherCard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had not eliminated the complexity.&lt;/p&gt;

&lt;p&gt;We had changed where knowledge was allowed to accumulate.&lt;/p&gt;

&lt;p&gt;I think that distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The files were the easy part
&lt;/h2&gt;

&lt;p&gt;Looking back, moving &lt;code&gt;WeatherCard&lt;/code&gt; into &lt;code&gt;packages/weather-card&lt;/code&gt; was one of the least interesting things we did.&lt;/p&gt;

&lt;p&gt;The package became useful only as other changes accumulated around it.&lt;/p&gt;

&lt;p&gt;Dependencies that had been ambient became explicit.&lt;/p&gt;

&lt;p&gt;State acquired owners.&lt;/p&gt;

&lt;p&gt;Data was interpreted closer to the code that understood it.&lt;/p&gt;

&lt;p&gt;Some semantic contracts became compilation failures.&lt;/p&gt;

&lt;p&gt;Release decisions that looked shared turned out not to be.&lt;/p&gt;

&lt;p&gt;And the old application remained operational while all of this happened.&lt;/p&gt;

&lt;p&gt;I started with a fairly mechanical picture of modularity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one large thing
      ↓
several smaller things
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That picture isn't wrong.&lt;/p&gt;

&lt;p&gt;It just doesn't say very much.&lt;/p&gt;

&lt;p&gt;The question I find more useful now is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If this part of the system changes, how much of the rest of the system needs to know?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For &lt;code&gt;WeatherCard&lt;/code&gt;, the answer gradually became: less.&lt;/p&gt;

&lt;p&gt;Not nothing.&lt;/p&gt;

&lt;p&gt;Just less.&lt;/p&gt;

&lt;p&gt;That turned out to be enough.&lt;/p&gt;

</description>
      <category>react</category>
      <category>modularise</category>
      <category>redux</category>
      <category>legacy</category>
    </item>
    <item>
      <title>Teaching an AI to Review Frontend Code</title>
      <dc:creator>Sergey</dc:creator>
      <pubDate>Sat, 29 Aug 2026 09:13:56 +0000</pubDate>
      <link>https://dev.to/bocharov/teaching-an-ai-to-review-frontend-code-1cc8</link>
      <guid>https://dev.to/bocharov/teaching-an-ai-to-review-frontend-code-1cc8</guid>
      <description>&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%2F812iz44dwn3wx5bmaodp.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%2F812iz44dwn3wx5bmaodp.png" alt="Teaching an AI to Review Frontend Code" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have been experimenting with AI-assisted code review for a while.&lt;/p&gt;

&lt;p&gt;The first attempts were not particularly useful.&lt;/p&gt;

&lt;p&gt;They were not &lt;em&gt;wrong&lt;/em&gt;, exactly. The reviews found things: a questionable name here, a function that might be extracted there, an opportunity for &lt;code&gt;useMemo&lt;/code&gt;, perhaps an &lt;code&gt;any&lt;/code&gt; that could be made more precise.&lt;/p&gt;

&lt;p&gt;There was plenty of activity.&lt;/p&gt;

&lt;p&gt;There was rather less reviewing.&lt;/p&gt;

&lt;p&gt;What bothered me was the lack of proportion. A naming suggestion and a possible race condition could appear next to each other as though they deserved roughly the same amount of attention.&lt;/p&gt;

&lt;p&gt;They do not.&lt;/p&gt;

&lt;p&gt;That observation led to a small experiment.&lt;/p&gt;

&lt;p&gt;Instead of asking an AI to &lt;em&gt;find problems in code&lt;/em&gt;, I tried to describe how I wanted a review to proceed.&lt;/p&gt;

&lt;p&gt;I ended up writing a code-review skill.&lt;/p&gt;

&lt;p&gt;This is a story about what went into it, what changed along the way, and what the exercise taught me about code review itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version was a checklist
&lt;/h2&gt;

&lt;p&gt;This seemed sensible.&lt;/p&gt;

&lt;p&gt;Frontend code has plenty of things worth checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript types&lt;/li&gt;
&lt;li&gt;React effects&lt;/li&gt;
&lt;li&gt;accessibility&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;li&gt;performance&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;li&gt;state management&lt;/li&gt;
&lt;li&gt;API contracts&lt;/li&gt;
&lt;li&gt;naming&lt;/li&gt;
&lt;li&gt;duplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wrote them down.&lt;/p&gt;

&lt;p&gt;The result was comprehensive.&lt;/p&gt;

&lt;p&gt;It was also not very good.&lt;/p&gt;

&lt;p&gt;Given enough checklist items, an AI can nearly always find something to say. The difficulty is that software engineering is rarely about finding the largest number of technically defensible observations.&lt;/p&gt;

&lt;p&gt;It is about deciding what matters.&lt;/p&gt;

&lt;p&gt;Consider a small change:&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="nf"&gt;useEffect&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="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setUser&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several things one might say about these four lines.&lt;/p&gt;

&lt;p&gt;But the interesting question, for me, is what happens when &lt;code&gt;userId&lt;/code&gt; changes before the first request completes.&lt;/p&gt;

&lt;p&gt;Request A starts for user 10.&lt;/p&gt;

&lt;p&gt;The component changes.&lt;/p&gt;

&lt;p&gt;Request B starts for user 11.&lt;/p&gt;

&lt;p&gt;B completes first.&lt;/p&gt;

&lt;p&gt;Then A completes.&lt;/p&gt;

&lt;p&gt;The code is still short. The types may still be correct. The linter may be entirely content.&lt;/p&gt;

&lt;p&gt;The UI may now be showing the wrong user.&lt;/p&gt;

&lt;p&gt;That was the first useful shift in the skill:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review the behaviour around the code, not merely the code in the diff.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A review needs an idea of what matters
&lt;/h2&gt;

&lt;p&gt;Once I had noticed this, the checklist began to look like the wrong abstraction.&lt;/p&gt;

&lt;p&gt;The skill needed priorities.&lt;/p&gt;

&lt;p&gt;I eventually wrote this near the beginning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review code as a production risk audit, not a style pass.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence became surprisingly useful.&lt;/p&gt;

&lt;p&gt;It gives the review a direction.&lt;/p&gt;

&lt;p&gt;A resource leak matters before a naming preference.&lt;/p&gt;

&lt;p&gt;A broken permission boundary matters before duplicated code.&lt;/p&gt;

&lt;p&gt;Losing a user's changes matters before whether a helper should be extracted.&lt;/p&gt;

&lt;p&gt;An out-of-order response matters before an unnecessary allocation.&lt;/p&gt;

&lt;p&gt;This does not make maintainability, performance or style irrelevant.&lt;/p&gt;

&lt;p&gt;It simply gives them somewhere to stand.&lt;/p&gt;

&lt;p&gt;The review starts with things that can break user trust or cause the running system to diverge from its intended behaviour: lifecycle leaks, asynchronous races, state integrity, permissions, persistence and other high-impact paths.&lt;/p&gt;

&lt;p&gt;That ordering made the reviews noticeably quieter.&lt;/p&gt;

&lt;p&gt;I considered that progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract before implementation
&lt;/h2&gt;

&lt;p&gt;The next problem was more subtle.&lt;/p&gt;

&lt;p&gt;How can a reviewer decide whether code is correct without first deciding what the code is supposed to do?&lt;/p&gt;

&lt;p&gt;This sounds obvious when written down.&lt;/p&gt;

&lt;p&gt;In practice, I had been skipping it.&lt;/p&gt;

&lt;p&gt;So the first review gate became &lt;strong&gt;Contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The contract is not necessarily written in one place. It may be distributed across the pull-request description, types, tests, public exports, documentation, neighbouring code and the vocabulary of the project.&lt;/p&gt;

&lt;p&gt;Even the word &lt;em&gt;refactor&lt;/em&gt; contains a contract.&lt;/p&gt;

&lt;p&gt;If I say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is only a refactor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am making a fairly strong claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Observable behaviour has not changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Similarly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This improves performance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;contains another claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is a measurable improvement, and the relevant behaviour has been preserved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I started treating those statements as claims rather than descriptions, the review changed.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this implementation good?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the more useful question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What has this change promised, and what would convince me that the promise holds?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I like this framing because it removes quite a lot of personal taste from the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diff is smaller than the change
&lt;/h2&gt;

&lt;p&gt;Another thing I kept running into was the boundary of the pull request.&lt;/p&gt;

&lt;p&gt;A prop changes in one component, but the behavioural change belongs to every consumer.&lt;/p&gt;

&lt;p&gt;A query key changes in one hook, but the consequence belongs to the cache.&lt;/p&gt;

&lt;p&gt;An API field changes in one type, but the actual surface may run through validation, mapping, persistence and several pieces of UI.&lt;/p&gt;

&lt;p&gt;A new option is particularly interesting.&lt;/p&gt;

&lt;p&gt;Adding it is easy.&lt;/p&gt;

&lt;p&gt;Working out what every existing consumer does when it encounters that option is often where the real review begins.&lt;/p&gt;

&lt;p&gt;So I added another gate: &lt;strong&gt;Impacted surface&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The instruction is essentially to follow changed values and behaviour through the system: props, state, API payloads, events, permissions, loading states, errors, cache keys, exports and side effects.&lt;/p&gt;

&lt;p&gt;This is expensive compared with commenting on the changed lines.&lt;/p&gt;

&lt;p&gt;It is also much closer to what I find myself doing when reviewing an important change manually.&lt;/p&gt;

&lt;p&gt;The diff tells me where the edit happened.&lt;/p&gt;

&lt;p&gt;It does not necessarily tell me where the change happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time deserves its own review
&lt;/h2&gt;

&lt;p&gt;Frontend software has another awkward property: much of its behaviour exists across time.&lt;/p&gt;

&lt;p&gt;A component renders, mounts, updates and unmounts.&lt;/p&gt;

&lt;p&gt;A request starts, gets cancelled, retries, resolves or becomes stale.&lt;/p&gt;

&lt;p&gt;A form is initialised, edited, validated, submitted, rejected, reset and perhaps abandoned halfway through.&lt;/p&gt;

&lt;p&gt;Looking at one moment in these processes hides a surprising number of bugs.&lt;/p&gt;

&lt;p&gt;So the skill began to describe lifecycles explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Component
render → hydrate → mount → update → suspend → unmount

Async work
start → cancel → retry → resolve/reject → stale result → cleanup

Form
initialise → edit → dirty → validate → submit → error/success → reset

Data
fetch → normalise → validate → cache → derive → render → mutate → invalidate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not meant as universal state machines.&lt;/p&gt;

&lt;p&gt;They are prompts for attention.&lt;/p&gt;

&lt;p&gt;When an effect changes, I want the review to follow it through unmount.&lt;/p&gt;

&lt;p&gt;When a form changes, I want to know what happens after the server rejects the submission.&lt;/p&gt;

&lt;p&gt;When caching changes, I want to know how stale data eventually becomes fresh again.&lt;/p&gt;

&lt;p&gt;This turned out to be one of the more useful additions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence changed the tone of the review
&lt;/h2&gt;

&lt;p&gt;There was still a problem.&lt;/p&gt;

&lt;p&gt;AI is rather good at producing plausible concerns.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This could cause unnecessary renders.&lt;/p&gt;

&lt;p&gt;There may be a race condition here.&lt;/p&gt;

&lt;p&gt;Consider memoising this value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each sentence sounds reasonable.&lt;/p&gt;

&lt;p&gt;None of them is necessarily true.&lt;/p&gt;

&lt;p&gt;I did not want to solve this by making the model more confident.&lt;/p&gt;

&lt;p&gt;Quite the opposite.&lt;/p&gt;

&lt;p&gt;I wanted uncertainty to become part of the output.&lt;/p&gt;

&lt;p&gt;That led to the &lt;strong&gt;Evidence&lt;/strong&gt; gate.&lt;/p&gt;

&lt;p&gt;A correctness change should have a regression test, or a reason why one is impractical.&lt;/p&gt;

&lt;p&gt;A behaviour-preserving refactor should have evidence that the relevant old behaviour still exists.&lt;/p&gt;

&lt;p&gt;A performance improvement should have a measurement.&lt;/p&gt;

&lt;p&gt;And when the available context cannot establish something, the review should say so.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I cannot establish whether this request is cancelled when the component unmounts. The caller lifecycle or a focused cancellation test would close that gap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I find that much more useful than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Potential memory leak.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The former tells me what is known, what is not known and how to reduce the uncertainty.&lt;/p&gt;

&lt;p&gt;The latter mostly tells me that a memory leak is imaginable.&lt;/p&gt;

&lt;p&gt;Perhaps unsurprisingly, this also made the reviews less argumentative.&lt;/p&gt;

&lt;p&gt;Evidence is easier to discuss than confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactoring needed a different rule
&lt;/h2&gt;

&lt;p&gt;At some point I tried using the same skill for refactoring.&lt;/p&gt;

&lt;p&gt;That exposed another distinction.&lt;/p&gt;

&lt;p&gt;When I ask for a review, I want the system to challenge behaviour.&lt;/p&gt;

&lt;p&gt;When I ask for a refactor, I usually want the opposite: improve the implementation while being extremely conservative about behaviour.&lt;/p&gt;

&lt;p&gt;So refactoring acquired a rather strict constraint:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Zero behaviour change unless explicitly approved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Public APIs, execution order, side effects, error semantics and observable identity all matter here.&lt;/p&gt;

&lt;p&gt;Even a cleaner implementation is not an improvement if it quietly changes one of them.&lt;/p&gt;

&lt;p&gt;This also changed how I think about abstraction.&lt;/p&gt;

&lt;p&gt;Two pieces of code looking similar is not sufficient reason to combine them.&lt;/p&gt;

&lt;p&gt;I now prefer to ask whether they share a contract and the same edge cases.&lt;/p&gt;

&lt;p&gt;If they do, perhaps there is an abstraction.&lt;/p&gt;

&lt;p&gt;If they merely happen to contain similar lines today, duplication may be cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the skill looks like now
&lt;/h2&gt;

&lt;p&gt;After several iterations, the structure became smaller than the original checklist.&lt;/p&gt;

&lt;p&gt;Conceptually, it is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contract
   ↓
Impacted surface
   ↓
Failure &amp;amp; divergence
   ↓
Evidence
   ↓
Lower-priority quality
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are specialised expansions underneath it for effects, forms, caches, permissions, dates, APIs, generated files, dependencies and other areas.&lt;/p&gt;

&lt;p&gt;But those are secondary.&lt;/p&gt;

&lt;p&gt;The sequence is the important part.&lt;/p&gt;

&lt;p&gt;It tells the reviewer where to spend attention before telling it what details it might notice.&lt;/p&gt;

&lt;p&gt;That distinction took me longer to see than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought I was writing instructions for an AI
&lt;/h2&gt;

&lt;p&gt;The slightly unexpected part of this experiment is that I am no longer sure the skill is primarily about AI.&lt;/p&gt;

&lt;p&gt;Writing it forced me to make some of my own review habits explicit.&lt;/p&gt;

&lt;p&gt;Why do I sometimes open five files before commenting on one changed line?&lt;/p&gt;

&lt;p&gt;Because I am tracing the impacted surface.&lt;/p&gt;

&lt;p&gt;Why does an innocent-looking &lt;code&gt;useEffect&lt;/code&gt; make me uncomfortable?&lt;/p&gt;

&lt;p&gt;Usually because I have not yet accounted for its lifecycle.&lt;/p&gt;

&lt;p&gt;Why am I reluctant to approve a refactor even though the new code is obviously cleaner?&lt;/p&gt;

&lt;p&gt;Because &lt;em&gt;cleaner&lt;/em&gt; is visible in the diff.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Behaviour preserving&lt;/em&gt; needs evidence.&lt;/p&gt;

&lt;p&gt;Why do some reviews with fifteen comments feel less useful than reviews with one?&lt;/p&gt;

&lt;p&gt;Because the number of observations is not the same thing as the amount of uncertainty removed.&lt;/p&gt;

&lt;p&gt;I started with a fairly mechanical question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How should an AI review frontend code?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I ended up with a different one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What do I actually do when I believe I am reviewing code well?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The skill is my current answer.&lt;/p&gt;

&lt;p&gt;I expect it to change.&lt;/p&gt;

&lt;p&gt;That may be the most useful property it has.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>codereview</category>
    </item>
  </channel>
</rss>
