<?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: Yuji Min</title>
    <description>The latest articles on DEV Community by Yuji Min (@yuji_min).</description>
    <link>https://dev.to/yuji_min</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2925094%2F3d57eaaa-844e-4a17-8d7a-aef1fe7f98a7.jpg</url>
      <title>DEV Community: Yuji Min</title>
      <link>https://dev.to/yuji_min</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuji_min"/>
    <language>en</language>
    <item>
      <title>Troubleshooting TRPC data flicker: From recognizing the problem to fixing it</title>
      <dc:creator>Yuji Min</dc:creator>
      <pubDate>Sun, 20 Jul 2025 04:21:42 +0000</pubDate>
      <link>https://dev.to/yuji_min/troubleshooting-trpc-data-flicker-from-recognizing-the-problem-to-fixing-it-30am</link>
      <guid>https://dev.to/yuji_min/troubleshooting-trpc-data-flicker-from-recognizing-the-problem-to-fixing-it-30am</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;1. Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The brief disappearance and reappearance of data on the screen—commonly referred to as “flickering”—is a problem nearly every frontend developer has encountered at least once. This flicker typically occurs when existing values momentarily vanish during a server data refetch, significantly affecting the user experience. In this post, I’ll walk through a real-world issue I encountered in a production project, analyze its cause, compare several possible solutions, and explain the decision-making process behind the approach I ultimately chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Identifying the Problem and Context&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In this project, I was tasked with resolving the flickering issue during the QA verification phase. At this stage, the most critical requirement was to fix the problem without affecting any tests already marked as “Pass” by the QA team. This meant preserving the stability of existing features while minimizing the scope of code changes. &lt;strong&gt;Rather than engaging in unnecessary refactoring, the priority was to accurately identify the issue and find a feasible solution within the given constraints.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The issue arose in the following scenario: The page I was working on sends user input to the server and displays simulation results. Since I’m unable to disclose the specifics of the actual project, I’ll explain using an analogy: a simulation of egg doneness based on boiling time. In this case, the user input is the “boiling time,” and the output is the “egg’s doneness.” Meanwhile, the server already stores a preset boiling time along with the corresponding egg status. For example, if the client sends an initial boiling time of 0, the server responds with a pre-existing value such as &lt;code&gt;{ time: 12, eggStatus: 30 }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In summary, the client sends &lt;code&gt;modifiedTime&lt;/code&gt; to the server, and the server responds with &lt;code&gt;{ time: &amp;lt;preset server time&amp;gt;, eggStatus: &amp;lt;simulated result based on modifiedTime&amp;gt; }&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pages/boilSimulation/index.page.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BoilSimulationPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setModifiedTime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="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="c1"&gt;// {time, eggStatus}&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBoilSimulationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="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;Current Time: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;time&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;button&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setModifiedTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&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="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;button&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;Simulation Time: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;modifiedTime&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;button&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setModifiedTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&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="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;button&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;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌀Loading Spinner&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;eggStatus&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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// queries/boilSimulation.ts&lt;/span&gt;
&lt;span class="c1"&gt;// A custom hook that directly returns the query result from tRPC&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;useBoilSimulationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Parameters&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;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boilSimulation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boilSimulation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// server/routers/boilSimulation.ts&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;boilSimulationRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;procedure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;boilSimulationResultsSchema&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;query&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="nx"&gt;input&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;// Simulation logic omitted&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;boilSimulationResults&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// {time, eggStatus}&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;Among the returned values, &lt;code&gt;eggStatus&lt;/code&gt; is a simulation result and was already covered with a loading spinner to handle delays. However, the &lt;code&gt;time&lt;/code&gt; value—preset on the server and never expected to change—was not handled with any loading treatment. As a result, when the user clicked the + or - buttons to change &lt;code&gt;modifiedTime&lt;/code&gt;, triggering a new simulation fetch, even the unchanged &lt;code&gt;time&lt;/code&gt; briefly disappeared and then reappeared.&lt;/p&gt;

&lt;p&gt;This flickering occurred because TanStack Query, by default, removes previous data when a new fetch begins, then re-caches the incoming data. During this transition, the &lt;code&gt;data&lt;/code&gt; becomes temporarily &lt;code&gt;undefined&lt;/code&gt;, leading to the flicker. &lt;strong&gt;Although &lt;code&gt;time&lt;/code&gt; is conceptually stable, it was grouped with &lt;code&gt;eggStatus&lt;/code&gt; inside the &lt;code&gt;data&lt;/code&gt; object and was thus cleared and reloaded during every fetch—causing unintended visual behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Exploring Potential Solutions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After identifying the root cause, I considered several approaches. Taking into account both practical constraints and structural complexity, I arrived at the following three potential solutions:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1) Splitting the API&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concept&lt;/strong&gt;: Separate &lt;code&gt;time&lt;/code&gt; and &lt;code&gt;eggStatus&lt;/code&gt; into distinct tRPC queries so they can be managed independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Each field can be fetched separately based on its update frequency, which prevents unnecessary flickering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it was ruled out&lt;/strong&gt;: Modifying the API structure at the verification stage carried significant risk and required substantial changes on both client and server. Thus, it was deemed infeasible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2) Managing &lt;code&gt;time&lt;/code&gt; in Local State&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concept&lt;/strong&gt;: Copy the server-provided &lt;code&gt;time&lt;/code&gt; into local state for independent rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Values stored in local state persist regardless of server fetches, eliminating flicker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it was ruled out&lt;/strong&gt;: This approach contradicts the core philosophy of TanStack Query, which promotes unified management of server state. Introducing local state solely for rendering introduces potential sync issues and maintenance overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3) Using the &lt;code&gt;keepPreviousData&lt;/code&gt; Option&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concept&lt;/strong&gt;: Utilize the &lt;code&gt;placeholderData: keepPreviousData&lt;/code&gt; option in TanStack Query to retain the previous data until the new response arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it was chosen&lt;/strong&gt;: This was the only solution that required minimal code changes while effectively resolving the flicker. It aligned with the intended usage of TanStack Query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome&lt;/strong&gt;: While a new fetch is underway, the previous data is retained and instantly replaced when the new data arrives—preventing the &lt;code&gt;data&lt;/code&gt; object from becoming &lt;code&gt;undefined&lt;/code&gt;, and thereby eliminating the flickering of the &lt;code&gt;time&lt;/code&gt; field.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;keepPreviousData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&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;useBoilSimulationResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Parameters&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;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boilSimulation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boilSimulation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;placeholderData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keepPreviousData&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Added keepPreviousData option&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;4. Considerations When Using &lt;code&gt;keepPreviousData&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Unexpectedly, a new issue emerged after applying &lt;code&gt;keepPreviousData&lt;/code&gt;. Previously, when fetching a new &lt;code&gt;eggStatus&lt;/code&gt;, the &lt;code&gt;isLoading&lt;/code&gt; flag would toggle to &lt;code&gt;true&lt;/code&gt;, triggering the spinner. However, after applying &lt;code&gt;keepPreviousData&lt;/code&gt;, &lt;code&gt;isLoading&lt;/code&gt; no longer became &lt;code&gt;true&lt;/code&gt;, preventing the spinner from appearing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Comparison: &lt;code&gt;isLoading&lt;/code&gt; vs &lt;code&gt;isPending&lt;/code&gt; vs &lt;code&gt;isFetching&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To understand this behavior, it's essential to understand the relationship between &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;fetchStatus&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;status&lt;/code&gt;: Does the query have data?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pending&lt;/code&gt;: No data has been received yet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;error&lt;/code&gt;: Query failed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;success&lt;/code&gt;: Data has been successfully fetched and cached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fetchStatus&lt;/code&gt;: Is the query function (&lt;code&gt;queryFn&lt;/code&gt;) running?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetching&lt;/code&gt;: Fetch in progress&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;paused&lt;/code&gt;: Fetch has been issued but paused (e.g., offline), resumes when network connectivity returns&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;idle&lt;/code&gt;: No active fetch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;isLoading&lt;/code&gt;&lt;/strong&gt;: As explained in &lt;a href="https://github.com/TanStack/query/discussions/6297#discussioncomment-7467010" rel="noopener noreferrer"&gt;this comment&lt;/a&gt; by TkDoDo and the &lt;a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/queryObserver.ts" rel="noopener noreferrer"&gt;source code&lt;/a&gt;, &lt;code&gt;isLoading&lt;/code&gt; is determined by the combination of &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;fetchStatus&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFetching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetchStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetching&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isFetching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;In other words, &lt;code&gt;isLoading&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; only when the query has &lt;strong&gt;no data&lt;/strong&gt; (&lt;code&gt;status === 'pending'&lt;/code&gt;) and a fetch is &lt;strong&gt;in progress&lt;/strong&gt; (&lt;code&gt;fetchStatus === 'fetching'&lt;/code&gt;). With &lt;code&gt;keepPreviousData&lt;/code&gt; enabled, previous data remains(&lt;code&gt;status !== 'pending'&lt;/code&gt;), so &lt;code&gt;isLoading&lt;/code&gt; never becomes &lt;code&gt;true&lt;/code&gt;. Thus, relying on &lt;code&gt;isLoading&lt;/code&gt; to show loading spinners no longer works.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;isPending&lt;/code&gt;&lt;/strong&gt;: Added in TanStack Query v5, &lt;code&gt;isPending&lt;/code&gt; may seem like a replacement for &lt;code&gt;isLoading&lt;/code&gt;, but they differ. As seen above, &lt;code&gt;isLoading&lt;/code&gt; requires both &lt;code&gt;isPending&lt;/code&gt; and &lt;code&gt;isFetching&lt;/code&gt; to be true. In contrast, &lt;code&gt;isPending&lt;/code&gt; is true &lt;strong&gt;as long as the query has not yet received data&lt;/strong&gt; (&lt;code&gt;status === 'pending'&lt;/code&gt;). It encompasses a broader range than &lt;code&gt;isLoading&lt;/code&gt;. It also pairs well with TypeScript’s type guard logic—for example, &lt;code&gt;!isPending &amp;amp;&amp;amp; !isError&lt;/code&gt; ensures &lt;code&gt;data&lt;/code&gt; is defined. Still, since the query retains previous data in this case, &lt;code&gt;isPending&lt;/code&gt; is also &lt;code&gt;false&lt;/code&gt;, making it unsuitable for controlling the loading spinner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;isFetching&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;isFetching&lt;/code&gt; turns &lt;code&gt;true&lt;/code&gt; whenever a fetch is in progress, regardless of whether previous data exists. Therefore, even when &lt;code&gt;keepPreviousData&lt;/code&gt; is in effect, &lt;code&gt;isFetching&lt;/code&gt; correctly reflects the fetch status. As such, it was the most reliable flag to control loading UI in this context.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a table summarizing the behaviors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;status&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;fetchStatus&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isLoading&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isFetching&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetching&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;success&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetching&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;success&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetching&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I validated these behaviors through the official documentation and GitHub Discussions and concluded that replacing &lt;code&gt;isLoading&lt;/code&gt; with &lt;code&gt;isFetching&lt;/code&gt; was the most reliable way to control the spinner in this page. &lt;strong&gt;The core insight behind &lt;code&gt;keepPreviousData&lt;/code&gt; lies not just in retaining values, but in understanding how it alters the query's state transitions.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This experience went beyond simply fixing a UI flicker—it prompted me to reflect on how to prioritize and make technical decisions under real-world constraints.&lt;/strong&gt; The problem could have easily been overlooked without a solid grasp of TanStack Query’s internal state mechanisms (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;fetchStatus&lt;/code&gt;). The decision to use &lt;code&gt;keepPreviousData&lt;/code&gt; allowed me to address the issue without restructuring the system, making it both practical and aligned with the library’s philosophy. Switching to &lt;code&gt;isFetching&lt;/code&gt; for spinner control preserved the user experience as intended.&lt;/p&gt;

&lt;p&gt;In hindsight, separating &lt;code&gt;time&lt;/code&gt; and &lt;code&gt;eggStatus&lt;/code&gt; into distinct APIs from the start might have been more ideal. However, in the real world, we often can’t afford perfect architectures. What matters more is making the best possible decision under given constraints—grounded in an accurate understanding of system behavior.&lt;/p&gt;

&lt;p&gt;Even a small UI issue like this can reveal deeper technical insights. Tackling such problems with depth and accuracy has proven to be a valuable part of my growth as a developer. Moving forward, I hope to continue focusing on subtle yet meaningful issues to create even more refined and reliable user experiences.&lt;/p&gt;

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