<?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: Adnan Saleem</title>
    <description>The latest articles on DEV Community by Adnan Saleem (@theadnansaleem).</description>
    <link>https://dev.to/theadnansaleem</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%2F4145243%2Fa59add16-1c74-4a70-b09d-d10b8a0facaa.png</url>
      <title>DEV Community: Adnan Saleem</title>
      <link>https://dev.to/theadnansaleem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/theadnansaleem"/>
    <language>en</language>
    <item>
      <title>A React dashboard that keeps up with a WebSocket feed</title>
      <dc:creator>Adnan Saleem</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/theadnansaleem/a-react-dashboard-that-keeps-up-with-a-websocket-feed-o3e</link>
      <guid>https://dev.to/theadnansaleem/a-react-dashboard-that-keeps-up-with-a-websocket-feed-o3e</guid>
      <description>&lt;p&gt;For a vehicle cybersecurity platform I engineered real-time threat detection dashboards in React and TypeScript on WebSocket feeds, which cut analyst time-to-insight by about 20%. A live dashboard has one job that a normal page does not: it must stay responsive while data keeps arriving, for hours. The &lt;a href="https://theadnansaleem.com/lab" rel="noopener noreferrer"&gt;feed demo on this site&lt;/a&gt; shows the same state handling on a simulated feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes wrong first
&lt;/h2&gt;

&lt;p&gt;The obvious version works in a demo and falls over in production:&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;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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="c1"&gt;// One render per message, and a list that grows for as long as the tab is open&lt;/span&gt;
  &lt;span class="nf"&gt;setEvents&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&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;It has two faults. Every message triggers a render, so a burst of fifty messages is fifty renders in a row. And the list never stops growing, so memory and render time climb all day until the tab is reloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render per frame, not per message
&lt;/h2&gt;

&lt;p&gt;The screen updates about sixty times a second, so rendering more often than that is wasted work. Collect incoming messages in a buffer and flush it once per animation frame:&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;flush&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="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;              &lt;span class="c1"&gt;// newest first&lt;/span&gt;
    &lt;span class="nf"&gt;setEvents&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// keep what the screen can show&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// at most one render per frame&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;cancelAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A burst of any size now costs one render. The cleanup function matters as much as the handler: without it, every remount leaves a socket open behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep only what the screen can show
&lt;/h2&gt;

&lt;p&gt;An analyst reads the latest few dozen events, not the last ten thousand. Cap the list (the demo keeps 40) and leave history to the server, which can page through it on request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregate as events arrive
&lt;/h2&gt;

&lt;p&gt;Totals and charts should not be recalculated from the list, because the list is capped and recounting is slow. Update them incrementally:&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="c1"&gt;// Counters grow by one per event; no need to recount the list&lt;/span&gt;
&lt;span class="nf"&gt;setCounts&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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;...&lt;/span&gt;&lt;span class="nx"&gt;prev&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;level&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;prev&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;level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// One bucket per second for the chart: add to the newest, drop the oldest every second&lt;/span&gt;
&lt;span class="nf"&gt;setHistory&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setHistory&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chart then draws thirty numbers, whatever the event rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for the connection dropping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect with backoff.&lt;/strong&gt; Wait one second, then two, then four, up to a ceiling, so a server restart is not met by every client at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the state.&lt;/strong&gt; A dashboard that silently stopped updating is worse than one that says it is reconnecting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch up.&lt;/strong&gt; After reconnecting, ask the server for what was missed, using the last event id you saw.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Give the user control
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pause.&lt;/strong&gt; Nobody can read a row that keeps moving. A pause button holds the view while events keep buffering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Announce politely.&lt;/strong&gt; Mark the event list with &lt;code&gt;role="log"&lt;/code&gt; so screen readers announce new entries without interrupting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make severity more than colour.&lt;/strong&gt; Pair each colour with a word, so the meaning survives for colour-blind users and in print.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to tell it holds
&lt;/h2&gt;

&lt;p&gt;Raise the event rate well past what production sends and leave the page open. Frame rate should stay steady and memory should level off, not climb. If either drifts, something is still growing without a cap.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://theadnansaleem.com/articles/real-time-dashboard-react-websocket" rel="noopener noreferrer"&gt;theadnansaleem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>websocket</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Role-based access control with data redaction in ASP.NET Core</title>
      <dc:creator>Adnan Saleem</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/theadnansaleem/role-based-access-control-with-data-redaction-in-aspnet-core-1o7o</link>
      <guid>https://dev.to/theadnansaleem/role-based-access-control-with-data-redaction-in-aspnet-core-1o7o</guid>
      <description>&lt;p&gt;On a vehicle cybersecurity platform I secured sensitive security data with four-tier role-based access control, secure .NET API integrations, and data redaction pipelines filtered by caller tier. The idea underneath is simple and often skipped: deciding who may call an endpoint is only half of access control. The other half is deciding what each caller gets back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiding a field in the UI is not security
&lt;/h2&gt;

&lt;p&gt;If the API returns a field and the frontend chooses not to show it, the field is one browser tool away from anyone who is logged in. Whatever a user must not see has to be removed on the server, before the response is serialized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions, two mechanisms
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;May this caller use this endpoint at all?&lt;/strong&gt; That is authorization, and ASP.NET Core policies answer it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which parts of the answer may this caller see?&lt;/strong&gt; That is redaction, and it needs its own step.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tiers as policies
&lt;/h2&gt;

&lt;p&gt;Give each user a tier claim at sign-in and express every rule as a named policy. Controllers then state what they need, and the rule itself lives in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs: each policy names the lowest tier that passes&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tier2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireClaim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tier3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireClaim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tier4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireClaim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"4"&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;h2&gt;
  
  
  One redaction step, used everywhere
&lt;/h2&gt;

&lt;p&gt;The endpoint loads the full record and hands it to a single function that builds the response for the caller's tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tier2"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"findings/{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FindingDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindFirstValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;)!);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FindingRedactor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finding&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FindingRedactor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;FindingDto&lt;/span&gt; &lt;span class="nf"&gt;For&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Finding&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// Each sensitive field names the tier that may see it; everyone else gets null&lt;/span&gt;
        &lt;span class="n"&gt;AffectedComponent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AffectedComponent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ReproductionSteps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReproductionSteps&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&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;Three properties make this hold up over time. The domain object never leaves the server; only the response object does. Every sensitive field states its own rule, so a reviewer can read the whole policy in one file. And a new field is absent from the response until someone adds it to the redactor, which makes the default safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where data leaks anyway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lists and search.&lt;/strong&gt; A redacted detail page is pointless if the list endpoint returns the full record. Send every response for that type through the same redactor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exports and reports.&lt;/strong&gt; CSV and PDF generation often bypasses the API layer. Give them the same step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters and counts.&lt;/strong&gt; If a low tier can filter by a hidden field, the result count reveals it. Reject filters on fields the caller cannot read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors and logs.&lt;/strong&gt; A validation message that quotes a hidden value leaks it. Keep sensitive values out of error text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time channels.&lt;/strong&gt; A WebSocket that broadcasts full records to every subscriber undoes all of the above. Redact per connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test it as a table
&lt;/h2&gt;

&lt;p&gt;Access rules are a grid of tiers against fields, so test them as one: for every tier, request the same record and assert exactly which fields are present. When someone adds a field, the test for the lowest tier fails until they decide who may see it. That failing test is the review.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://theadnansaleem.com/articles/role-based-access-control-data-redaction-dotnet" rel="noopener noreferrer"&gt;theadnansaleem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>security</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>Passing Core Web Vitals in a React app: what moves LCP</title>
      <dc:creator>Adnan Saleem</dc:creator>
      <pubDate>Sat, 10 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/theadnansaleem/passing-core-web-vitals-in-a-react-app-what-moves-lcp-47f3</link>
      <guid>https://dev.to/theadnansaleem/passing-core-web-vitals-in-a-react-app-what-moves-lcp-47f3</guid>
      <description>&lt;p&gt;On Qatar's national event platforms I brought core pages to Core Web Vitals compliance through memoization, virtualization, lazy loading and code splitting, which improved LCP by about 35% and TTI by about 30%. On a payments platform, dynamic code splitting cut the initial bundle by about 35%. The method is the same every time, and it starts with measuring, not with optimizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LCP&lt;/strong&gt; (Largest Contentful Paint): when the biggest visible element appears. Good is 2.5 seconds or less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP&lt;/strong&gt; (Interaction to Next Paint): how long the page takes to respond to a tap or key press. Good is 200 milliseconds or less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLS&lt;/strong&gt; (Cumulative Layout Shift): how much the page jumps while loading. Good is 0.1 or less.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google judges these on real visits at the 75th percentile, so a fast laptop on office wifi tells you very little. Test with mobile emulation and a throttled connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the LCP element first
&lt;/h2&gt;

&lt;p&gt;Lighthouse and the Performance panel in Chrome both name the element that counts as the largest paint. Until you know which one it is, every optimization is a guess. It is usually a hero image or a headline, and the question is always the same: what has to download and run before that one element can appear?&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship less before the first paint
&lt;/h2&gt;

&lt;p&gt;Most React apps send every screen to every visitor. Code splitting sends a screen only when it is opened:&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="c1"&gt;// Before: the reports screen ships in the first bundle, to every visitor&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Reports&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;./Reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// After: it is downloaded only when someone opens it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Reports&lt;/span&gt;&lt;span class="dl"&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;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&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;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScreenSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;Reports&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;Suspense&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;Split by route first, then split heavy pieces inside a route: charts, editors, maps, export dialogs. Anything below the fold or behind a click is a candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preload only what the first screen needs
&lt;/h2&gt;

&lt;p&gt;Preloading is a promise that a file is urgent. Break that promise and the urgent files wait behind the others. This site is a small example: every page preloaded seven font files, four of which were used on one page each. Removing those four preloads took the Lighthouse mobile score from between 85 and 88 to 92 and cut the simulated LCP from about 4.1 seconds to 3.4, with no visible change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep interactions cheap
&lt;/h2&gt;

&lt;p&gt;INP suffers when one interaction re-renders too much. Two tools cover most cases. Memoization (&lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;) stops components and calculations from repeating when their inputs did not change. Virtualization keeps long lists and tables down to the rows that are on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop the page from jumping
&lt;/h2&gt;

&lt;p&gt;Layout shift comes from content that arrives without reserved space: images, embeds, late banners, fonts that swap to a different width. Reserve the space up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* The image box has its final size before the image arrives, so nothing jumps */&lt;/span&gt;
&lt;span class="nc"&gt;.hero-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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;h2&gt;
  
  
  Measure before and after, the same way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run the same test three times before a change and three times after. Single runs vary too much to trust.&lt;/li&gt;
&lt;li&gt;Change one thing at a time, so you know what worked. I tried deferring hidden images on this site and it measured no gain, so it was dropped.&lt;/li&gt;
&lt;li&gt;Lab tools estimate. Real-user data, from the Chrome UX Report or your own analytics, is what Google uses, so confirm there once traffic allows.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://theadnansaleem.com/articles/core-web-vitals-react-lcp-code-splitting" rel="noopener noreferrer"&gt;theadnansaleem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>webperf</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rendering 20,000 rows in React without freezing the page</title>
      <dc:creator>Adnan Saleem</dc:creator>
      <pubDate>Wed, 07 Oct 2026 10:56:49 +0000</pubDate>
      <link>https://dev.to/theadnansaleem/rendering-20000-rows-in-react-without-freezing-the-page-27kn</link>
      <guid>https://dev.to/theadnansaleem/rendering-20000-rows-in-react-without-freezing-the-page-27kn</guid>
      <description>&lt;p&gt;On a multi-currency payments platform I optimized large data tables with react-window virtualization and memoization, which cut render time by about 70% on datasets over 20,000 rows. The technique is small enough to write by hand, and writing it once makes the libraries easy to reason about. The code below is the same code that runs in the &lt;a href="https://theadnansaleem.com/lab" rel="noopener noreferrer"&gt;live demo on this site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a big table is slow
&lt;/h2&gt;

&lt;p&gt;The data is rarely the problem. Twenty thousand objects in memory is nothing. The cost is the DOM: every row is several elements that the browser has to create, style, lay out and paint, and React has to reconcile all of them on every update. A filter keystroke that touches 20,000 rows means 20,000 rows of work, even though the user can see about twelve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render only what is visible
&lt;/h2&gt;

&lt;p&gt;Virtualization, also called windowing, keeps the full list in memory and puts only the visible slice in the DOM. With fixed-height rows it is arithmetic on the scroll position:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// row height in px&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VIEW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;432&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// height of the scroll area in px&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OVERSCAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// extra rows above and below, so fast scrolling shows no gaps&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;OVERSCAN&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;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;VIEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;OVERSCAN&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;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then give the browser an inner element as tall as the whole list, and shift the visible slice down to where it belongs:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scroller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VIEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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;spacer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relative&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;slice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`translateY(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;scroller&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onScroll&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setScrollTop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&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;/* Full height, so the scrollbar behaves as if every row were there */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;spacer&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slice&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="nx"&gt;visible&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;row&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&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;row&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;row&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&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="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;div&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;In the demo that turns 20,000 rows of data into roughly two dozen rows in the DOM, whatever the scroll position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where memoization fits
&lt;/h2&gt;

&lt;p&gt;Windowing fixes how many rows exist. Memoization fixes how often the survivors re-render. Two places matter:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&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;Row&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;row&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"row"&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="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The row.&lt;/strong&gt; Wrapped in &lt;code&gt;React.memo&lt;/code&gt; with a stable &lt;code&gt;key&lt;/code&gt;, a row that scrolls but does not change is not rendered again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The derived list.&lt;/strong&gt; Filtering and sorting 20,000 items on every scroll event is wasted work. &lt;code&gt;useMemo&lt;/code&gt; recomputes only when the inputs change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Memoization only helps if props are stable. An inline object or arrow function passed to every row defeats it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure it
&lt;/h2&gt;

&lt;p&gt;React finishing is not the same as the user seeing the result. To time an update through to paint, wait two animation frames after the state change:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ms to paint&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;Record the number with virtualization off, then on, with the same data and the same action. That before and after is the only claim worth putting in a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variable row heights.&lt;/strong&gt; The arithmetic above needs a fixed height. For rows that grow, use a library that measures them, and expect more complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find in page.&lt;/strong&gt; The browser's search cannot see rows that are not in the DOM. Provide your own filter, as the demo does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility.&lt;/strong&gt; Tell assistive technology the real size with &lt;code&gt;aria-rowcount&lt;/code&gt; and &lt;code&gt;aria-rowindex&lt;/code&gt;, and make the scroll area focusable so it can be scrolled from the keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production use.&lt;/strong&gt; For real products use a maintained library such as react-window. It handles the edge cases; the principle is exactly the one above.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://theadnansaleem.com/articles/react-table-virtualization-20000-rows" rel="noopener noreferrer"&gt;theadnansaleem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Migrating a Visual FoxPro system to .NET: an order of work that holds up</title>
      <dc:creator>Adnan Saleem</dc:creator>
      <pubDate>Mon, 05 Oct 2026 10:56:45 +0000</pubDate>
      <link>https://dev.to/theadnansaleem/migrating-a-visual-foxpro-system-to-net-an-order-of-work-that-holds-up-10l7</link>
      <guid>https://dev.to/theadnansaleem/migrating-a-visual-foxpro-system-to-net-an-order-of-work-that-holds-up-10l7</guid>
      <description>&lt;p&gt;Over a two-and-a-half-year engagement I migrated 42 financial workflows from a legacy Visual FoxPro system to C# and .NET Core, as the sole engineer. The old system carried two decades of business logic and had no written specification. The domain layer was rebuilt on Entity Framework Core with 42 T-SQL stored procedures against SQL Server, the FoxPro reports were replaced with reporting workflows in a Blazor application, and the old system was retired. This is the order of work I would recommend to anyone facing the same kind of migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these migrations are hard
&lt;/h2&gt;

&lt;p&gt;Visual FoxPro mixes data, logic and screens in one place. A form can hold validation, calculations and direct table updates, so there is no layer to lift out. Microsoft ended support for it years ago, which means the people who understand the system are often the only documentation left. The risk is not the new code. The risk is losing a rule that nobody remembers is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Inventory the workflows, not the files
&lt;/h2&gt;

&lt;p&gt;Start from what the business does: each task a user completes from start to finish, such as posting a payment or closing a period. List them, name an owner for each, and note which screens, tables and reports each one touches. That list becomes the plan and the definition of done. In my case there were 42.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Treat the old code as the specification
&lt;/h2&gt;

&lt;p&gt;When there is no written spec, the running system is the spec, including its oddities. For each workflow, read the FoxPro code and write down the rules in plain language, then confirm them with someone who uses the system. Record real inputs and the outputs they produce. Those pairs become the tests that the new system must pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Move the data model deliberately
&lt;/h2&gt;

&lt;p&gt;FoxPro tables are loosely typed, often without enforced keys. Moving to SQL Server is the moment to add what was missing: primary and foreign keys, proper date and decimal types, and constraints for rules that used to live only in forms. Do not copy the old structure column for column. Redesign it, and write a repeatable script that loads the legacy data into the new shape, so the load can be rehearsed many times before the real cutover.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Put the rules in a domain layer
&lt;/h2&gt;

&lt;p&gt;Business rules that were scattered across forms belong in one place in the new system: a domain layer in C# that knows nothing about screens or the database. That is what makes the rules testable, and it is what stops the new system from turning into the old one. Entity Framework Core handles ordinary reads and writes. For heavy set-based work, such as period-end calculations over many rows, a stored procedure is often the clearer and faster tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keep data access testable
&lt;/h2&gt;

&lt;p&gt;Hide the database behind a narrow interface, so domain logic can be tested without one and the stored procedures can be tested against a real one. Financial systems reward this more than most: when a number is questioned, you want a test that reproduces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Leave reports until the data is right
&lt;/h2&gt;

&lt;p&gt;Reports are where users notice differences first, and they depend on everything underneath. Rebuild them after the workflows that feed them, and compare old and new output line by line on the same data. A report that matches is the most convincing evidence that a workflow was migrated correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Migrate one workflow at a time, then retire
&lt;/h2&gt;

&lt;p&gt;A single big cutover concentrates all the risk in one weekend. Moving one workflow at a time keeps each step small enough to verify and to reverse. Where you can, run old and new side by side for a period and compare results. Retire the old system only when the inventory from step one is fully ticked off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Budget more time for understanding than for coding. The rewrite is the easy half.&lt;/li&gt;
&lt;li&gt;Keep a written log of every rule you find and every decision you make. It becomes the specification the old system never had.&lt;/li&gt;
&lt;li&gt;Do not improve behaviour while migrating it. Match first, then change, so a difference always means a bug.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://theadnansaleem.com/articles/visual-foxpro-to-dotnet-migration-guide" rel="noopener noreferrer"&gt;theadnansaleem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>legacy</category>
      <category>migration</category>
    </item>
    <item>
      <title>The hard part of micro frontends is the contract, not the bundler</title>
      <dc:creator>Adnan Saleem</dc:creator>
      <pubDate>Sun, 27 Sep 2026 08:25:52 +0000</pubDate>
      <link>https://dev.to/theadnansaleem/the-dashboard-is-blank-for-some-users-f2l</link>
      <guid>https://dev.to/theadnansaleem/the-dashboard-is-blank-for-some-users-f2l</guid>
      <description>&lt;p&gt;Most Module Federation write-ups stop at the config. You add the plugin, you name a few exposes and remotes, the demo loads a button from another build, and the post ends. That part takes an afternoon.&lt;/p&gt;

&lt;p&gt;The part that decides whether this survives contact with several teams is the contract between the shell and the remotes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we were splitting
&lt;/h2&gt;

&lt;p&gt;A React frontend for a government events platform, built as one application, released as one unit. Every team waited for the slowest team in the release. Nothing shipped alone.&lt;/p&gt;

&lt;p&gt;The goal was not novelty. It was letting teams deploy independently without turning every deploy into a coordination meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the config is the easy half
&lt;/h2&gt;

&lt;p&gt;With Module Federation, the shell loads remote modules at runtime. That runtime step is exactly where the guarantees you had at build time disappear.&lt;/p&gt;

&lt;p&gt;In a monolith, a mismatch between two parts of the app is a build error. You cannot ship it. After the split, the shell and the remote are built at different times, by different pipelines, possibly weeks apart. A mismatch is no longer a build error. It is a blank panel in production, or a shared library initialised twice, or a hook called against a React copy the shell does not know about.&lt;/p&gt;

&lt;p&gt;Nothing in the bundler config prevents that. The config is what makes loading possible. It says nothing about what is safe to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract
&lt;/h2&gt;

&lt;p&gt;So we made the boundary explicit, and versioned it.&lt;/p&gt;

&lt;p&gt;Each remote declares what it exposes and what it expects: the props of the exposed module, the shape of the events it emits, and the range of the shared dependencies it was built against. The shell checks that range before it mounts anything. If a remote sits outside it, the shell refuses it and renders a defined fallback, rather than mounting a component that will throw halfway through a user's session.&lt;/p&gt;

&lt;p&gt;Three things this gets you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Failures move left.&lt;/strong&gt; A mismatch is caught at load, in one place, with a message naming the remote and the expected range, instead of surfacing as a broken screen a user reports two days later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent deploys become boring.&lt;/strong&gt; A team ships when their remote satisfies the contract. No release train, no queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared dependencies stop being folklore.&lt;/strong&gt; React and the design system are declared as shared with explicit ranges, so "which version is actually loaded" has an answer you can read instead of guess.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The release cycle came down by about 40%. Most of that was not build time. It was the waiting that disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the contract before you touch the build config.&lt;/strong&gt; The config is reversible in an afternoon. A contract that was never written down becomes tribal knowledge, and tribal knowledge is what breaks when someone new ships their first remote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide what happens when a remote fails to load, before it happens.&lt;/strong&gt; Shell fallback, error boundary, feature flag, take your pick. The default is a blank rectangle, and the default is the one you get if you do not choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the shared list short.&lt;/strong&gt; Every shared dependency is a coupling you have to version. React and the design system, yes. A date library, probably not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not split by page. Split by ownership.&lt;/strong&gt; The seam should follow the team that maintains the code, because the point of the exercise is independent deploys, not smaller bundles. Smaller bundles are a side effect, and you can get those from code splitting without any of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to do this
&lt;/h2&gt;

&lt;p&gt;If one team owns the whole frontend, this is overhead with no payoff. You get the coordination cost of a distributed system and keep the release cadence of a monolith. Code splitting and a clean module boundary will serve you better.&lt;/p&gt;

&lt;p&gt;Micro frontends solve an organisational problem. If you do not have that problem, the bundler config is not going to give you one worth having.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://theadnansaleem.com/work/qatar-events-platform-module-federation" rel="noopener noreferrer"&gt;theadnansaleem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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