<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[react-state-basis]]></title><description><![CDATA[react-state-basis]]></description><link>https://react-state-basis.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aa6d5c42a1ed9b6fe01f234/f8216021-01d6-40a9-938f-23badd4269e9.png</url><title>react-state-basis</title><link>https://react-state-basis.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 19 Sep 2026 21:37:33 GMT</lastBuildDate><atom:link href="https://react-state-basis.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[React DevTools tells you why a component rendered. This tells you what updated with it.]]></title><description><![CDATA[React DevTools, why-did-you-render, and React Scan answer a useful question: why did this component render?
They do not answer a different one that shows up in real apps:
What updated, what updated wi]]></description><link>https://react-state-basis.hashnode.dev/react-state-basis-runtime-diagnostics</link><guid isPermaLink="true">https://react-state-basis.hashnode.dev/react-state-basis-runtime-diagnostics</guid><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[devtools]]></category><category><![CDATA[performance]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Petar Liovic]]></dc:creator><pubDate>Sun, 13 Sep 2026 17:15:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa6d5c42a1ed9b6fe01f234/d49ce06a-f41c-4f77-836f-e7c76b6f2553.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>React DevTools, <a href="https://github.com/welldone-software/why-did-you-render">why-did-you-render</a>, and <a href="https://github.com/aidenybai/react-scan">React Scan</a> answer a useful question: <strong>why did this component render?</strong></p>
<p>They do not answer a different one that shows up in real apps:</p>
<p><strong>What updated, what updated with it, and what looks upstream?</strong></p>
<p>That is what <a href="https://github.com/liovic/react-state-basis">react-state-basis</a> is for.</p>
<p>It is a <strong>dev-time</strong> diagnostic. It records <em>when</em> state writes land - not the values - and flags repeated timing patterns: extra frames, correlated flags, context copies, and update fan-out.</p>
<p>Live playground: <a href="https://stackblitz.com/github/liovic/basis-live-demo">StackBlitz demo</a> - pick a scene, open the preview console.</p>
<h2>The problem it looks at</h2>
<p>This is legal React and still a common source of extra paints:</p>
<pre><code class="language-tsx">const [a, setA] = useState(0);
const [b, setB] = useState(0);

useEffect(() =&gt; {
  setB(a + 1);
}, [a]);
</code></pre>
<p>Click the button a few times. Basis typically reports:</p>
<pre><code class="language-text">⚡ BASIS | DOUBLE RENDER
📍 Location: YourComponent.tsx
Issue: effect_L5 triggers b in a separate frame.
Fix: Derive b during the render phase (remove effect) or wrap in useMemo.
</code></pre>
<p>That “Fix:” line is a <strong>prompt from a rolling frame window</strong>, not a proof. Repeat the interaction and see whether the pattern holds.</p>
<p>Same idea for flags that always move together (<code>isLoading</code> / <code>isSuccess</code> / <code>hasData</code>), local state that only mirrors Context, or one click that fans out across several stores.</p>
<h2>What “Basis” means</h2>
<p>The name is a linear-algebra metaphor: a basis is a minimal set of independent vectors. Here it means “state that looks like its own source of truth,” as opposed to values you could compute during render.</p>
<p>The library does <strong>not</strong> prove independence. It approximates it from timing and from a short-lived update graph.</p>
<h2>Quick start (Vite)</h2>
<p>You keep importing from <code>react</code>. A build plugin attaches names so reports are not full of anonymous hooks.</p>
<pre><code class="language-bash">npm i react-state-basis
</code></pre>
<p><code>vite.config.ts</code>:</p>
<pre><code class="language-ts">import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { basis } from 'react-state-basis/vite';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['react-state-basis/plugin']],
      },
    }),
    basis(),
  ],
});
</code></pre>
<p>Wrap the app:</p>
<pre><code class="language-tsx">import { BasisProvider } from 'react-state-basis';

root.render(
  &lt;BasisProvider debug={true} showHUD={true}&gt;
    &lt;App /&gt;
  &lt;/BasisProvider&gt;
);
</code></pre>
<p><code>showHUD={false}</code> keeps diagnostics in the console only.</p>
<h3>Next.js (experimental)</h3>
<p>App Router, <strong>webpack only</strong>. Turbopack does not support the SWC wasm plugin yet.</p>
<pre><code class="language-ts">// next.config.ts
experimental: {
  swcPlugins: [['react-state-basis/swc', {}]],
}
</code></pre>
<pre><code class="language-json">"dev": "next dev --webpack",
"build": "next build --webpack"
</code></pre>
<p>The provider must be a Client Component. Without <code>--webpack</code>, hooks still run but stay anonymous. <code>"use server"</code> files are not instrumented.</p>
<h2>Reports after you use the app</h2>
<p>With <code>debug={true}</code>:</p>
<pre><code class="language-js">window.printBasisReport()  // ranked “start here” list
window.printBasisGraph()   // observed update graph
window.getBasisGraph()     // same graph as JSON
window.getBasisMetrics()   // engine timings
</code></pre>
<p>Example graph from the playground:</p>
<pre><code class="language-text">📊 BASIS | CAUSAL GRAPH  7 nodes · 7 edges · 2 sources · buffer window 50
parent → child = observed cause → update. (×N) = times in this window.
⚡ Event · 3 targets · ×2
    BooleanEntanglement.tsx → isLoading redundant
    BooleanEntanglement.tsx → isSuccess redundant
    BooleanEntanglement.tsx → hasData redundant
↯ WeatherLab.tsx → effect @ L7
    WeatherLab.tsx → fahrenheit (×2)
</code></pre>
<p>An edge is something Basis <strong>saw in this window</strong>. It is not a proof of causality. <code>redundant</code> means “these writes kept landing together,” not “delete this state.”</p>
<p>Zustand stores can sit on the same graph:</p>
<pre><code class="language-ts">import { basisLogger } from 'react-state-basis/zustand';
</code></pre>
<h2>What it looks for</h2>
<table>
<thead>
<tr>
<th>Pattern</th>
<th>What Basis saw</th>
</tr>
</thead>
<tbody><tr>
<td>Effect-driven extra frame</td>
<td>An effect writes state after another update. If that value can be computed during render, the second paint may be unnecessary.</td>
</tr>
<tr>
<td>Correlated updates</td>
<td>Two pieces of state repeatedly move in the same window. Correlation is reported; merge is not assumed.</td>
</tr>
<tr>
<td>Fragmented updates</td>
<td>One interaction updates several components, contexts, or stores.</td>
</tr>
<tr>
<td>Context / store mirroring</td>
<td>A local hook repeatedly follows Context or a store.</td>
</tr>
<tr>
<td>Update origins</td>
<td>When several writes land together, the graph points at what looks upstream.</td>
</tr>
</tbody></table>
<p>Full list: <a href="https://github.com/liovic/react-state-basis/wiki/Detected-patterns">Detected patterns</a>.</p>
<h2>Signals, not proofs</h2>
<p>Detections use timing, correlation, update order, roles, and graph structure. Valid React can still look busy.</p>
<p>Intentional sync, drafts, animations, reducers, stores, and coordinated transitions can all produce hits. Use the signal with your knowledge of the app.</p>
<p>Ignore a whole file (must be the leading comment, nothing else on that line):</p>
<pre><code class="language-ts">// @basis-ignore
import { useState } from 'react';
</code></pre>
<p>Ignore one hook call:</p>
<pre><code class="language-ts">// @basis-ignore-next-line
const [ticks, setTicks] = useState(0);
</code></pre>
<p>That call is rewritten to the real React hook. Other calls in the same file stay instrumented.</p>
<h2>How the engine actually works</h2>
<p>Updates are sampled on <code>requestAnimationFrame</code>. Same tick means the same paint frame, not an arbitrary clock.</p>
<p>Each instrumented variable gets a fixed <code>Uint8Array</code> ring (~50 frames). A write in that frame is <code>1</code>; no write is <code>0</code>. Values are never stored.</p>
<p>Two views of the same trace:</p>
<ul>
<li><p><strong>Per variable</strong> - do these two timelines move together, or does one follow the other?</p>
</li>
<li><p><strong>Across the app</strong> - those pairwise links become edges. Walking the graph asks what sits upstream.</p>
</li>
</ul>
<p>Heavy work runs on <code>requestIdleCallback</code>. The hot path stays on fixed-size buffers.</p>
<p>Pair scoring (since 0.6.6) is not “cosine &gt; 0.88”. Cosine is display-only. A pair hits only if the overlap is rare under a hypergeometric null <strong>and</strong> the quieter timeline lands on the other one at least ~65% of the time.</p>
<p>Limits worth knowing:</p>
<ul>
<li><p>Timing and edges only - never values or dependency arrays</p>
</li>
<li><p>Sliding window of seconds, not the whole session</p>
</li>
<li><p>A late fetch can look like two unrelated updates</p>
</li>
<li><p>Same-frame coincidence can look related</p>
</li>
</ul>
<h2>Privacy and production</h2>
<ul>
<li><p>Records timing, roles, and update relationships - <strong>not state values</strong></p>
</li>
<li><p>Production entry is a small shim; monitoring is off</p>
</li>
<li><p>The Babel/SWC plugin only rewrites the hook and context imports it needs. Everything else still comes from <code>react</code></p>
</li>
</ul>
<h2>Real-codebase demos (not “we fixed these apps”)</h2>
<p>These show output on public code. A hit is not automatically a defect.</p>
<ul>
<li><p><a href="https://github.com/satnaing/shadcn-admin/pull/274">shadcn-admin #274</a> - redundant viewport state; merged</p>
</li>
<li><p><a href="https://github.com/excalidraw/excalidraw/pull/10637">Excalidraw #10637</a> - theme sync pattern; not merged</p>
</li>
</ul>
<h2>If you try it</h2>
<ol>
<li><p>Open the <a href="https://stackblitz.com/github/liovic/basis-live-demo">live demo</a></p>
</li>
<li><p>Trigger Weather Lab and Boolean Entanglement</p>
</li>
<li><p>Run <code>printBasisReport()</code> and <code>printBasisGraph()</code> in the console</p>
</li>
<li><p>Then wire the Vite plugin into one of your own apps and click through a real flow</p>
</li>
</ol>
<p>Repo, wiki, and roadmap:</p>
<p><a href="https://github.com/liovic/react-state-basis">https://github.com/liovic/react-state-basis</a></p>
<p>If you try it on your own app, I'd genuinely like to know what it flags - especially false positives, since those are the cases that improve the heuristics.</p>
]]></content:encoded></item></channel></rss>