<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.gsconrad.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.gsconrad.com/" rel="alternate" type="text/html" /><updated>2025-01-31T02:32:51+00:00</updated><id>https://blog.gsconrad.com/feed.xml</id><title type="html">Gregory Conrad’s Blog</title><subtitle>Gregory Conrad&apos;s personal blog.</subtitle><entry><title type="html">The Problem With State Management, and Why It Shouldn’t Be a Problem</title><link href="https://blog.gsconrad.com/2023/12/22/the-problem-with-state-management.html" rel="alternate" type="text/html" title="The Problem With State Management, and Why It Shouldn’t Be a Problem" /><published>2023-12-22T00:00:00+00:00</published><updated>2023-12-22T00:00:00+00:00</updated><id>https://blog.gsconrad.com/2023/12/22/the-problem-with-state-management</id><content type="html" xml:base="https://blog.gsconrad.com/2023/12/22/the-problem-with-state-management.html"><![CDATA[<blockquote>
  <p>This few-minute and bite-sized read is condensed from my recently-completed
<a href="/thesis.pdf">Master’s Thesis</a>.
Go take a look at it after you’re done here if you want to learn more!</p>
</blockquote>

<p>In case you haven’t noticed, there are more state management frameworks than atoms in the universe,
and there appears to be a new addition every day.
But state management should be easy, right?
Just update some state when a user interacts with a UI, and then update the UI accordingly.
Here’s the catch: the second you add new feature requirements into an application,
it isn’t such a clear cut problem and things can start to get tricky.</p>

<h2 id="background">Background</h2>
<p>To handle these trickier scenarios,
many solutions available today do just one or two things <em>really well</em>.
For example, BLoC is great at separating complex state logic from UI code via the reducer pattern.
Riverpod, Signals, and Recoil are fantastic in regards to data modeling and data flow,
at least once you wrap your head around the whole reactive paradigm.
Although not ideal for state management (which would be a whole blog post on its own),
streams are still great at representing transformations on individualized and discrete data.
If you are familiar with it,
<code class="language-plaintext highlighter-rouge">ValueNotifier</code> is perfect when you have a single mutable variable
whose mutations need to be reflected in your UI.
GetX is, well, an exception to this pattern, as it does so many things and none of them well.</p>

<h2 id="the-problem">The Problem</h2>
<p>So why do we have all of these libraries?
Short answer: it’s because they all fill a different need,
and only specialize in that one area or two.
While this makes them great and highly specialized
for the specific problem they were designed to solve,
this comes with the drawback that they are pretty unergonomic for everything else.</p>

<p>As an example, BLoC introduced cubits because normal blocs
are almost always too verbose for simple state/UI relationships.
While Riverpod readily supports data compostion and caching,
<a href="https://github.com/rrousselGit/riverpod/issues/1032">data persistence</a>
and <a href="https://github.com/rrousselGit/riverpod/issues/1660">data mutations</a>
have been open issues for several years now,
and both are common requirements in offline-first and networked applications.</p>

<h2 id="the-solution">The Solution</h2>
<blockquote>
  <p>This section will briefly go into a bit of some theory here, so bear with me.</p>
</blockquote>

<p>When you are building an application, you should normally start from a singular
target, goal, or idea in mind.
Then, you break down that one overarching concept into a set of feature requirements.
Breaking those feature requirements down further,
you will arrive at smaller and more individualized feature requirements.
This process repeats until you eventually get down to the code itself,
at which point you start to implement individual features.</p>

<p>Here’s the key observation: this entire process actually forms a tree!
As such, we can say that <em>features assemble into trees</em>
(and this concept is really important for the rest of this post).
Perhaps even more important is that many of these features have commonalities
that are shared across subtrees,
which opens the door for high code reuse.</p>

<blockquote>
  <p>As an aside, I often find that when I program,
the closer I keep some code to its underlying theory, the cleaner the resulting code is;
inheritance (when used over composition) and unwarranted abstractions often result in a mess.</p>
</blockquote>

<h3 id="features-as-trees">Features as Trees</h3>
<p>Getting back to reality here, what do “features as trees” look like,
or even mean for that matter?</p>

<p>Let’s take an application that needs to perform a simple network request (say an HTTP GET).
This alone sounds like a pretty simple requirement!
Now how about we need to add in a cache for this network request.
We may need to put some thought here into orchestrating the requests and caching,
but this should be doable.
But wait! Our customer now wants to be able to perform POST requests too,
so we’ll need to also add invalidation.
How do we manage all of these competing requirements?
And as a thought experiment–think of all the moving pieces you may need
in your own state management solution of choice to implement this.</p>

<p>In our particular example, the overarching feature is having some best-effort data
always available to an application user,
which itself is a <em>composition</em> over the underlying (and independent)
network request, caching, and invalidation features.</p>

<h3 id="composition">Composition</h3>
<p>For those unfamiliar with Component-Based Software Engineering,
the general idea is that a developer can produce independent <em>components</em>
that can later be <em>assembled</em> into a full application.
Composition in this manner results in loosely coupled applications
and <a href="https://dl.acm.org/doi/10.5555/379381">significantly increases code quality</a>.</p>

<p>While global/application-level state and logic is assembled into a dependency graph<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>,
individual application features form a tree.
As such, in our networking example above,
we can assemble the individual features (network request, cache, and invalidation)
together into a tree.</p>

<h3 id="in-code">In Code</h3>
<p>Let’s again consider our network + cache + invalidation example.
To support the overarching feature (displaying highly-available best-effort data to our users),
we ideally want to be able to assemble the individual features together
with something like the following pseudo-Dart:</p>
<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">({</span><span class="n">MyData</span> <span class="kt">Function</span><span class="p">()</span> <span class="n">getState</span><span class="p">,</span> <span class="kt">void</span> <span class="kt">Function</span><span class="p">()</span> <span class="n">invalidateState</span><span class="p">})</span> <span class="n">myFancyFeature</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">networkClient</span> <span class="o">=</span> <span class="n">networkFeature</span><span class="p">(</span><span class="n">yourApiUrl</span><span class="p">);</span>
  <span class="kd">final</span> <span class="p">(</span><span class="n">getNetworkState</span><span class="p">,</span> <span class="n">invalidateNetworkState</span><span class="p">)</span> <span class="o">=</span> <span class="n">invalidationFeature</span><span class="p">(</span><span class="n">networkClient</span><span class="p">);</span>
  <span class="kd">final</span> <span class="p">(</span><span class="n">persistedState</span><span class="p">,</span> <span class="n">setPersistedState</span><span class="p">)</span> <span class="o">=</span> <span class="n">cacheFeature</span><span class="p">(</span>
    <span class="nl">read:</span> <span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">readFromDatabaseOfYourChoice</span><span class="p">(),</span>
    <span class="nl">write:</span> <span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">writeToDatabaseOfYourChoice</span><span class="p">(</span><span class="n">data</span><span class="p">),</span>
  <span class="p">);</span>

  <span class="k">return</span> <span class="p">({</span>
    <span class="nl">getState:</span> <span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">persistedState</span> <span class="o">??</span> <span class="n">getNetworkState</span><span class="p">()</span><span class="o">.</span><span class="na">then</span><span class="p">(</span><span class="n">setPersistedState</span><span class="p">),</span>
    <span class="nl">invalidateState:</span> <span class="p">()</span> <span class="p">{</span>
      <span class="n">invalidateNetworkState</span><span class="p">();</span>
      <span class="n">setPersistedState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
    <span class="p">},</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>See how easy that problem gets once we introduce composition?
<em>Magic</em>.</p>

<h4 id="that-pseudocode-is-great-but-how-do-we-actually-do-that">“That Pseudocode Is Great, but How Do We Actually Do That?”</h4>
<p>I am only aware of two approaches that will let you write code with feature-focused composition<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</p>

<p>The first one you may have heard of: Hooks!
Aside from the commonly-discussed benefits like automatically managed
text editing and animation controllers,
hooks shine due to their easy composability.</p>

<p>There’s just one big issue with hooks:
they only work within widgets for <a href="https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app">ephemeral state</a>.</p>

<p>And that is where we come to our next approach: <a href="https://github.com/gregoryconrad/rearch-dart/">ReArch</a>.</p>

<blockquote>
  <p>Disclaimer: I am the author of ReArch.
While I would love ReArch to get some exposure as a result of this post,
a strong motivation behind this written piece is my interest in the “meta” of software engineering.</p>
</blockquote>

<p>ReArch enables feature-focused composition for both ephemeral and app state via <em>side effects</em>,
which are named as such because they provide applications with mutability
and a mechanism to interact with the outside world.
With ReArch’s side effects model, applications are also given high code reuse
and knowledge transference between all application layers <em>for free</em>.</p>

<p>Finally, here is one way we can implement our above example via feature composition
for some application-level state in ReArch:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// This function is a "capsule", which represents a piece of app state.</span>
<span class="c1">// See more here: https://rearch.gsconrad.com/core/capsules</span>
<span class="p">({</span>
  <span class="n">AsyncValue</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="kt">Function</span><span class="p">()</span> <span class="n">getData</span><span class="p">,</span>
  <span class="kt">void</span> <span class="kt">Function</span><span class="p">()</span> <span class="n">invalidateData</span><span class="p">,</span>
<span class="p">})</span> <span class="n">myCachedNetworkedDataCapsule</span><span class="p">(</span><span class="n">CapsuleHandle</span> <span class="n">use</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="p">(</span><span class="n">persistedState</span><span class="p">,</span> <span class="n">persistData</span><span class="p">)</span> <span class="o">=</span> <span class="n">use</span><span class="o">.</span><span class="na">persist</span><span class="p">(</span>
    <span class="nl">read:</span> <span class="n">readFromDatabase</span><span class="p">,</span>
    <span class="nl">write:</span> <span class="n">writeToDatabase</span><span class="p">,</span>
  <span class="p">);</span>
  <span class="kd">final</span> <span class="p">(</span><span class="n">getNetworkState</span><span class="p">,</span> <span class="n">invalidateNetworkRequest</span><span class="p">)</span> <span class="o">=</span> <span class="n">use</span><span class="o">.</span><span class="na">invalidatableFuture</span><span class="p">(</span>
    <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
      <span class="kd">final</span> <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">fetchFromNetwork</span><span class="p">();</span>
      <span class="n">persistData</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">data</span><span class="p">;</span>
    <span class="p">},</span>
  <span class="p">);</span>
  <span class="kd">final</span> <span class="n">runTransaction</span> <span class="o">=</span> <span class="n">use</span><span class="o">.</span><span class="na">transactionRunner</span><span class="p">();</span> <span class="c1">// more on this in a second</span>

  <span class="k">return</span> <span class="p">(</span>
    <span class="nl">getData:</span> <span class="p">()</span> <span class="p">{</span>
      <span class="k">return</span> <span class="k">switch</span> <span class="p">(</span><span class="n">persistedState</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// If we have some form of useful data, let's use it</span>
        <span class="c1">// instead of going to the network.</span>
        <span class="n">AsyncData</span><span class="p">&lt;</span><span class="kt">int</span><span class="o">?</span><span class="p">&gt;(</span><span class="o">:</span><span class="kd">final</span> <span class="n">data</span><span class="p">)</span> <span class="n">when</span> <span class="n">data</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">AsyncData</span><span class="p">(</span><span class="n">data</span><span class="p">),</span>

        <span class="c1">// If we still don't know what our new persisted data status is,</span>
        <span class="c1">// let's wait a bit before falling back to the network.</span>
        <span class="n">AsyncLoading</span><span class="p">&lt;</span><span class="kt">int</span><span class="o">?</span><span class="p">&gt;(</span><span class="nl">previousData:</span> <span class="n">Some</span><span class="p">(</span><span class="o">:</span><span class="kd">final</span> <span class="n">value</span><span class="p">))</span>
            <span class="n">when</span> <span class="n">value</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">=</span><span class="p">&gt;</span>
          <span class="n">AsyncLoading</span><span class="p">(</span><span class="n">Some</span><span class="p">(</span><span class="n">value</span><span class="p">)),</span>
        <span class="n">AsyncLoading</span><span class="p">&lt;</span><span class="kt">int</span><span class="o">?</span><span class="p">&gt;()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="kd">const</span> <span class="n">AsyncLoading</span><span class="p">(</span><span class="n">None</span><span class="p">()),</span>

        <span class="c1">// If persisting error-ed and/or we don't have any useful data,</span>
        <span class="c1">// we need to resort to the network.</span>
        <span class="n">_</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">getNetworkState</span><span class="p">(),</span>
      <span class="p">};</span>
    <span class="p">},</span>

    <span class="c1">// "runTransaction" allows you to update multiple side effects at once,</span>
    <span class="c1">// resulting in only a singular build.</span>
    <span class="nl">invalidateData:</span> <span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">runTransaction</span><span class="p">(()</span> <span class="p">{</span>
          <span class="n">invalidateNetworkRequest</span><span class="p">();</span>
          <span class="n">persistData</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
        <span class="p">}),</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>ReArch is also the subject of my Master’s Thesis,
and I have spent the better part of a year honing and improving it<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>.
If you are familiar with Hooks/Riverpod/Signals/Recoil,
ReArch should feel exceedingly natural to you.
ReArch also supports the reducer pattern that is employed by BLoC
for particularly complex state relations.
Even then, if you would prefer a more traditional/OOP-oriented approach to application development,
you have my full endorsement to use your own approach to feature composition
coupled with some form of dependency inversion!
You may just eventually find that easy composition and reactivity is a nice touch,
and ReArch will be waiting for you then. 😉</p>

<h2 id="conclusion">Conclusion</h2>
<p>The problem with state management, as I have outlined it above,
is the lack of feature-focused composition being readily available,
which causes every library to reinvent the wheel every time users request new and common features.
Thus, the solution is simple: develop a minimal framework for composition
that users can extend themselves to develop their own features with ease.
I present ReArch for this purpose, but hooks are also sufficient
(at least just for ephemeral state).</p>

<!-- Footnotes -->
<p><br /></p>

<hr />
<p><br />
<!-- footnotes will be rendered here --></p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Even if you are not using something like Riverpod/Signals/Recoil, dependency inversion techniques and libraries like Guice actually do result in dependency graphs. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Some readers may suggest the use of mixins; I’d advise against this. In addition to relying on inheritance over true composition, mixins only support a <em>single</em> usage per <code class="language-plaintext highlighter-rouge">with MyMixin</code>, which breaks the whole “feature tree” idea (say your component now needs to persist two unrelated values–you’re out of luck since you can’t do <code class="language-plaintext highlighter-rouge">with MyMixin, MyMixin</code>). <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>You may have seen the <a href="https://www.reddit.com/r/FlutterDev/comments/16n446w/announcing_rearch_a_reimagined_way_to/">original 1.0.0 announcement</a> a few months ago. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[This few-minute and bite-sized read is condensed from my recently-completed Master’s Thesis. Go take a look at it after you’re done here if you want to learn more!]]></summary></entry></feed>