<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
    <channel>
        <title>notes: dirty-hacks</title>
        <link>https://notes.zachmanson.com/dirty-hacks</link>
        <description>Notes tagged #dirty-hacks</description>
        <atom:link href="https://notes.zachmanson.com/dirty-hacks" rel="self" />
        <docs>http://www.rssboard.org/rss-specification</docs>
        <generator>ochrs</generator>
        <image>
            <url>https://zachmanson.com/icons/android-chrome-256x256.png</url>
            <title>notes: dirty-hacks</title>
            <link>https://notes.zachmanson.com/dirty-hacks</link>
        </image>
        <language>en</language>
        <lastBuildDate>Sat, 09 May 2026 04:04:15 </lastBuildDate>
        
        <item>
            <title>RTK Query vs. Infinite Scrolling</title>
            <link>https://notes.zachmanson.com/rtk-query-vs.-infinite-scrolling</link>
            
            <content:encoded>
                <![CDATA[<p><strong>Update 2026-02-04: There is now native support for infinite scrolling-style loading patterns.</strong> See the <a href="https://github.com/reduxjs/redux-toolkit/releases/tag/v2.6.0">announcement here</a> and the <a href="https://redux-toolkit.js.org/rtk-query/usage/infinite-queries">docs here</a>.</p>
<hr />
<p>RTK Query with <a href="https://notes.zachmanson.com/react">React</a> is pretty great! The primary pattern you find yourself using with it is: </p>
<ol>
<li>writing a small API definition that provides <ol>
<li>the endpoint url </li>
<li>type definitions for request args and response schema</li>
<li>(usually) simply logic for converting passed argument type into request params/body</li>
<li>(usually) simple logic for converting response payload into the response schema</li>
</ol>
</li>
<li>importing a hook that RTK Query automatically generates for you</li>
<li>reuse the same hook across multiple components</li>
</ol>
<p>RTK Query handles loading states, stale while revalidate states, error states, type safety, and caching.</p>
<p>When writing React components, the caching is the most impactful of these.   You can just call the same hook in many different components, and as long as the params are the same the same it will only be a single network request. This removes the need for many invocations <code>useState</code> and Redux state calls, since you can just feign a call to the backend every time you need that data.</p>
<p>There is a little boilerplate you have to write, but the meat of a basic RTK Query definition will look something like this:</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">ids</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>This will generate a hook called <code>useGetLocation</code> which will be cached.  Suddenly you never need to store a <code>Location</code> object in state to share it across components and you can just call <code>useGetLocation({id:5})</code> every time you need it. It also provides a lazy version of the hook that returns a normal function you can use to retrieve the data, which is good for APIs that need to be called multiple times.</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">LocationCard</span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="nx">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="nx">location</span><span class="p">,</span><span class="w"> </span><span class="nx">isLoading</span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useGetLocation</span><span class="p">({</span><span class="nx">id</span><span class="o">:</span><span class="nx">id</span><span class="p">});</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">getLocationsLazy</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useLazyGetLocation</span><span class="p">();</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="nx">location</span><span class="p">.</span><span class="nx">address</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>For straightforward GET requests this pattern stellar.  RTK Query encourages you to put all the API definitions into a single file (<code>src/api/api.ts</code>), or into a small number files with particular scopes.  For small-medium applications this works well, if you know the URL of an endpoint you want to hit you can just search within the <code>api.ts</code> file, use the accompanying hook and you are good to go, type safety and all.  I'm sure this single file approach would breakdown for big applications but its very pleasant at smaller scales.</p>
<p>RTK Query also lets you do POST/PUT/PATCH requests with a similar definition style. Instead of using <code>build.query</code>, you use <code>build.mutation</code>.</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">addLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.mutation</span><span class="o">&lt;</span>
<span class="w">      </span><span class="p">{</span><span class="w"> </span><span class="nx">location_id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">site_id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">address</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">abn</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">user_id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="nx">NewLocation</span>
<span class="w">    </span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">body</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;POST&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="kt">body</span><span class="p">,</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">    </span><span class="p">}),</span>
<span class="w">    </span><span class="nx">patchLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.mutation</span><span class="o">&lt;</span><span class="ow">void</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">Partial</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;PATCH&quot;</span><span class="p">,</span>
<span class="w">          </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="sb">`api/v1/location/</span><span class="si">${</span><span class="nx">primaryKey</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="w">          </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="kt">data</span><span class="p">,</span>
<span class="w">        </span><span class="p">};</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>The hooks generated here will return a function that can be used to send requests.</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">LocationCard</span><span class="p">({</span><span class="nx">id</span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="nx">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="nx">location</span><span class="p">,</span><span class="w"> </span><span class="nx">isLoading</span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useGetLocationQuery</span><span class="p">({</span><span class="nx">id</span><span class="o">:</span><span class="nx">id</span><span class="p">})</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">patchLocation</span><span class="p">,</span><span class="w"> </span><span class="nx">isPatchLoading</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePatchLocationMutation</span><span class="p">()</span>

<span class="w">    </span><span class="kd">function</span><span class="w"> </span><span class="nx">onUpdate</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="o">:</span><span class="nx">Partial</span><span class="p">&lt;</span><span class="nt">Location</span><span class="p">&gt;)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">patchLocation</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="nx">location</span><span class="p">.</span><span class="nx">address</span><span class="p">}</span>
<span class="w">            </span><span class="p">...</span>
<span class="w">            </span><span class="p">{</span><span class="cm">/* some input that calls onUpdate on change */</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>
<p>But wait! If I patch a location, how will the useGetLocation hook know that it's cache is invalid?</p>
<h2 id="cache-invalidation">Cache Invalidation</h2>
<p>RTK Query uses a tag system for cache invalidation, where a tag represents a kind of query.  Queries can assign themselves tags, and mutations can provide a list of tags they will invalidate. </p>
<p>These can be static</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">ids</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;Location&#39;</span><span class="p">],</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>or dynamic, based on the params given</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">ids</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/location&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">},</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">id</span><span class="p">}]</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>And similarly for mutations</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">patchLocation</span><span class="o">:</span><span class="w"> </span><span class="kt">build.mutation</span><span class="o">&lt;</span><span class="ow">void</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">Partial</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;PATCH&quot;</span><span class="p">,</span>
<span class="w">          </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="sb">`api/v1/location/</span><span class="si">${</span><span class="nx">primaryKey</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
<span class="w">          </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="kt">data</span><span class="p">,</span>
<span class="w">        </span><span class="p">};</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">invalidatesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="kr">type</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Location&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">arg.id</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="c1">// or just [&quot;Location&quot;] to invalidate the whole category</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>Where you can, dynamic tags are generally better since you have much more granular control.  Again, this works pretty well for straightforward queries.</p>
<h2 id="pagination">Pagination</h2>
<p>RTK Query doesn't include a mechanism for paging data, but passing a page number as a param works fine if you have discrete pages.</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/locations&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">}</span>
<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">result</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">location</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">location.id</span><span class="p">}),</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;LIST&quot;</span><span class="p">}],</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<h2 id="but-what-about-infinite-scroll">But what about infinite scroll?</h2>
<p>When infinitely scrolling, normally you'd have an array that stores all the results that have been loaded so far, and then you'd append new results to the end of this list when they are loaded in (maybe even dropping some entries from the start if you are memory constrained).  The hook generated by the previous example will only ever let you access one page's entries at a time. </p>
<p>RTK Query doesn't have any special functions for infinite scrolling, but the pieces it gives you are enough to make it work. When writing a query definition you can explicitly set which params contribute are used to define unique cache entries, and another function to say how old cache entries should be overwritten by new ones.</p>
<div class="highlight"><pre><span></span><code><span class="w">    </span><span class="nx">getLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">Location</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/locations&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">}</span>

<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">result</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">location</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">location.id</span><span class="p">}),</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;LIST&quot;</span><span class="p">}],</span>

<span class="w">      </span><span class="c1">// overwrite cached value when page value changes, treat page</span>
<span class="w">      </span><span class="nx">serializeQueryArgs</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">queryArgs</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="p">...</span><span class="nx">queryArgs</span><span class="p">,</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">undefined</span><span class="w"> </span><span class="p">}),</span>

<span class="w">      </span><span class="c1">// when overwriting cache value, append new data to old list</span>
<span class="w">      </span><span class="nx">merge</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">currentCache</span><span class="p">,</span><span class="w"> </span><span class="nx">newData</span><span class="p">,</span><span class="w"> </span><span class="nx">otherArgs</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">currentCache</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">newData</span><span class="p">);</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>This works riiiiight until you need to invalidate the cache. Say you have a page with infinite scroll via pressing a load more button at the bottom, and the user has pressed it 3 times. At this stage, the hook will look like <code>const {data} = useGetLocationsQuery({page: 4});</code>. If page size is 20, data, will contain 80 entries. Then you edit an entry that appears on page 3. Or you add a new location.  Both of these operation will invalidate the cache, but the merge strategy provided will simply add the new cache to the end of the previous cache, so  <code>const {data} = useGetLocationsQuery({page: 4});</code> will now have 100 entries, 80 from the original 4 pages and 20 from the 4th page repeated again.  <strong>Ideally the merge function would have a way of distinguishing cache updates that are due to tag invalidation from cache updates due to parameter changes, but there is currently no way to do this</strong>.</p>
<h2 id="workarounds">Workarounds</h2>
<p>There are a <a href="https://github.com/reduxjs/redux-toolkit/discussions/1163">number</a> <a href="https://github.com/reduxjs/redux-toolkit/issues/3733">of</a> <a href="https://github.com/reduxjs/redux-toolkit/discussions/3174">discussion</a> of how to mitigate this.  The solution I went with looks like this.</p>
<p>Firstly, the response payload must include the page number metadata.  This is usually the case with paginated endpoints, but this data now needs to be included in the cached result in the query definition.</p>
<p>When merging the cache, only append the new data if the new data page number is greater than the old value:</p>
<div class="highlight"><pre><span></span><code><span class="kr">type</span><span class="w"> </span><span class="nx">PaginatedData</span><span class="o">&lt;</span><span class="nx">ItemType</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">pages</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">per_page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">total</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
<span class="w">  </span><span class="nx">items</span><span class="o">:</span><span class="w"> </span><span class="kt">ItemType</span><span class="p">[];</span>
<span class="p">};</span>
<span class="p">...</span>

<span class="w">    </span><span class="nx">getLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">build.query</span><span class="o">&lt;</span><span class="nx">PaginatedData</span><span class="o">&lt;</span><span class="nx">Location</span><span class="o">&gt;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">({</span>
<span class="w">      </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
<span class="w">        </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;api/v1/locations&quot;</span><span class="p">,</span>
<span class="w">        </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="p">}</span>

<span class="w">      </span><span class="p">}),</span>
<span class="w">      </span><span class="nx">transformResponse</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">response</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span>
<span class="w">      </span><span class="nx">providesTags</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">result</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">,</span><span class="w"> </span><span class="nx">arg</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">result</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">location</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">location.id</span><span class="p">}),</span><span class="w"> </span><span class="p">{</span><span class="kr">type</span><span class="o">:</span><span class="s2">&quot;Location&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;LIST&quot;</span><span class="p">}],</span>

<span class="w">      </span><span class="c1">// overwrite cached value when page value changes, treat page</span>
<span class="w">      </span><span class="nx">serializeQueryArgs</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">queryArgs</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="p">...</span><span class="nx">queryArgs</span><span class="p">,</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="kt">undefined</span><span class="w"> </span><span class="p">}),</span>

<span class="w">      </span><span class="c1">// when overwriting cache value, append new data to old list if page value increases</span>
<span class="w">      </span><span class="nx">merge</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">currentCache</span><span class="p">,</span><span class="w"> </span><span class="nx">newData</span><span class="p">,</span><span class="w"> </span><span class="nx">otherArgs</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">newData</span><span class="p">.</span><span class="nx">page</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="nx">currentCache</span><span class="p">.</span><span class="nx">page</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="nx">currentCache</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">newData</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span>
<span class="w">          </span><span class="nx">currentCache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="nx">newData</span><span class="p">,</span><span class="w"> </span><span class="nx">items</span><span class="o">:</span><span class="w"> </span><span class="kt">currentCache.items</span><span class="w"> </span><span class="p">};</span>
<span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">          </span><span class="k">return</span><span class="w"> </span><span class="nx">newData</span><span class="p">;</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">    </span><span class="p">}),</span>
</code></pre></div>
<p>Then in the component where the hook is called, ensure that the page number is reset to 1 whenever a mutation occurs that will invalidate the <code>useGetLocationsQuery</code> data.  Since this page isn't an increase from the previous, the old cache will be discarded. This can be annoying since the component that triggers the mutation and invalidates the data may be completely seperate from the component that queries for the data.  </p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">EditLocationAddress</span><span class="p">({</span><span class="nx">id</span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="kt">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">patchLocation</span><span class="p">,</span><span class="w"> </span><span class="nx">isPatchLoading</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePatchLocationMutation</span><span class="p">();</span>

<span class="w">    </span><span class="kd">function</span><span class="w"> </span><span class="nx">onUpdate</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="o">:</span><span class="kt">Partial</span><span class="p">&lt;</span><span class="nt">Location</span><span class="p">&gt;)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">patchLocation</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="p">);</span>
<span class="w">        </span><span class="c1">// TODO somehow reset the page number to 0 in all relevant places</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="cm">/* some input that calls onUpdate on change */</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>There is probably a clever way of doing this where the cached page number is used as the canonical page number across all components, and then you could force a reset back to one from any component, but I haven't needed to implement this just yet in any applications.</p>
<p>Maybe something like this? I haven't tried it yet.</p>
<div class="highlight"><pre><span></span><code><span class="kd">function</span><span class="w"> </span><span class="nx">EditLocationAddress</span><span class="p">({</span><span class="nx">id</span><span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">:</span><span class="nx">number</span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">patchLocation</span><span class="p">,</span><span class="w"> </span><span class="nx">isPatchLoading</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePatchLocationMutation</span><span class="p">();</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">getLocationsLazy</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useLazyGetLocationsQuery</span><span class="p">();</span>

<span class="w">    </span><span class="kd">function</span><span class="w"> </span><span class="nx">onUpdate</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="o">:</span><span class="nx">Partial</span><span class="p">&lt;</span><span class="nt">Location</span><span class="p">&gt;)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">patchLocation</span><span class="p">(</span><span class="nx">locationUpdate</span><span class="p">);</span>
<span class="w">        </span><span class="nx">getLocationsLazy</span><span class="p">(</span>
<span class="w">            </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">},</span>
<span class="w">            </span><span class="kc">true</span><span class="w"> </span><span class="c1">// lazy queries ignore cache by default, true overrides this</span>
<span class="w">        </span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="p">...</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w">        </span><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">            </span><span class="p">{</span><span class="cm">/* some input that calls onUpdate on change */</span><span class="p">}</span>
<span class="w">        </span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="w">    </span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>]]>
            </content:encoded>
            <guid isPermaLink="false">https://notes.zachmanson.com/rtk-query-vs.-infinite-scrolling</guid>
            <pubDate>2024-06-03</pubDate>
        </item>
        

    </channel>
</rss>