erock's bloghttps://erock.prose.sh2022-06-29T00:10:06ZTechnical writings by Eric BowererockRefactoring listifi to use saga-query2022-07-05T13:45:07Zhttps://erock.prose.sh/refactor-listifi-to-use-saga-query<p>Over the weekend I decided to refactor <a href="https://listifi.app" rel="nofollow">listifi.app</a> to use
my new library <a href="https://github.com/neurosnap/saga-query" rel="nofollow">saga-query</a>.</p>
<p>The results of that work culminated in a
<a href="https://github.com/neurosnap/listifi/pull/2" rel="nofollow">PR</a> where I was able to remove
roughly 300 lines of business logic from the listifi codebase. As you'll see, I
was able to accomplish this by abstracting typical lifecycle processes like
setting up loaders -- which were previously manually written -- to use
middleware through <code>saga-query</code>. The conversion process was pretty painless.</p>
<p>I'll extract a couple of examples demonstrating how we can dramatically remove
business logic by using <code>saga-query</code>.</p>
<h2 id="example-authentication-logic"><a class="anchor" href="#example-authentication-logic" rel="nofollow">#</a> Example: Authentication logic</h2>
<p>The previous API interaction was built with
<a href="https://github.com/neurosnap/redux-cofx" rel="nofollow">redux-cofx</a> which for the purposes of
this blog article can be hot swapped for
<a href="https://github.com/redux-saga/redux-saga" rel="nofollow">redux-saga</a>.</p>
<p>Here are the before and after files for my authentication logic:</p>
<ul>
<li><a href="https://github.com/neurosnap/listifi/blob/20ad30e0b23e04614cb504788f736c257e8a34d1/src/auth/index.ts" rel="nofollow">Before</a></li>
<li><a href="https://github.com/neurosnap/listifi/blob/5ae6930a9617cafbd5a9557d909c94c39c39d674/src/auth/index.ts" rel="nofollow">After</a></li>
</ul>
<p>The logic is these files are identical. A cursory glance can see that I was able
to <strong>reduce the amount of code by 50%.</strong> Let's zoom in to see what changed by
looking at a single API request.</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">login</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="s2">"LOGIN"</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">function</span><span class="o">*</span> <span class="nx">onLoginLocal</span><span class="p">(</span><span class="nx">body</span>: <span class="kt">LoginLocalParams</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"> <span class="kr">const</span> <span class="nx">loaderName</span> <span class="o">=</span> <span class="nx">Loaders</span><span class="p">.</span><span class="nx">login</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">setLoaderStart</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> <span class="kr">const</span> <span class="nx">resp</span>: <span class="kt">ApiFetchResponse</span><span class="p"><</span><span class="nt">TokenResponse</span><span class="p">></span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"> <span class="nx">apiFetch</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> <span class="s2">"/auth/login/local"</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"> <span class="nx">auth</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"> <span class="nx">method</span><span class="o">:</span> <span class="s2">"POST"</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"> <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"> <span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">resp</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">setLoaderError</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span><span class="p">,</span> <span class="nx">message</span>: <span class="kt">resp.data.message</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"> <span class="kr">const</span> <span class="nx">clientToken</span> <span class="o">=</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span><span class="nx">postLogin</span><span class="p">,</span> <span class="nx">clientToken</span><span class="p">,</span> <span class="nx">loaderName</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="kd">function</span><span class="o">*</span> <span class="nx">watchLoginLocal() {</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"> <span class="k">yield</span> <span class="nx">takeEvery</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span><span class="nx">login</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="nx">onLoginLocal</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">sagas</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">watchLoginLocal</span> <span class="p">};</span>
</span></span></code></pre><p>This is what a common saga looks like in listifi:</p>
<ul>
<li>We start the loader</li>
<li>We make the request</li>
<li>We check if the request was successful</li>
<li>We extract and trasform the data</li>
<li>We save the data to redux</li>
<li>We stop the loader</li>
</ul>
<p>This is a painfully redundent process and any attempt to abstract the
functionality end up creating a large configuration object to accommodate all
use-cases. I have two other functions that do virtually the exact same thing:</p>
<p><a href="https://github.com/neurosnap/listifi/blob/20ad30e0b23e04614cb504788f736c257e8a34d1/src/auth/index.ts#L31" rel="nofollow">loginGoogle</a>,
and
<a href="https://github.com/neurosnap/listifi/blob/20ad30e0b23e04614cb504788f736c257e8a34d1/src/auth/index.ts#L111" rel="nofollow">register</a>.</p>
<p>Now let's see what it looks like with <code>saga-query</code>:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span><span class="o">*</span> <span class="nx">authBasic</span><span class="p">(</span><span class="nx">ctx</span>: <span class="kt">ApiCtx</span><span class="o"><</span><span class="p">{</span> <span class="nx">token</span>: <span class="kt">string</span> <span class="p">}</span><span class="o">></span><span class="p">,</span> <span class="nx">next</span>: <span class="kt">Next</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"> <span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"> <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">payload</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"> <span class="k">yield</span> <span class="nx">next</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span><span class="nx">postLogin</span><span class="p">,</span> <span class="nx">ctx</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">loginGoogle</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="nx">post</span><span class="p"><</span><span class="nt">AuthGoogle</span><span class="p">>(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"> <span class="s2">"/auth/login/google"</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"> <span class="nx">authBasic</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">login</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="nx">post</span><span class="p"><</span><span class="nt">LoginLocalParams</span><span class="p">>(</span><span class="s2">"/auth/login/local"</span><span class="p">,</span> <span class="nx">authBasic</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">register</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="nx">post</span><span class="p"><</span><span class="nt">RegisterParams</span><span class="p">>(</span><span class="s2">"/auth/register"</span><span class="p">,</span> <span class="nx">authBasic</span><span class="p">);</span>
</span></span></code></pre><p>Wow! I was able to completely abstract the request lifecycle logic into a single
function and then have <code>loginGoogle</code>, <code>login</code>, and <code>register</code> use it. How is
this possible? We're able to inject lifecycle hooks into our function by using
pre-built middleware: <code>requestMonitor</code> and <code>requestParser</code> which get registered
once for all endpoints
<a href="https://github.com/neurosnap/listifi/blob/5ae6930a9617cafbd5a9557d909c94c39c39d674/src/api/index.ts#L79-L83" rel="nofollow">here</a>.</p>
<h2 id="example-comment-logic"><a class="anchor" href="#example-comment-logic" rel="nofollow">#</a> Example: Comment logic</h2>
<p>Here's another example I came across when refactoring that was also a very
pleasent developer experience. I have logic to fetch comments not only for the
list but also for each list item in that list. The logic is very similar: fetch
the data and extract the comments to save them to redux. I have two functions:
<code>onFetchComments</code> and <code>onFetchListComments</code>.</p>
<ul>
<li><a href="https://github.com/neurosnap/listifi/blob/20ad30e0b23e04614cb504788f736c257e8a34d1/src/comments/index.ts" rel="nofollow">Before</a></li>
<li><a href="https://github.com/neurosnap/listifi/blob/5ae6930a9617cafbd5a9557d909c94c39c39d674/src/comments/index.ts" rel="nofollow">After</a></li>
</ul>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// I'm going to cut out the action creation and saga watch logic just to make
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// it easier to see the main differences.
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">function</span><span class="o">*</span> <span class="nx">onFetchComments</span><span class="p">({</span> <span class="nx">itemId</span><span class="p">,</span> <span class="nx">listId</span> <span class="p">}</span><span class="o">:</span> <span class="nx">FetchComments</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"> <span class="kr">const</span> <span class="nx">loaderName</span> <span class="o">=</span> <span class="nx">Loaders</span><span class="p">.</span><span class="nx">fetchComments</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">setLoaderStart</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"> <span class="kr">const</span> <span class="nx">res</span>: <span class="kt">ApiFetchResponse</span><span class="p"><</span><span class="nt">FetchListCommentsResponse</span><span class="p">></span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"> <span class="nx">apiFetch</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"> <span class="sb">`/lists/</span><span class="si">${</span><span class="nx">listId</span><span class="si">}</span><span class="sb">/items/</span><span class="si">${</span><span class="nx">itemId</span><span class="si">}</span><span class="sb">/comments`</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"> <span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">setLoaderError</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span><span class="p">,</span> <span class="nx">message</span>: <span class="kt">res.data.message</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"> <span class="kr">const</span> <span class="nx">comments</span> <span class="o">=</span> <span class="nx">processComments</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">comments</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"> <span class="kr">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="nx">processUsers</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">users</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"> <span class="k">yield</span> <span class="nx">batch</span><span class="p">([</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"> <span class="nx">setLoaderSuccess</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span> <span class="p">}),</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"> <span class="nx">addComments</span><span class="p">(</span><span class="nx">comments</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"> <span class="nx">addUsers</span><span class="p">(</span><span class="nx">users</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"> <span class="p">]);</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="kd">function</span><span class="o">*</span> <span class="nx">onFetchListComments</span><span class="p">({</span> <span class="nx">listId</span> <span class="p">}</span><span class="o">:</span> <span class="p">{</span> <span class="nx">listId</span>: <span class="kt">string</span> <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"> <span class="kr">const</span> <span class="nx">loaderName</span> <span class="o">=</span> <span class="nx">Loaders</span><span class="p">.</span><span class="nx">fetchListComments</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">setLoaderStart</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"> <span class="kr">const</span> <span class="nx">res</span>: <span class="kt">ApiFetchResponse</span><span class="p"><</span><span class="nt">FetchListCommentsResponse</span><span class="p">></span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"> <span class="nx">apiFetch</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"> <span class="sb">`/comments/</span><span class="si">${</span><span class="nx">listId</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"> <span class="p">);</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">
</span></span><span class="line"><span class="ln">35</span><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">setLoaderError</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span><span class="p">,</span> <span class="nx">message</span>: <span class="kt">res.data.message</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl"> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">
</span></span><span class="line"><span class="ln">40</span><span class="cl"> <span class="kr">const</span> <span class="nx">comments</span> <span class="o">=</span> <span class="nx">processComments</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">comments</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"> <span class="kr">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="nx">processUsers</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">users</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"> <span class="k">yield</span> <span class="nx">batch</span><span class="p">([</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl"> <span class="nx">setLoaderSuccess</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">loaderName</span> <span class="p">}),</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl"> <span class="nx">addComments</span><span class="p">(</span><span class="nx">comments</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl"> <span class="nx">addUsers</span><span class="p">(</span><span class="nx">users</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl"> <span class="p">]);</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>You can see that I tried to abstract as much as I could previously, but because
of subtle differences between the two functions, it didn't seem worth it to take
it much further. With <code>saga-query</code> it was clear how to improve these functions.</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span><span class="o">*</span> <span class="nx">basicComments</span><span class="p">(</span><span class="nx">ctx</span>: <span class="kt">ApiCtx</span><span class="p"><</span><span class="nt">FetchListCommentsResponse</span><span class="p">>,</span> <span class="nx">next</span>: <span class="kt">Next</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"> <span class="k">yield</span> <span class="nx">next</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"> <span class="kr">const</span> <span class="p">{</span> <span class="nx">data</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"> <span class="kr">const</span> <span class="nx">comments</span> <span class="o">=</span> <span class="nx">processComments</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">comments</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"> <span class="kr">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="nx">processUsers</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">users</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"> <span class="nx">ctx</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">addComments</span><span class="p">(</span><span class="nx">comments</span><span class="p">),</span> <span class="nx">addUsers</span><span class="p">(</span><span class="nx">users</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">fetchComments</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="kr">get</span><span class="p"><</span><span class="nt">FetchComments</span><span class="p">>(</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"> <span class="s2">"/lists/:listId/items/:itemId/comments"</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"> <span class="nx">basicComments</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">fetchListComments</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="kr">get</span><span class="o"><</span><span class="p">{</span> <span class="nx">listId</span>: <span class="kt">string</span> <span class="p">}</span><span class="o">></span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"> <span class="s2">"/comments/:listId"</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"> <span class="nx">basicComments</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">);</span>
</span></span></code></pre><p>Once again, by using a middleware system with <code>saga-query</code> I was able to cut out
a ton of repeated logic.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion" rel="nofollow">#</a> Conclusion</h2>
<p>This trend of being able to leverage a middleware system to remove duplicated
logic for every API interaction was common in this refactor which resulted in
less code and a better developer experience.</p>
<p><a href="https://github.com/neurosnap/saga-query" rel="nofollow">Visit the saga-query repo to learn more about how the middleware system works.</a></p>
<p>Have a comment? Start a discussion by sending an email to
<a href="mailto:~erock/public-inbox@lists.sr.ht" rel="nofollow">my public inbox</a>.</p>
Documenting journey to refactor listifi to use saga-querysaga-query initial release2022-07-05T13:45:08Zhttps://erock.prose.sh/saga-query<p>I'm excited to officially announce the initial release of a new library I wrote
called <a href="https://github.com/neurosnap/saga-query" rel="nofollow">saga-query</a>.</p>
<p>For the past month, most of my free time has been dedicated to building a new
way to manage data fetching using <code>redux</code> and <code>redux-saga</code>, primarily for
<code>react</code> apps. My goal for this library is to create a koa-like middleware system
for interacting with APIs using <code>redux-saga</code>.</p>
<p>There are a lot of common processes to fetching API data:</p>
<ul>
<li>Preparing the HTTP request</li>
<li>Setting loading states</li>
<li>Making the HTTP request</li>
<li>Processing the results</li>
<li>Storing the results</li>
</ul>
<p>On top of those common ETL processes, there's also UX-specific functionality
baked into it:</p>
<ul>
<li>Optimistic updates</li>
<li>Undo</li>
<li>Offline support</li>
<li>Error handling</li>
</ul>
<p>There's a lot going on here, not to mention the asynchronous nature of
javascript makes it even more complicated. Creating common abstractions that
work for the needs of every web app is difficult. It's difficult to construct
common abstractions even for a specific app's needs.</p>
<p>I love <a href="https://koajs.com" rel="nofollow">koa</a>, it's a simple web server written in node.js
that creates a robust middleware system that allows end-developers to manage API
requests and responses. I thought to myself: what if we did that same thing on
the front-end? What if we could create a middleware system using <code>redux-saga</code>
that handles all the ETL logic of interacting with an API? That idea culminated
in <code>saga-query</code>.</p>
<p>This library was ultimately inspired by
<a href="https://react-query.tanstack.com/" rel="nofollow">react-query</a>,
<a href="https://redux-toolkit.js.org/" rel="nofollow">redux-toolkit</a>, and <a href="https://koajs.com" rel="nofollow">koajs</a>.</p>
<p>Those libraries offer a ton of functionality for data fetching and caching.</p>
<p>The <code>redux</code> maintainers have done a great job moving the state management
ecosystem forward by creating common abstractions people can use to reduce
boilerplate. However, there are no common abstractions being built for people
using <code>redux-saga</code> which is my library of choice for medium to large sized
projects. <code>redux-toolkit</code> heavily prefers <code>redux-thunk</code> and has little interest
in supporting paradigms that revolve around sagas.</p>
<p><code>react-query</code> has made the distinction clear between UI state and API data for
state management. Data, they argue, should be automatically cached and not
something the end-developer needs to think about. I love this idea because
caching API data is a huge part of constructing a front-end application.
However, for medium to large sized projects, I've inevitably needed to control
the async flow control as well as the caching logic for my API data. It's not
uncommon to performance tune my selectors, build special indexes to make queries
faster, and to leverage memoization with <code>reselect</code> to squeeze performance out
of my data caching layer. I'm also not a fan of every piece of business logic
living inside of the view layer, <code>react</code>. I think with <code>react-query</code>, <code>react</code> is
consuming to entire stack and I find that to be less than idea. I want my
side-effects to be treated as data and separated from my view layer. For these
reasons and more, <code>react-query</code> is not something I would reach for unless it was
a small application.</p>
<blockquote>
<p>To learn more about why I prefer <code>redux-saga</code> and treating side-effects as
data, read my previous article on
<a href="https://erock.io/2020/01/01/redux-saga-style-guide.html" rel="nofollow">redux-saga style guide</a>
or
<a href="https://erock.io/2019/04/12/simplify-testing-async-io-javascript.html" rel="nofollow">simplify testing async I/O in javascript</a>.</p>
</blockquote>
<p>If you're like me, someone who prefers treating side-effects as data and wants
full control over the data synchronization of your application, I recommend
giving <a href="https://github.com/neurosnap/saga-query" rel="nofollow">saga-query</a> a try. This
library, coupled with <a href="https://github.com/neurosnap/robodux" rel="nofollow">robodux</a> will not
only reduce boilerplate, but give you total control over fetching and caching
logic using a koa-style middleware.</p>
<p><a href="https://github.com/neurosnap/saga-query" rel="nofollow">Visit the Github repo to learn more.</a></p>
<p>Have a comment? Start a discussion by sending an email to
<a href="mailto:~erock/public-inbox@lists.sr.ht" rel="nofollow">my public inbox</a>.</p>
Data fetching and caching library for redux-saga