<?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: Katerina Bulkina</title>
    <description>The latest articles on DEV Community by Katerina Bulkina (@katerina_bulkina).</description>
    <link>https://dev.to/katerina_bulkina</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%2F4182234%2F8a984ad1-2c70-4f4e-9cb9-71ccbdd53c84.jpg</url>
      <title>DEV Community: Katerina Bulkina</title>
      <link>https://dev.to/katerina_bulkina</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/katerina_bulkina"/>
    <language>en</language>
    <item>
      <title>How to Structure a B2B SaaS Frontend Around Changing Operational Data</title>
      <dc:creator>Katerina Bulkina</dc:creator>
      <pubDate>Thu, 15 Oct 2026 05:15:19 +0000</pubDate>
      <link>https://dev.to/katerina_bulkina/how-to-structure-a-b2b-saas-frontend-around-changing-operational-data-5ghk</link>
      <guid>https://dev.to/katerina_bulkina/how-to-structure-a-b2b-saas-frontend-around-changing-operational-data-5ghk</guid>
      <description>&lt;p&gt;Imagine you're working in a logistics dashboard. You filter shipments to see everything that's delayed. You open one record, check its ETA, and start investigating the problem.&lt;/p&gt;

&lt;p&gt;Meanwhile, new shipment information arrives from the server. One shipment changes status. Another gets a new ETA. A background request finishes just after you've changed your filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should the interface do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If every update replaces the entire screen state, users can lose their place. If the application keeps showing old records, the interface becomes unreliable.&lt;/p&gt;

&lt;p&gt;We worked with this type of operational data while designing and developing LogiCore, a logistics and warehouse SaaS application at UITOP.&lt;/p&gt;

&lt;p&gt;The product connects shipment monitoring, warehouse locations, inventory records, and operational metrics. Its frontend uses React, TypeScript, and Zustand, with data served through REST APIs backed by Node.js, Express, PostgreSQL, and Prisma.&lt;/p&gt;

&lt;p&gt;For shipment monitoring, our team used Zustand-managed polling to retrieve updated status, ETA, and timestamp information. That project gives us a useful starting point for a broader engineering question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should a React application manage user interactions and changing server data at the same time?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start by separating three kinds of state
&lt;/h2&gt;

&lt;p&gt;A common source of frontend bugs is treating every piece of information on a screen as one large object.&lt;/p&gt;

&lt;p&gt;Consider a shipment monitoring page. It may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current shipment records and their statuses&lt;/li&gt;
&lt;li&gt;Active filters and pagination&lt;/li&gt;
&lt;li&gt;The shipment the user has selected&lt;/li&gt;
&lt;li&gt;Loading and refresh indicators&lt;/li&gt;
&lt;li&gt;Errors from API requests&lt;/li&gt;
&lt;li&gt;A timestamp showing when the data was updated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These values have different responsibilities. I find it useful to separate them into three groups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server data
&lt;/h2&gt;

&lt;p&gt;This represents information the application receives from its backend.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shipment ID&lt;/li&gt;
&lt;li&gt;Current shipment status&lt;/li&gt;
&lt;li&gt;ETA&lt;/li&gt;
&lt;li&gt;Last carrier update&lt;/li&gt;
&lt;li&gt;Total number of records matching a query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend determines the current business state. The frontend presents a snapshot of that information.&lt;/p&gt;

&lt;h2&gt;
  
  
  User interface state
&lt;/h2&gt;

&lt;p&gt;This represents the user's current interaction with the page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Active status filter&lt;/li&gt;
&lt;li&gt;Current page&lt;/li&gt;
&lt;li&gt;Selected shipment ID&lt;/li&gt;
&lt;li&gt;Expanded details&lt;/li&gt;
&lt;li&gt;Open dialogs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A background data refresh generally should not reset these choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request state
&lt;/h2&gt;

&lt;p&gt;This describes what's happening while the application communicates with the server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial loading&lt;/li&gt;
&lt;li&gt;Background refresh in progress&lt;/li&gt;
&lt;li&gt;Last successful response&lt;/li&gt;
&lt;li&gt;Request failure&lt;/li&gt;
&lt;li&gt;Data that may be stale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This third group matters because a screen can have valid data and an active request at the same time. A user should still be able to inspect the current results while the application retrieves newer information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the user's current view explicit
&lt;/h2&gt;

&lt;p&gt;Suppose a shipment table supports two filters: All and Delayed. The user can also navigate between pages and select a shipment. Those interactions can be represented independently from the returned shipment records.&lt;/p&gt;

&lt;p&gt;Zustand example:&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;create&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="s2"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StatusFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delayed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShipmentViewState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StatusFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;page&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="nl"&gt;selectedShipmentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StatusFilter&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;setPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;page&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="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;selectShipment&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useShipmentView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShipmentViewState&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;set&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;selectedShipmentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&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;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;selectedShipmentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;

    &lt;span class="na"&gt;setPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;

    &lt;span class="na"&gt;selectShipment&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="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;selectedShipmentId&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="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing the status filter resets pagination to page 1. The previous page might not exist in the filtered result.&lt;/p&gt;

&lt;p&gt;Selection is stored by shipment ID. If shipment records are refreshed, the application can still identify the selected item. The selected object doesn't need to be stored as a separate copy of the entire record.&lt;/p&gt;

&lt;p&gt;Whether selection should be cleared when filters change depends on the workflow. In this example, we clear it explicitly. In other products, keeping the detail view open may be preferable.&lt;/p&gt;

&lt;p&gt;The important part is making that behavior intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep polling separate from table interactions
&lt;/h2&gt;

&lt;p&gt;In LogiCore, shipment status and ETA information are refreshed through background requests to the REST API.&lt;/p&gt;

&lt;p&gt;Polling is a reasonable approach when the application needs periodic updates and the source data doesn't require a continuous streaming connection. The challenge is managing those requests as the user changes the current view.&lt;/p&gt;

&lt;p&gt;A naive implementation might look like this:&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;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/shipments&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;setShipments&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="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't account for current filters or pagination. It has no error handling. Requests can overlap if one response takes longer than the interval.&lt;/p&gt;

&lt;p&gt;It also doesn't address what happens when the component unmounts or the user switches to a different query. A more reliable implementation gives the polling process a clear lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  React polling example
&lt;/h2&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Shipment&lt;/span&gt; &lt;span class="o"&gt;=&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="nl"&gt;status&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;eta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShipmentPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;total&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="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FeedState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShipmentPage&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;syncing&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useShipmentFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;status&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="nx"&gt;page&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FeedState&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;syncing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&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;controller&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;AbortController&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;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&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;setFeed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;syncing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&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;active&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="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previous&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;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;syncing&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="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;try&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;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`/api/shipments?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShipmentPage&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="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;key&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="na"&gt;syncing&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&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;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setFeed&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;previous&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;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
              &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;syncing&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Could not refresh shipments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;previous&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="k"&gt;finally&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="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15000&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refresh&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;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;syncing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&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;&lt;em&gt;The endpoint, response shape, and 15-second interval are illustrative.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Several common problems here:&lt;/p&gt;

&lt;h2&gt;
  
  
  Requests follow the active query
&lt;/h2&gt;

&lt;p&gt;The effect depends on the current filter and page. When either changes, React cleans up the previous effect and starts a new request process for the new query. &lt;/p&gt;

&lt;h2&gt;
  
  
  Old requests cannot update the new view
&lt;/h2&gt;

&lt;p&gt;The cleanup aborts the previous fetch. The active check also prevents a response from updating component state after its effect has been cleaned up. That matters because network requests do not necessarily finish in the order they started. Imagine the user selects Delayed and immediately switches back to All.&lt;/p&gt;

&lt;p&gt;The Delayed request may finish last. Its response should not replace the current All result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling requests don't overlap
&lt;/h2&gt;

&lt;p&gt;The next timer is scheduled after the current request settles. That prevents this particular polling loop from starting another request while its previous request is still pending. In a production application, I would also consider an upper limit on request duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing results remain available during a refresh
&lt;/h2&gt;

&lt;p&gt;After the initial load succeeds, setting syncing to true doesn't clear the current response. The interface can continue showing its most recent results while the next request is in progress. This makes a useful distinction between initial loading and refreshing existing data.&lt;/p&gt;

&lt;p&gt;For a real application, I would also consider request deduplication, visibility-aware polling, authentication expiration, retry backoff, and how multiple components share the same data. A dedicated server-state library may be appropriate when those responsibilities grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every refresh a predictable UI behavior
&lt;/h2&gt;

&lt;p&gt;Fetching current data is only half the implementation. The frontend also needs rules for presenting that data. Consider a shipment with this information:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6cz2d9txey9szdky4hk.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%2Fi6cz2d9txey9szdky4hk.png" alt="A shipment record before and after an update: status, ETA, and the latest carrier timestamp." width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the user is looking at that shipment, the interface should present the newer information in the relevant record. But the application must also decide what happens if the new status changes which rows match the active filter.&lt;/p&gt;

&lt;p&gt;Suppose the current view shows only In transit shipments. After a refresh, a shipment becomes Delayed.&lt;/p&gt;

&lt;p&gt;Should it disappear immediately?&lt;/p&gt;

&lt;p&gt;There isn't one correct answer for every operational product. Immediate removal maintains strict consistency with the filter. It can also make a row disappear while someone is examining it.&lt;/p&gt;

&lt;p&gt;An alternative interaction is to retain the currently displayed set temporarily and indicate that newer results are available.&lt;/p&gt;

&lt;p&gt;The appropriate behavior depends on the task. For a monitoring dashboard, timely changes may take priority. For a table where operators select records and perform multi-step actions, preserving working context may be more important. This is a product decision that frontend and UX teams should agree on before implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat timestamps as part of data reliability
&lt;/h2&gt;

&lt;p&gt;A label such as "Last updated" sounds simple, but developers need to agree on what it represents.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the external carrier produced the information&lt;/li&gt;
&lt;li&gt;When the backend received the update&lt;/li&gt;
&lt;li&gt;When the API response was generated&lt;/li&gt;
&lt;li&gt;When the frontend successfully fetched the response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These times can differ. A successful API request at 12:30 doesn't prove that the carrier supplied new data at 12:30.&lt;/p&gt;

&lt;p&gt;In LogiCore, shipment monitoring includes status information, ETA drift indicators, and last-update timestamps associated with refreshed carrier data.&lt;/p&gt;

&lt;p&gt;For similar systems, I would make timestamp ownership explicit in the API contract. The response might distinguish:&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;"shipmentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SHP-2048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delayed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-23T16:10:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"carrierUpdatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-23T12:24:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fetchedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-23T12:30:00Z"&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;It can show when the shipment information last changed and separately track when the application last synchronized.&lt;/p&gt;

&lt;p&gt;This distinction becomes useful during outages, integration delays, and incidents where a user needs to know how current the information actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  A failed refresh should be visible
&lt;/h2&gt;

&lt;p&gt;Imagine a dispatcher reviewing 40 shipments. The page initially loads correctly. Five minutes later, the network becomes unstable and the background refresh fails.&lt;/p&gt;

&lt;p&gt;Should the table disappear?&lt;/p&gt;

&lt;p&gt;Usually, the application still has a previously successful response that the user can inspect. The interface can preserve those records and show that the latest refresh failed. A useful set of UI states will include:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam6v76ts8yx1p7zefbwt.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%2Fam6v76ts8yx1p7zefbwt.png" alt="How the shipment table handles loading, refreshes, errors, empty results, and expired sessions." width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An expired session needs special handling. The product should not keep issuing the same unauthorized request every few seconds. If the request fails repeatedly, retry logic may need an increasing delay.&lt;/p&gt;

&lt;p&gt;These are recommended patterns for operational software. Their exact implementation should follow the application's security requirements and data sensitivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't let background updates interrupt active work
&lt;/h2&gt;

&lt;p&gt;There is another problem that appears when a product moves beyond read-only dashboards.&lt;/p&gt;

&lt;p&gt;Suppose a user is editing the assigned carrier or reviewing a shipment exception while new information arrives from the backend. A refresh that blindly replaces an entire local object can overwrite in-progress changes.&lt;/p&gt;

&lt;p&gt;For editable records, I would define which fields belong to the current server snapshot and which fields belong to an active local operation. The application also needs a policy for conflicts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has the record changed since the user opened it?&lt;/li&gt;
&lt;li&gt;Is the user editing the same field that changed on the server?&lt;/li&gt;
&lt;li&gt;Can the operation be submitted against the newer record version?&lt;/li&gt;
&lt;li&gt;Does the user need to review the difference before continuing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depending on the product, that may require version fields, conditional updates, or a conflict-resolution interaction.&lt;/p&gt;

&lt;p&gt;These decisions are especially important in ERP, CRM, warehouse management, and other B2B applications where multiple users work with related records. This is a general implementation consideration, not a claim about a specific LogiCore editing workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would test before release
&lt;/h2&gt;

&lt;p&gt;For a React interface with changing operational data, I would test scenarios beyond the happy path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rapid filter changes&lt;/strong&gt;. Switch between filters while requests are pending. Confirm that the UI displays the result for the current query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow responses&lt;/strong&gt;. Simulate a request taking longer than the normal refresh interval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unmounting&lt;/strong&gt;. Navigate away while a request is pending. Confirm that the polling process stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated failures&lt;/strong&gt;. Simulate network errors and authentication expiration. Check what remains visible and how the interface recovers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changing records&lt;/strong&gt;. Update a shipment while a user is reviewing its details. Confirm that selection and navigation behave as intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inactive tabs&lt;/strong&gt;. Check whether background polling continues unnecessarily when the page isn't being used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different refresh speeds&lt;/strong&gt;. Evaluate whether status changes appear frequently enough for the operational requirements and whether polling creates unnecessary backend traffic.&lt;/p&gt;

&lt;p&gt;I would also review accessibility: refresh indicators should communicate meaningful changes, and frequent updates should not continuously interrupt screen-reader users.&lt;/p&gt;

&lt;h2&gt;
  
  
  What LogiCore taught us about operational frontend architecture
&lt;/h2&gt;

&lt;p&gt;UITOP designed and developed LogiCore as one application connecting shipment monitoring, warehouse structures, inventory, and operational data.&lt;/p&gt;

&lt;p&gt;The shipment interface uses React and TypeScript, with TanStack Table for table behavior and Zustand-managed polling for refreshed shipment information.&lt;/p&gt;

&lt;p&gt;The backend uses Node.js, Express, PostgreSQL, Prisma, and REST APIs.&lt;/p&gt;

&lt;p&gt;Those implementation decisions illustrate an important relationship between the frontend and its data sources. Users control filters, pagination, and navigation. The backend supplies current operational records. The request lifecycle determines when newer data reaches the interface.&lt;/p&gt;

&lt;p&gt;A frontend architecture needs to manage those responsibilities consistently as the product grows. If I were reviewing an existing B2B SaaS product today, I would start with one practical scenario:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the user changes a filter at the same moment a background request returns newer data?&lt;/strong&gt;&lt;br&gt;
The answer reveals a lot about the quality of the application's state management, request handling, and user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you read this, read also about the project and the team
&lt;/h2&gt;

&lt;p&gt;LogiCore is a logistics and warehouse SaaS designed and developed by UITOP, a product design and software development team specializing in complex B2B applications.&lt;/p&gt;

&lt;p&gt;The project covered product design, frontend and backend development, database architecture, API development, and QA.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://uitop.design/case-studies/logicore/" rel="noopener noreferrer"&gt;LogiCore case study&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>saas</category>
    </item>
    <item>
      <title>Building a shipment table for thousands of live records. What I learned on LogiCore?</title>
      <dc:creator>Katerina Bulkina</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:30:58 +0000</pubDate>
      <link>https://dev.to/katerina_bulkina/building-a-shipment-table-for-thousands-of-live-records-what-i-learned-on-logicore-51np</link>
      <guid>https://dev.to/katerina_bulkina/building-a-shipment-table-for-thousands-of-live-records-what-i-learned-on-logicore-51np</guid>
      <description>&lt;p&gt;A shipment table seems easy, a few columns, some filters, a status badge. Then real data shows up...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THOUSANDS&lt;/strong&gt; of shipments, statuses that change all day, and operators who need to find the one late truck fast.&lt;/p&gt;

&lt;p&gt;A table that feels fine with 50 test rows can crawl with 5,000.&lt;/p&gt;

&lt;p&gt;I ran into this on LogiCore, a logistics and warehouse management platform UITOP designed and built from scratch, for a UK logistics company. The client ran a multi-million dollar business across three or four separate tools. Data didn’t match between them, updates came late, and managers made big calls without the full picture. They wanted one system for the warehouse floor and the office.&lt;/p&gt;

&lt;p&gt;Our design, project management, development and QA teams worked on it together. Here’s how we built the shipment table, and why we made each choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the table had to do
&lt;/h2&gt;

&lt;p&gt;Operators used the table to find shipments and spot the ones that need attention. And they did it while statuses and ETAs kept changing.&lt;/p&gt;

&lt;p&gt;The frontend runs on &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt; and &lt;strong&gt;Vite&lt;/strong&gt;, with TanStack Table (React Table v8), &lt;strong&gt;Zustand&lt;/strong&gt; and &lt;strong&gt;Tailwind CSS&lt;/strong&gt;. The backend uses &lt;strong&gt;Node.js&lt;/strong&gt;, &lt;strong&gt;Express&lt;/strong&gt;, &lt;strong&gt;PostgreSQL&lt;/strong&gt; and &lt;strong&gt;Prisma ORM&lt;/strong&gt; behind a REST API.&lt;/p&gt;

&lt;p&gt;We had three separate problems to solve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;too much data to load, &lt;/li&gt;
&lt;li&gt;too many rows to draw,&lt;/li&gt;
&lt;li&gt;data that goes stale while the page is open.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Problem 1: the browser doesn’t need every shipment
&lt;/h2&gt;

&lt;p&gt;Why load thousands of records when the operator looks at one screen at a time? Loading everything makes the first view slow and wastes memory.&lt;/p&gt;

&lt;p&gt;So we used server-side pagination. The frontend asks the API for one page. PostgreSQL runs the query with the active filters and returns only that page.&lt;/p&gt;

&lt;p&gt;Details matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reset the page when a filter changes. If someone sits on page 12 and the new filter returns three pages, they should land on page 1, not on an empty screen.&lt;/li&gt;
&lt;li&gt;Keep sorting stable. Records change all day, so the API needs a fixed order. Otherwise rows jump between pages and people lose their place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem 2: one page can still be heavy
&lt;/h2&gt;

&lt;p&gt;Pagination cuts how much data comes back. It doesn’t cut how much the browser has to draw. Each shipment row carries a status badge, an ETA drift indicator and a “Last Updated” timestamp. Draw hundreds of those at once, and React has a lot of work to do.&lt;/p&gt;

&lt;p&gt;So we added row virtualization on top. The table only mounts the rows on screen, plus a small buffer for scrolling. TanStack Table handles the table logic, and the virtualization layer decides which rows exist in the DOM right now.&lt;/p&gt;

&lt;p&gt;The two techniques solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-side pagination controls how much data travels from the database to the browser.&lt;/li&gt;
&lt;li&gt;Virtualization controls how much the browser renders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you test both: API response times and rendering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9w76xrdkz5t1tdcywy0o.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%2F9w76xrdkz5t1tdcywy0o.png" alt="Left node: a white rounded platform holding a floating frosted-glass UI panel showing a data table with rows, a pagination control and a filter chip, rendered in blue glass. Small clean label floating above: " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: the data changes while someone is looking at it
&lt;/h2&gt;

&lt;p&gt;A shipment moves to a new status. An ETA slips. A carrier sends an update. If the screen shows old data, the operator decides based on a stale picture, and that’s the problem LogiCore was built to fix.&lt;/p&gt;

&lt;p&gt;We handled this with background polling. A Zustand store fetches fresh data from the REST API on a schedule, and the badges, ETA drift indicators and timestamps update from it. “Last Updated” shows the latest carrier update, not a cached value.&lt;/p&gt;

&lt;p&gt;The tricky part here is UX. If a badge changes while someone reads the list, the table shouldn’t reset their filters or scroll position. Polling also raises its own questions: how often to refresh, what to do with failed requests, and whether to keep polling when nobody is at the screen.&lt;/p&gt;

&lt;p&gt;Polling fit LogiCore’s update rhythm. A product that needs second-by-second updates might be better off with WebSockets or server-sent events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend decides how fast the table feels
&lt;/h2&gt;

&lt;p&gt;A slow query makes even a well-tuned React table feel broken. So a lot of the speed work happened below the UI.&lt;/p&gt;

&lt;p&gt;On the inventory screen, a multi-location SKU expands with a scoped REST call that fetches only that item’s child records. The first load stays at 100 rows, no matter how deep the data goes. The warehouse location tree loads the same way, one level at a time, from facility down to shelf.&lt;/p&gt;

&lt;p&gt;Filters by client, warehouse or SKU run through Prisma’s dynamic where clauses. That way we avoid raw SQL and still support any filter combination. For the KPI cards, PostgreSQL window functions calculate week-over-week changes inside the database, and the frontend just shows the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design and performance are one conversation
&lt;/h2&gt;

&lt;p&gt;More columns don’t make a table more useful. They make it harder to scan. We gave status and timing a fixed place in each row, because that’s what operators check first.&lt;/p&gt;

&lt;p&gt;Every design choice here has a frontend cost. Columns decide how heavy each cell is. Filters decide what the API has to query. And the design system needs states for loading and errors, not only the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out of it
&lt;/h2&gt;

&lt;p&gt;Before launch, our QA team ran stress tests under heavy load, and the platform held at 100% stability. The office and the warehouse now work from the same live data. Managers see transit delays and stock shortages as they happen, so they fix problems before those cost money. Daily shipping decisions got 2x faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start with your own table
&lt;/h2&gt;

&lt;p&gt;If you’re building a B2B SaaS product with big tables, start with three questions. How much data does the screen request? How much does it render? What happens when the records change? The answers usually point to the next engineering decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you make a React table fast with thousands of records?&lt;/strong&gt;&lt;br&gt;
Combine server-side pagination with row virtualization. Pagination limits what the API sends, and virtualization limits what the browser draws. On LogiCore, we used both with TanStack Table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should a logistics dashboard use polling or WebSockets?&lt;/strong&gt;&lt;br&gt;
It depends on how fast the data changes. Polling worked for LogiCore’s shipment statuses and ETAs. If operators need second-by-second updates, WebSockets or server-sent events fit better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I look for in a team building custom warehouse management software?&lt;/strong&gt;&lt;br&gt;
Look for a team that covers design, backend and QA together, because table speed depends on the database as much as on the UI. UITOP built LogiCore that way, and the platform held 100% stability under peak-load stress tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the project
&lt;/h2&gt;

&lt;p&gt;LogiCore is a logistics and warehouse management platform designed and built by UITOP, a product design and development studio for complex B2B software.&lt;/p&gt;

&lt;p&gt;Case study: &lt;a href="https://uitop.design/case-studies/logicore/" rel="noopener noreferrer"&gt;https://uitop.design/case-studies/logicore/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
