<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>✨ EFLx ☁️</title><description>✨ EFL&apos;s personal blog. ☁️</description><link>https://eflx.top</link><item><title>First Principles of UI Composition</title><link>https://eflx.top/blog/ui-composition-first-principle</link><guid isPermaLink="true">https://eflx.top/blog/ui-composition-first-principle</guid><description>This is the valuable insight I&apos;ve gained regarding avoiding UI sloppiness...</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you are vibe-coding user interfaces and you are a layperson when it comes to UI/UX, to keep your app from looking like slop, just feed this article to your agents.&lt;/p&gt;
&lt;p&gt;There are four levels of components.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Level I: the headless components. They handle all the low-level, tedious, and laborious tasks—the core behavior of a component, its DOM positioning, accessibility, and mobile adaptation (not stylistic adjustments, but things like &quot;Can the context menu be brought up by long-pressing on mobile?&quot;). They don&apos;t concern themselves with how the component looks visually; if you use them directly, you&apos;ll only get a complex API and an unstyled appearance.&lt;/li&gt;
&lt;li&gt;Level II: the design system components, like shadcn/ui. These components form the cornerstone of the entire UI design system, e.g. buttons and cards. They are primarily wrappers around Level I headless components, adding styles that ensure consistency across all components and providing multiple variants.&lt;/li&gt;
&lt;li&gt;Level III: the layout components. These components aren&apos;t necessarily related to the first two levels, but they abstract the layouts commonly used in your app. Extracting layout components rather than having them scattered throughout the code improves code maintainability and readability. However, this is not a must.&lt;/li&gt;
&lt;li&gt;Level IV: the consumer components. These are the components that actually use design system components and layout components to directly build your user interface.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To make your UI code readable and maintainable, here are five minimal rules.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use shadcn/ui to &lt;a href=&quot;https://ui.shadcn.com/create&quot;&gt;create&lt;/a&gt; a project. Unless you have a solid grasp of the ins and outs, don&apos;t mess with it since it&apos;s the best and most streamlined architecture you&apos;ll ever come across.&lt;/li&gt;
&lt;li&gt;When you need to modify a component&apos;s styling, think about your design system. If you&apos;re completely new to this, just go to https://ui.shadcn.com/create I linked above. It offers simple options that even a novice can use, yet they&apos;re powerful enough to create a wide variety of interface styles. Click Get Code – Existing Project to send your customized presets to your agents. Please keep in mind that this is a rare opportunity to experiment with styling while avoiding a sloppy look. Opportunities like this really don&apos;t come along very often. Thank shadcn[^1].&lt;/li&gt;
&lt;li&gt;If you&apos;re determined to do so, or if your agents have convinced you to, you can (allow them to) modify the component styles in shadcn/ui. For experienced developers, shadcn/ui actually encourages this; that&apos;s precisely why it&apos;s distributed as copyable and modifiable code.&lt;/li&gt;
&lt;li&gt;Keep components at each level separate. Keep design system components in &lt;code&gt;components/ui&lt;/code&gt;, layout components in &lt;code&gt;components/layout&lt;/code&gt;, and consumer components organized within your feature or page modules. As your project grows rapidly, there&apos;s no need to rush to strictly extract layout components, but doing so from time to time is very helpful in making you realize just how much your app resembles slop, so you can then correct it. If you create a button with a new style in a consumer component, be careful. You(r agent) should be writing a design system component according to the standards for design system components and reuse it.&lt;/li&gt;
&lt;li&gt;When using components, strictly adhere to the &quot;composition&quot; advocated by the design system components. For example, place individual fields inside a &lt;code&gt;FieldGroup&lt;/code&gt; rather than using a single &lt;code&gt;div&lt;/code&gt; and writing a bunch of Flexbox styles. Consider props like &lt;code&gt;variant&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, etc. before writing &lt;code&gt;className=&lt;/code&gt;. If you create design system components other than those from shadcn/ui, please ensure that they also provide out-of-the-box compositions, just like the shadcn/ui components. Your agents are good at mimicking; have them mimic shadcn/ui.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The above assumes you are using Tailwind CSS. If you aren&apos;t, you likely won&apos;t be able to use shadcn/ui either, but the underlying principles remain the same, and your agents will have no trouble understanding them.&lt;/p&gt;
&lt;p&gt;The above assumes you are developing an application in React. If you aren&apos;t, the underlying principles remain the same, and your agents will have no trouble understanding them.&lt;/p&gt;
&lt;p&gt;The above assumes you know absolutely nothing about UI design but still have some aspirations in that area. If that&apos;s not the case, thank you for listening and for your laughter.&lt;/p&gt;
&lt;p&gt;[^1]: In addition to shadcn/ui, there are other excellent similar projects out there. If you or your agents are familiar enough with them, it&apos;s entirely possible to replace it!&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>After Safe &amp; Consistent SWR</title><link>https://eflx.top/blog/after-safe-consistent-swr/zh-hans</link><guid isPermaLink="true">https://eflx.top/blog/after-safe-consistent-swr/zh-hans</guid><description>Some reflections, but haven&apos;t changed anything...</description><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前段时间，我写了 &lt;a href=&quot;/blog/safe-consistent-swr&quot;&gt;Safe &amp;#x26; Consistent SWR&lt;/a&gt; 这篇文章，介绍了在纯客户端的 React 应用中，通过 SWR 或类似库进行数据获取的一种范式。
在实践了这种模式两个月后，这一范式的问题也逐渐浮现，并值得理性看待。&lt;/p&gt;
&lt;h2&gt;事实来源的混乱&lt;/h2&gt;
&lt;p&gt;随着业务逻辑越来越复杂，数据的“scope”或“endpoint”会越来越多。你需要处理用户、项目、帖子等不同范围的数据和业务逻辑。这时，问题随之显现：
SWR 模式通过 Key 来识别某种数据，并在此基础上构建缓存、去重、突变等功能。但是，十个 Endpoint 意味着十个 Key 定义，二十个 Endpoint 意味着二十个 Key 定义。
当规模开始增大时，你会开始思考减少模板代码。让我们看看：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在后端，我们通过 &lt;code&gt;/api&lt;/code&gt; 或 Electron IPC 或别的什么方式，暴露了各种 CRUD 接口。&lt;/li&gt;
&lt;li&gt;在前端 SWR 层，我们为每个 CRUD 接口定义了 Key、获取数据的 SWR 钩子和突变数据的 SWR 钩子和突变数据的钩子。&lt;/li&gt;
&lt;li&gt;在前端组件中，我们使用了这些钩子。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;貌似没有什么地方可以精简。显然，SWR 层需要手动建立 Key 与后端数据之间的关系，手写某个 CRUD 操作会改变哪些数据。这是令人不适的根源。&lt;/p&gt;
&lt;p&gt;一个后端操作会改变什么，这件事归根结底只有后端知道。
因此，把“赋予数据 Endpoint、将变更操作与数据突变绑定”这样的任务交给前端，是不合逻辑的。后端才是这些对应关系的事实来源。&lt;/p&gt;
&lt;h2&gt;tRPC？Server Action？GraphQL？&lt;/h2&gt;
&lt;p&gt;我首先想到了 &lt;a href=&quot;https://github.com/jsonnull/electron-trpc&quot;&gt;electron-trpc&lt;/a&gt;。也许这有些倒反天罡，对于一般的 &lt;code&gt;/api&lt;/code&gt; 服务器，
tRPC 也是可用的方案。然而，这样会带来极大的重构成本，将后端切换到耦合度低的设计时（e.g.，运行 Go 或 Python 服务）更有风险。&lt;/p&gt;
&lt;p&gt;那 Server Action 如何呢？我必须坦言，这样做的 DX 是非常好的。在一个“全栈”的 React 应用中，使用 RSC + Server Action 等原语并不是洪水猛兽，
事实上这些东西就像米其林餐厅里十万镑一勺的甜点，遇到一些问题时小用一下 RSC 给人的感觉非常舒适和优雅。
然而，我们不仅正在讨论具备复杂数据模型的应用——在这种情况下，Server Action 不见得好到哪去——而且，我们还在讨论纯客户端应用的解决方案。&lt;/p&gt;
&lt;p&gt;既然后端要保持纯粹，又需要存在一个中间层，这让我们想到了 Codegen。正好，drizzle-orm 还允许我们生成 zod schema，一举两得！在这里，codegen 的意思是
根据 Schema 生成 SWR hooks。其实这个想法蛮好的，但是并没有解决突变关联的问题，也就是一个数据变更操作会引发哪些 Key 突变的问题。&lt;/p&gt;
&lt;p&gt;最后我们貌似只有 GraphQL 之类的选项了。当我提到 GraphQL 时，它正处于一个很尴尬的境地：如果应用已经很复杂，这意味着和 tRPC 一样高的重构成本。
如果应用本身是 toy 级别的，为什么还要这么做呢？为了解决一点点过度设计的问题，我们引入了一个巨大的过度设计的设计……这不好。&lt;/p&gt;
&lt;h2&gt;略微迷茫&lt;/h2&gt;
&lt;p&gt;因此，经过许久的思想实验之后，我其实没有对原有架构做出任何足够的改进。不过，在过去两个月里，我已经尝试了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Service Locator 架构：将 SWR hooks 分类放进“service”对象中，所有 services 以 React Context 形式提供。&lt;/li&gt;
&lt;li&gt;一些减少模板代码的 utility，可以快速封装 IPC 函数或者 fetch &lt;code&gt;/api/&lt;/code&gt; 调用。&lt;/li&gt;
&lt;li&gt;在旧文章中，我采用基于 &lt;code&gt;{ endpoint: ..., /* other params */ }&lt;/code&gt; 的 Key 形状。但我发现 TanStack Query 鼓励的基于数组的 Key 甚至可能更好。我曾经假设我有耐心精心控制各个 key 的 mutate，最后发现，当你拥有“所有模型”、“用户添加的模型”、“用户固定的模型”、“真正的所有模型”、“模型列表的所有模型”这些乱七八糟的 Endpoint 之后，你只会在 mutate 里面一网打尽。我正在试验，并且感受到了 &lt;code&gt;[&apos;models&apos;, &apos;pinned&apos;]&lt;/code&gt; 而非 &lt;code&gt;{ endpoint: &apos;pinnedModels&apos; }&lt;/code&gt; 的正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前来看，虽然 SWR 层的代码在前端看起来非常庞大，但是它的运行仍然是稳定可靠的。加入一个新功能时，也只需要无脑复制一些模板即可。
未来，我可能会发布 Safe &amp;#x26; Consistent SWR / Query 的第二版，包含所有更新的最佳实践。就目前为止，我依然认为这样做是最可靠、最有效的数据获取解决方案。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Stateless Agent Matters</title><link>https://eflx.top/blog/stateless-agent-matters</link><guid isPermaLink="true">https://eflx.top/blog/stateless-agent-matters</guid><description>The Agent pattern, which has long been overlooked.</description><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The moment the term &lt;strong&gt;Agent Harness&lt;/strong&gt; was coined, it meant the concept had entered the realm of the &quot;named&quot;.
Once a practice is named—once people realize that existing language cannot precisely describe it and thus borrow a new term—it begins its transition from a &quot;niche&quot; practice to a &quot;best practice&quot;.&lt;/p&gt;
&lt;p&gt;I still remember the days when people would scramble to pack source code into long-form text,
frantically pasting it into the ChatGPT chat box.
Even now, amidst the dread of Claude Code launching ten rounds of an Explore Agent,
I sometimes copy and paste simple code problems into &lt;a href=&quot;https://aistudio.google.com&quot;&gt;Google AI Studio&lt;/a&gt;
just to get a blissfully innocent response.&lt;/p&gt;
&lt;p&gt;Regardless, the debate over whether reasoning models require Prompt Engineering has ended.
A set of well-crafted prompts and tool schemas can grant a model extraordinary capabilities,
and this—in a quiet, unobtrusive way—is becoming the consensus.&lt;/p&gt;
&lt;h2&gt;Wait. So what is a Stateless Agent?&lt;/h2&gt;
&lt;p&gt;My perspective originates from a strange sensation regarding &quot;Memory&quot;.
We first saw memory features in ChatGPT and Google Gemini;
later, Claude Code added the so-called &lt;code&gt;MEMORY.md&lt;/code&gt;.
Some LLM clients, like &lt;a href=&quot;https://alma.now/&quot;&gt;alma&lt;/a&gt;, provided impressive memory functions.
On April 16, the &lt;a href=&quot;https://openai.com/index/codex-for-almost-everything/&quot;&gt;new version of OpenAI Codex&lt;/a&gt; also introduced memory capabilities.&lt;/p&gt;
&lt;p&gt;Actually, the definition of memory is quite broad. Most approaches are relatively &quot;primitive&quot;, essentially revolving around one or more text files that the LLM summarizes and updates in the background. There are also &quot;academic&quot; efforts, such as &lt;a href=&quot;https://mem.nowledge.co/&quot;&gt;Nowledge Mem&lt;/a&gt;, which constructs a vast array of concepts around the aggregation and distillation of memory.&lt;/p&gt;
&lt;p&gt;But I started to ponder a question: What is the role of memory in the LLM workflow?&lt;/p&gt;
&lt;p&gt;When we ask an LLM to work on a codebase, the process generally looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The human provides instructions or a goal, perhaps along with constraints and a process. At this point, the model possesses a portion of the context.&lt;/li&gt;
&lt;li&gt;The Agent gathers additional context through various means until the context is large enough to reach a certain threshold.&lt;/li&gt;
&lt;li&gt;The Agent begins to think about how to proceed, and the context expands further to the next threshold.&lt;/li&gt;
&lt;li&gt;Finally, the Agent builds the solution.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Viewed this way, Memory is just a form of context. Context also includes other code in the repository, the Agent&apos;s inquiries to the user, and so on.&lt;/p&gt;
&lt;p&gt;In communication, people are mainly bothered by two things: the other party&apos;s lack of comprehension, and a lack of &quot;tacit understanding&quot; (filling in the missing context). Because LLMs aren&apos;t powerful enough yet, redundant context is often used to compensate. This has led to &quot;spec-driven vibe coding&quot;—a programming method that relies on a large volume of descriptions (whether handwritten or AIGC) to ensure the output of vibe coding aligns with the intent. Similarly, people want &quot;tacit understanding,&quot; or to maintain consistency, or believe that diachronic accumulation leads to progress. Thus, Memory was introduced.&lt;/p&gt;
&lt;p&gt;One could say both approaches are quite correct; practice has proven they yield results. But the problem is: these practices are intended to make an otherwise potentially incorrect result (the product of vibe coding) correct. Therefore, there is more than one way to ensure correctness; furthermore, the real task should be finding the &lt;em&gt;optimal&lt;/em&gt; way to ensure that potentially incorrect results become correct.&lt;/p&gt;
&lt;p&gt;This is easy to understand: if we only care about whether the LLM outputs correct code, we could just provide increasingly detailed Plans and Specifications, eventually telling it exactly how to write the code. The LLM then merely becomes a tool-caller, responsible for applying that code to the repository.&lt;/p&gt;
&lt;p&gt;I know this &quot;limit&quot; (in the mathematical sense) might seem nonsensical, but it proves a point: we should seek &lt;em&gt;effective&lt;/em&gt; methods, and simply &quot;making the LLM&apos;s output correct&quot; does not necessarily mean effective.&lt;/p&gt;
&lt;p&gt;Robert Englander wrote in &lt;a href=&quot;https://robenglander.com/writing/engineering-alignment/&quot;&gt;Engineering Alignment in Probabilistic Generation&lt;/a&gt; that the goal of software engineering has always been correctness. LLMs have changed how errors occur. In a probabilistic generation process, correctness degrades at the boundaries between different levels (from Intent to Spec, from Spec to CoT, from CoT to code generation). This degradation manifests as subtle semantic shifts, the filling of implicit assumptions, and the compression of ambiguity. Unlike explicit errors in traditional systems, an LLM-generated system might compile successfully, pass all tests, and run smoothly, yet still have semantically drifted from the original intent.&lt;/p&gt;
&lt;p&gt;In that article, the author proposes five planes and emphasizes governing the boundaries between them. But I want to look at this from another perspective. The ontology of an LLM lies in predictive generation, which dictates that n-shot is the most powerful way to influence it. In other words, the content generated by an LLM largely cannot be measured by &quot;correctness.&quot; Correctness does not exist &lt;em&gt;within&lt;/em&gt; the LLM; it exists &lt;em&gt;within the context&lt;/em&gt;. A certain context implies a certain probability distribution of outputs. Relying solely on the &quot;extreme pressure&quot; of context cannot make an LLM 100% generate a specific output, nor does it guarantee that the low-probability &quot;hallucinations&quot; in the distribution will be more &quot;correct.&quot;&lt;/p&gt;
&lt;p&gt;Correctness doesn&apos;t stem from &quot;engineering discipline&quot; either. Specifications don&apos;t actually manufacture correctness; they just shift from one error to another, trending toward so-called correctness through a spiral of development. Every &quot;correct&quot; state is actually an &quot;error&quot; relative to the previous harness, and this series does not converge.&lt;/p&gt;
&lt;p&gt;If we analyze further, human intent itself carries uncertainty, and there is always a &quot;remnant of the Real&quot; that cannot be described by language. This view might not be popular in software engineering, or even in the natural sciences. But if it is true—and I believe it is—then even without LLM assistance, code written by humans is bound to be not entirely correct.&lt;/p&gt;
&lt;p&gt;Therefore, my conclusion is: Correctness is located neither in the LLM nor in the Specification. It resides in the collective effect of all contexts and may also rely on the psyche of the Reviewer.&lt;/p&gt;
&lt;p&gt;If this is true, what does it mean? First, we can no longer pin our hopes for correctness on providing a complete, non-deviating intent. This is why I propose the distinction between Stateful and Stateless context. When we write a Specification or introduce memory into the context, we are actually performing a derivation of our intent. This is why I call it &quot;stateful.&quot; Memory is derived from historical records; the Spec is derived from the developer’s intent. The Agent no longer thinks in terms of the source code, but instead obeys the state represented by the Spec. In other words, developers are still &lt;strong&gt;almost&lt;/strong&gt; programming manually—only the language has shifted from a programming language to natural language—and they must rely on various constraints to compensate for the limitations of natural language. Moving from Intent to Spec creates a semantic drift at the boundary. When using Memory, one similarly sacrifices the &quot;Markovian&quot; nature of the &quot;here-and-now&quot; intent, hoping the model gains an understanding of the intent&apos;s perimeter through historical data.&lt;/p&gt;
&lt;p&gt;This is what I find strange.&lt;/p&gt;
&lt;p&gt;I believe the context should strictly consist only of my description of intent, and the LLM &lt;strong&gt;should be trained&lt;/strong&gt; to possess this capability: it should be able to identify whether the current context allows it to write &lt;em&gt;sufficiently&lt;/em&gt; correct (rather than &lt;em&gt;perfectly&lt;/em&gt; correct) code. If it is insufficient, it should use tools like Explore, Ask User, or something like &lt;a href=&quot;https://context7.com/&quot;&gt;context7&lt;/a&gt; to further acquire context until it is &lt;em&gt;confident&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Why is this the right way? Clearly, people have placed too much focus on &quot;state.&quot; The LLM is a black box, but people unconsciously assume it is a black box like a function-where if the input is precise enough, the output will be too. However, the LLM is essentially a Schrödinger-like black box. When this nature surfaces, people start adjusting the input, increasing precision, or introducing diachronicity—adding twelve steps where each step is &quot;precisely&quot; defined, or maintaining a memory file that constantly rolls and updates.&lt;/p&gt;
&lt;p&gt;This is actually a &lt;em&gt;neurotic&lt;/em&gt; behavior—I say this without malice or intent to attack, because according to psychoanalytic theory, everyone is structured as a neurotic, psychotic, or perverse, may also include autism (categories still debated). The point of highlighting this is to show that such behavior serves to satisfy one&apos;s own psychological energy; it aligns with the unconscious desire to be &quot;a bit more precise,&quot; &quot;a bit more comfortable,&quot; or to &quot;question, overrule, and rebuild.&quot; This doesn&apos;t mean my proposed method is free of such factors, but when &quot;satisfaction&quot; becomes the substantive drive, I find myself compelled to step back and reflect.&lt;/p&gt;
&lt;h2&gt;The Stateless Methodology&lt;/h2&gt;
&lt;p&gt;After the discussion above, my point becomes clear: supplementing context based on Specifications or Memory belongs to a &quot;stateful&quot; derivative practice in the vibe coding realm. A &lt;strong&gt;stateless&lt;/strong&gt; approach, however, relies on the LLM&apos;s decision-making ability, keeping the &quot;be a bit more precise&quot; or &quot;question-and-restart&quot; cycle within the Agent Loop itself.&lt;/p&gt;
&lt;p&gt;A typical &lt;strong&gt;Stateless Agent Loop&lt;/strong&gt; looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user describes the intent, possibly including constraints and processes.&lt;/li&gt;
&lt;li&gt;The Agent performs an &lt;strong&gt;Explore&lt;/strong&gt; to obtain facts from the code source.&lt;/li&gt;
&lt;li&gt;The Agent may use tools to gain more context, such as reading the documentations.&lt;/li&gt;
&lt;li&gt;The Agent &lt;strong&gt;asks the user&lt;/strong&gt; for clarification on key issues.&lt;/li&gt;
&lt;li&gt;Steps 2-4 are repeated.&lt;/li&gt;
&lt;li&gt;Plan and build up.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This idea coincides with the philosophy of Claude Code or the Claude Agent SDK. If you have experienced a standard Claude Code session, you know the flow is remarkably smooth: you only need basic language skills to describe the task clearly (and concisely), and Claude will gather the information it needs. Sometimes, it stops to ask questions—questions that are often brilliant because they cover exactly what your initial description overlooked, sometimes even suggesting better designs within the question. Then, it generates a Plan and finishes building within minutes.&lt;/p&gt;
&lt;p&gt;Of course, the stateless approach has its limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The quality of the codebase must be ensured.&lt;/strong&gt; For example, if the frontend code does not misuse Tailwind CSS to override component styles, the Agent will notice this while exploring the component’s use cases, learn from this few-shot example, and apply the same practice in subsequent code. Otherwise, one would have to devote extensive sections in Spec to explicitly instruct the agent not to do so.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sufficient context acquisition methods must be provided.&lt;/strong&gt; If there are no Agent Skills or tools like context7, knowledge won&apos;t appear out of thin air, which limits the ceiling of the LLM&apos;s capability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The LLM must cooperate with us.&lt;/strong&gt; In trying some models, like OpenAI’s Codex series, I sensed they weren&apos;t trained to work this way; a Spec-driven approach suits them better. I suspect this is why Codex App recently added memory features. Meanwhile, models like Claude and &lt;a href=&quot;https://z.ai/&quot;&gt;GLM&lt;/a&gt; excel at the stateless loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Excessive consumption of context windows.&lt;/strong&gt; When the model runs the &quot;explore subagent&quot;, the scope is inevitably too broad, which consumes more tokens and time and requires additional inference—though I don&apos;t think the spec-driven mode can save on tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, only one question remains: in my argument, I specifically pointed out the &quot;vibe coding&quot; scenario. What about other scenarios?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Generalized Vibe Coding:&lt;/strong&gt; For instance, project management or note organization. The user always needs to provide a Notion MCP to read/write Notion databases, or an Obsidian CLI to access note content. In these cases, the source of truth is external. The model&apos;s task is to operate on these external states. This is essentially no different from vibe coding and fits our reasoning above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-generalized Vibe Coding:&lt;/strong&gt; For example, topics related to personal life. In this case, unless you meticulously list every trivial matter of your life in a memo and, when making a decision or growing, update the memo first and tell yourself &quot;Bingo, Exp+1,&quot; then this is clearly a scenario where &lt;strong&gt;Memory&lt;/strong&gt; would shine.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Finally&lt;/h2&gt;
&lt;p&gt;Thus, we can reach a moderate conclusion. &quot;Stateful&quot; is one approach; &quot;Stateless&quot; is the missing other. For a long time, it has rarely been named or praised, which is one of the reasons I wrote this article.&lt;/p&gt;
&lt;p&gt;I have mentioned that some stateful practices are neurotic in nature - but the whole history of software engineering, and even human history, is structurally neurotic, and &quot;neurotic&quot; has become a purely neutral word: type checking, unit testing, formal proof, CI/CD, these are quite great practices.&lt;/p&gt;
&lt;p&gt;I do not believe there is a hierarchy between the two methods; the scenarios where Stateless doesn&apos;t apply and its limitations are very clear. I promote the Stateless mode out of personal preference, just as others promote Stateful practices. But I have never believed that Stateful should be discarded or that Stateless is the only &quot;correct&quot; way. The purpose of proposing Stateless is to find more paths in the practice of ensuring that the LLM&apos;s potentially incorrect outputs become correct—because that is the only way to get one step closer to the truth.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>(简体中文) Stateless Agent Matters</title><link>https://eflx.top/blog/stateless-agent-matters/zh-hans</link><guid isPermaLink="true">https://eflx.top/blog/stateless-agent-matters/zh-hans</guid><description>The Agent pattern, which has long been overlooked.</description><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Agent Harness 这个词被造出来之后，就意味着它开始进入被人们命名的领域。
一旦一种做法开始被人们命名，一旦人们发现语言无法精确地描述，而赋予他一个借用的词汇，他就开始从本质上是“小众”的实践变为“最佳实践”。&lt;/p&gt;
&lt;p&gt;我依旧记得在过去，人们通过各种方法，把源码打包进一个长文本，急头白脸地粘贴到 ChatGPT 聊天框中的日子。
哪怕现在，带着对 Claude Code 启动十轮 Explore Agent 的恐惧，对一些简单代码问题，我也会复制、粘贴到 &lt;a href=&quot;https://aistudio.google.com&quot;&gt;Google AI Studio&lt;/a&gt; 得到天真无邪的回复。&lt;/p&gt;
&lt;p&gt;无论如何，人们结束了推理模型是否需要 Prompt Engineering 的争论，一套 well-crafted 提示词和工具 schema 能让模型获得卓尔不凡的能力，以悄无声息的方式，开始成为人们的共识。&lt;/p&gt;
&lt;h2&gt;等等。所以什么是 Stateless Agent&lt;/h2&gt;
&lt;p&gt;我的这种观点起源于一种奇怪的感受，它是关于 Memory 的。最开始我们在 ChatGPT 和 Google Gemini 里面见到记忆功能，Claude Code 也加入了所谓的 MEMORY.md。
后来，一些 LLM 客户端比如 &lt;a href=&quot;https://alma.now/&quot;&gt;alma&lt;/a&gt; 提供了令人印象深刻的记忆功能。4 月 16 日，OpenAI Codex 的&lt;a href=&quot;https://openai.com/index/codex-for-almost-everything/&quot;&gt;新版本&lt;/a&gt;也加入了记忆功能。&lt;/p&gt;
&lt;p&gt;其实对记忆的定义比较广泛。绝大多数做法比较“原始”，比如他们基本上是基于一个或多个文本文件，让 LLM 在后台总结、更新这些文件。
也有一些“学院派”的努力，如 &lt;a href=&quot;https://mem.nowledge.co/&quot;&gt;Nowledge Mem&lt;/a&gt; 围绕记忆的聚合与提炼构造了大量 primitives。&lt;/p&gt;
&lt;p&gt;但是我开始思考一个问题：记忆在 LLM 工作流程中的角色是什么？&lt;/p&gt;
&lt;p&gt;当我们让 LLM 在代码库中工作时，基本上是这样的流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;人类提供指令或目的，也许还有约束和过程。此时模型拥有了一部分上下文。&lt;/li&gt;
&lt;li&gt;Agent 通过一些方式，收集其它的上下文，直到上下文足够大，达到某一界限&lt;/li&gt;
&lt;li&gt;此时 Agent 开始思考怎么做，上下文进一步扩大到下一界限&lt;/li&gt;
&lt;li&gt;最终，Agent build up 解决方案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果这么看，Memory 是一种上下文。上下文还包括代码库中的其他代码，Agent 中途向用户发起意图询问，等等。&lt;/p&gt;
&lt;p&gt;在沟通这件事上，人们主要被两点所困扰。一是对方理解能力差，二是对方和自己没有默契（补全缺失的语境）。
LLM 还不够强大，有时候更冗余的上下文可以弥补这点，于是出现了 spec-driven vibe coding，这主要是通过大量（无论是手写还是 AIGC）类似 Specification 描述，确保 vibe coding 产物符合意图的编程方式。
同理，有时候人们希望得到默契，或者希望保持一致性，或是认为历时性的东西可以带来进步。因此，Memory 也加入了。&lt;/p&gt;
&lt;p&gt;可以说，这两个做法都是相当正确的，实践已经证明了这样做确实可以带来成效。
但是问题是：这些做法意在让原本可能不正确的结果（vibe coding 产物）变得正确，因此，使得结果变得正确的做法也就不止这一种；
更甚，在这个问题上，应该做的是寻找最优的、确保可能不正确结果变得正确的这样一个做法。
这很好理解：如果我们只关心 LLM 能否输出正确的代码的话，我们只需要提供越来越细致的 Plan 和 Specification，最后干脆告诉他应该怎么写代码，他只负责调用工具，真正将这些代码应用到代码库中。&lt;/p&gt;
&lt;p&gt;我知道这个（数学意义上的）“极限”推演可能 makes no sense，但是这能够证明：我们应该寻找有效的方式，而单纯地做到“让 LLM 输出结果变得正确”在这里不完全意味着有效。&lt;/p&gt;
&lt;p&gt;Robert Englander 撰写了这篇文章：&lt;a href=&quot;https://robenglander.com/writing/engineering-alignment/&quot;&gt;Engineering Alignment in Probabilistic Generation&lt;/a&gt;。
他认为，软件工程的目标始终是正确性。LLM 改变了错误发生的方式。在概率性的生成过程中，正确性在不同层级之间的转换边界会发生降级（从意图到 Specification，从 Specification 到 CoT，从 CoT 到生成）。
这种降级表现为细微的语义偏移、隐性假设的填充和歧义的压缩。与传统系统的显性错误不同，LLM 生成的系统可能编译成功、通过所有测试、运行平稳，但在语义上已经偏离了最初的意图。&lt;/p&gt;
&lt;p&gt;因此，在那篇文章中，作者进一步提出了五大平面，并强调治理他们之间的边界的重要性。但我想从另一个视角看待这个问题。
LLM 的本体论在于预测生成，这决定了 n-shot 是对他最有力的影响方式。换句话说，LLM 生成的内容很大程度上不能用“正确性”去衡量；
正确性并不存在于 LLM 中，而是存在于上下文中。某个上下文意味着一些输出的概率分布，单纯地依靠上下文的极限施压不能让 LLM 100% 生成某种输出，
甚至不一定意味着在概率分布中，那些小概率的“幻觉”结果会更加“正确”。
正确性也不来源于“工程纪律”，Specification 事实上没有制造正确性，只是从一个错误转向另一个错误，在大量的螺旋式发展中趋于所谓的正确，每个正确实际上相对于每前一个 harness 都是“错误”，而这样的级数是不收敛的。&lt;/p&gt;
&lt;p&gt;如果进一步分析下去，人的意图本身就带有不确定性，并且总存在无法用语言描述的实在界之剩余。这种观点在软件工程领域不一定受待见，甚至在自然科学领域都不一定。
但是，假如真的如此——我相信真的如此——那么，哪怕没有 LLM 辅助，人写的代码也势必不完全正确。&lt;/p&gt;
&lt;p&gt;因此，我得出的结论是：正确性不位于 LLM，也不位于 Specification。它位于所有上下文的共同作用，并依赖于 Reviewer 的心理。&lt;/p&gt;
&lt;p&gt;如果这是真的，这意味着什么？首先我们不能再寄正确性的希望于提供完整、不偏移的意图上。这是我提出 Stateful / Stateless context 的原因。
当我们写 Specification，或者将记忆引入上下文的时候，我们实际上在做一件事：将我们的意图进行了一步派生。
从 Intend 到 Specification 是那篇文章所说的一个转换边界，这里产生了语义的漂移，很好理解。
使用 Memory 时，同样放弃了“我”的意图的“马尔科夫链”-like 的性质，希望模型通过 Memory 获得对意图周延之后的理解。&lt;/p&gt;
&lt;p&gt;这才是我觉得奇怪的地方。&lt;/p&gt;
&lt;p&gt;我认为，上下文应该严格地只包括我的意图描述，而 LLM &lt;strong&gt;被训练到&lt;/strong&gt;应该具备这样的能力：他能够识别目前的上下文是否允许自己写出&lt;em&gt;足够&lt;/em&gt;正确（而非&lt;em&gt;完全&lt;/em&gt;正确）的代码；
如果不够，他应该使用 Explore, Ask User, &lt;a href=&quot;https://context7.com/&quot;&gt;context7&lt;/a&gt; 之类的工具进一步获取上下文，直到自己&lt;em&gt;有把握&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;为什么这样做是对的？很显然，人们把过多的注意力放到了状态上。LLM 是黑箱，但人们在无意识中总认为 LLM 是函数式的黑箱，只要输入足够精确，输出也就能足够精确。
然而，LLM 本质上是薛定谔那样的黑箱。所以当这种性质浮现时，人们就会开始调整输入，进一步增加输入的精确性，或者引入历时性，引入十二个步骤，每一步都精确一下；
或者维护一个记忆文件，不断滚动、更新。这其实是一种神经症行为——我没有恶意，也没有在攻击任何人，因为根据精神分析的理论，任何人不是精神病就是神经症结构（也有可能是变态或孤独症，这些分类尚有争议）。
指出这一点的目的在于，指出这种行为只是在 satisfy 人自身的心理能量，符合人无意识中的欲望——再精确一点、再舒适一点，或者质疑、推翻、重建、再质疑。
这不意味着我所主张的做法就不包含这种因素，但是当 satisfication 成为实质上的驱力时，我不得不抽出身来思考。&lt;/p&gt;
&lt;h2&gt;Stateless 方法论&lt;/h2&gt;
&lt;p&gt;经过上面的讨论，我的观点大概明了了：基于 Specification 或 Memory 补全上下文，在 vibe coding 领域，属于 stateful 的派生形做法。
而 stateless 的做法则指望 LLM 拥有决策能力，让“更加精确一点”或“质疑-重来”的循环留在 Agent Loop 内部。&lt;/p&gt;
&lt;p&gt;一个典型的 Stateless Agent Loop 是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户描述意图，可能还包括约束和过程&lt;/li&gt;
&lt;li&gt;进行 Explore，获得代码方面的事实来源&lt;/li&gt;
&lt;li&gt;就关键问题询问用户获得 clarification&lt;/li&gt;
&lt;li&gt;重复 2-3 步&lt;/li&gt;
&lt;li&gt;Plan and build up&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个想法与 Claude Code 或 Claude Agent SDK 的想法不谋而合。如果您体验过一次标准的 Claude Code 作业，会知道这个流程相当丝滑：
您只需要具备基本语言能力，清晰（并且务必简洁）地描述任务，然后 Claude 就会搜集他所需要的信息。有时候，他会停下来问出一些问题，
这些问题相当牛逼，因为刚好覆盖了你最开始的描述忽略掉的一些点，甚至有时候会在问题中 suggest 更好的设计。
然后，他生成一份 Plan，并在几分钟内构建完毕。&lt;/p&gt;
&lt;p&gt;当然，stateless 的做法也存在限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须提供足够的获取上下文的方式。假如没有 Agent Skills 或者 context7 之类的东西，知识不会凭空产生，这将限制 LLM 的能力高度。&lt;/li&gt;
&lt;li&gt;LLM 必须配合我们。在尝试一些模型时，比如 OpenAI 的 Codex 系列，我明显感知到它并没有被训练地像这样工作，Spec-driven 的方案更适合它。我想这也是为什么 Codex App 最近加入了记忆功能。而 Claude 和 (GLM)[https://z.ai/] 之类的模型则很擅长 stateless loop。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，我们只剩下一个问题了：在我的观点中，我明确指出了“vibe coding”这一场景。别的场景呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;广义 vibe coding：譬如项目管理、笔记整理这样的场景。用户总需要提供 Notion MCP 来读写 Notion 数据库，提供 Obsidian CLI 来访问 Obsidian 笔记内容，等等。在这种情况下，事实来源位于外部，模型的任务是对这些外部状态进行操作，它本质上与 vibe coding 没有区别，适用于我们上面的推理。&lt;/li&gt;
&lt;li&gt;非广义 vibe coding：譬如个人相关的话题。在这件事上，除非您将您生活中所有琐碎的事务事无巨细地罗列在备忘录里，并且当您作出决定或成长时，会先更改备忘录然后告诉自己：bingo，经验+1，否则这种场景显然就是 Memory 会大显身手的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最终&lt;/h2&gt;
&lt;p&gt;所以，我们可以得出温和派的结论了。“Stateful” 是一种做法，“Stateless” 是缺失的另一种。在很长一段时间内，人们很少命名或赞美它，这是我写这篇文章的原因之一。
我并不认为两种方法之间有高下之分，Stateless 不适用的场景和限制条件都很清晰。我带着个人喜好推崇 Stateless 模式，就像人们推崇 Stateful 的实践一样，但我从不认为 Stateful 应该被摒弃、Stateless 才是完全正确的。
提出 Stateless 的目的仍在于：在试图确保 LLM 可能不正确的输出变得正确的实践中，找出更多的道路，因为这样才能向真理更进一步。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>After Safe &amp; Consistent SWR</title><link>https://eflx.top/blog/after-safe-consistent-swr</link><guid isPermaLink="true">https://eflx.top/blog/after-safe-consistent-swr</guid><description>Some reflections, but haven&apos;t changed anything...</description><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some time ago, I wrote &lt;a href=&quot;/blog/safe-consistent-swr&quot;&gt;Safe &amp;#x26; Consistent SWR&lt;/a&gt;, which introduced a paradigm for data fetching in pure client-side React applications using SWR or similar libraries.
After practicing this pattern for two months, the issues with this paradigm have gradually surfaced, and they deserve a rational look.&lt;/p&gt;
&lt;h2&gt;The Chaos of the Source of Truth&lt;/h2&gt;
&lt;p&gt;As business logic becomes more complex, the number of data &quot;scopes&quot; or &quot;endpoints&quot; increases. You need to handle data and business logic for different domains like users, projects, and posts. This is where the problem emerges:
The SWR pattern identifies a specific piece of data through a &quot;Key,&quot; and builds caching, deduplication, and mutations on top of it. However, ten endpoints mean ten key definitions; twenty endpoints mean twenty key definitions.
As the scale grows, you start thinking about reducing boilerplate code. Let&apos;s look at the flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On the backend, we expose various CRUD interfaces via &lt;code&gt;/api&lt;/code&gt;, Electron IPC, or other means.&lt;/li&gt;
&lt;li&gt;In the frontend SWR layer, we define the Key, the SWR hook for fetching data, and the SWR hooks for mutating data for every single CRUD interface.&lt;/li&gt;
&lt;li&gt;In the frontend components, we use these hooks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There seems to be nowhere to streamline. Obviously, the SWR layer requires manual effort to establish the relationship between Keys and backend data, manually defining which CRUD operations affect which pieces of data. This is the root of the discomfort.&lt;/p&gt;
&lt;p&gt;Ultimately, only the backend knows what a backend operation will change.
Therefore, handing the task of &quot;assigning endpoints to data and binding mutation operations to data updates&quot; to the frontend is inherently illogical. The backend is the true source of truth for these mappings.&lt;/p&gt;
&lt;h2&gt;tRPC? Server Action? GraphQL?&lt;/h2&gt;
&lt;p&gt;I first thought of &lt;a href=&quot;https://github.com/jsonnull/electron-trpc&quot;&gt;electron-trpc&lt;/a&gt;. Perhaps it&apos;s a bit &quot;heretical,&quot; but for general &lt;code&gt;/api&lt;/code&gt; servers, tRPC is a viable solution. However, this brings significant refactoring costs and carries risks when switching the backend to a decoupled design (e.g., running a Go or Python service).&lt;/p&gt;
&lt;p&gt;What about Server Actions? I must admit, the DX is excellent. In a &quot;full-stack&quot; React app, using primitives like RSC + Server Actions isn&apos;t some monster to be feared. In fact, they are like those £10,000-a-spoon desserts in a Michelin-starred restaurant—using RSC sparingly when encountering certain problems feels incredibly comfortable and elegant.
However, we are not only discussing applications with complex data models—where Server Actions might not be much better—but also solutions for pure client-side applications.&lt;/p&gt;
&lt;p&gt;Since the backend needs to remain &quot;pure&quot; but an intermediate layer is required, Codegen comes to mind. Conveniently, &lt;code&gt;drizzle-orm&lt;/code&gt; even allows us to generate Zod schemas, killing two birds with one stone! Here, &quot;codegen&quot; means generating SWR hooks based on a schema. It&apos;s actually a good idea, but it doesn&apos;t solve the mutation association problem—the issue of which data change operation triggers which Key mutation.&lt;/p&gt;
&lt;p&gt;Finally, we are left with options like GraphQL. When I mention GraphQL, it&apos;s currently in an awkward spot: if the app is already complex, it means a refactoring cost as high as tRPC. If the app itself is toy-level, why bother? To solve a small problem of over-engineering, we introduce a massive over-engineered design... that&apos;s not good.&lt;/p&gt;
&lt;h2&gt;Slightly Lost&lt;/h2&gt;
&lt;p&gt;Consequently, after a long period of thought experiments, I haven&apos;t actually made any sufficient improvements to the original architecture. However, over the past two months, I have tried:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Service Locator Architecture&lt;/strong&gt;: Categorizing SWR hooks into &quot;service&quot; objects, with all services provided via React Context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boilerplate reduction utilities&lt;/strong&gt;: Quickly wrapping IPC functions or fetch &lt;code&gt;/api/&lt;/code&gt; calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key Shapes&lt;/strong&gt;: In my previous article, I adopted a Key shape based on &lt;code&gt;{ endpoint: ..., /* other params */ }&lt;/code&gt;. But I&apos;ve found that the array-based Keys encouraged by TanStack Query might even be better. I once assumed I had the patience to meticulously control the mutation of each individual key. In the end, I realized that once you have a mess of endpoints like &quot;all models,&quot; &quot;user-added models,&quot; &quot;user-pinned models,&quot; &quot;true all models,&quot; and &quot;all models in the list,&quot; you just end up invalidating everything in a single blanket mutation anyway. I am currently experimenting with and feeling the &quot;correctness&quot; of &lt;code&gt;[&apos;models&apos;, &apos;pinned&apos;]&lt;/code&gt; over &lt;code&gt;{ endpoint: &apos;pinnedModels&apos; }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For now, although the SWR layer code looks massive on the frontend, its operation remains stable and reliable. When adding a new feature, you only need to mindlessly copy some templates.
In the future, I may release a V2 of Safe &amp;#x26; Consistent SWR / Query, including all updated best practices. For now, I still believe this is the most reliable and effective data fetching solution.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Modern React on Client Side</title><link>https://eflx.top/blog/modern-react-client-side</link><guid isPermaLink="true">https://eflx.top/blog/modern-react-client-side</guid><description>Let&apos;s dive into the new era of React.</description><pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;Since C++ 11, features like move semantics, resource management, and functional programming have completely shattered the prejudice that C++ is just &quot;C with Classes.&quot; People call this &quot;Modern C++.&quot;
In fact, the React paradigm has also matured in recent years. I believe that React after version 18 can also be called &quot;Modern React.&quot;
The updates over these years haven&apos;t just been about adding a few Hooks; they represent a continued reform of the paradigm. We are moving from imperatively managing life cycles and state synchronization to declaratively defining resources and behaviors. We are stepping closer to true declarativeness.&lt;/p&gt;
&lt;h2&gt;Legacy React&lt;/h2&gt;
&lt;p&gt;In the past, we were accustomed to using &lt;code&gt;useEffect&lt;/code&gt; to handle all logic. To &quot;peek&quot; at a state (reading it without wanting it to be reactive), we might have deliberately ignored some dependencies in the dependency array, yet the linter would stop us from doing so. Things like &lt;code&gt;eslint-ignore&lt;/code&gt; flooded our code.&lt;/p&gt;
&lt;p&gt;Undoubtedly, this leads to code that is difficult to maintain. More insidiously, it confuses the coder&apos;s brain—or rather, creates a chaotic understanding of the framework&apos;s logic itself. In such a state, the code written can hardly be high-quality, highly readable, or modern.&lt;/p&gt;
&lt;p&gt;Moreover, &lt;code&gt;useEffect&lt;/code&gt; has deviated from its original intention: &lt;strong&gt;to synchronize internal state with external systems&lt;/strong&gt;. Furthermore, if you are synchronizing an external store, &lt;code&gt;useEffect&lt;/code&gt; cannot even be considered a good solution.&lt;/p&gt;
&lt;p&gt;To protect performance under the re-rendering mechanism, &lt;code&gt;useCallback&lt;/code&gt; and &lt;code&gt;useMemo&lt;/code&gt; were also heavily used. This syntactic redundancy exacerbated the issues with dependency arrays and linters.&lt;/p&gt;
&lt;h2&gt;A Shift in Mental Model: Taking &lt;code&gt;useEffectEvent&lt;/code&gt; as an Example&lt;/h2&gt;
&lt;p&gt;In Modern React, &lt;code&gt;useEffect&lt;/code&gt; must be reactive. If you need to read a state, but that state change shouldn&apos;t trigger the &lt;code&gt;useEffect&lt;/code&gt;, you should use &lt;a href=&quot;https://react.dev/reference/react/useEffectEvent&quot;&gt;&lt;code&gt;useEffectEvent&lt;/code&gt;&lt;/a&gt;.
Here is an example from the official documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() =&gt; {
    showNotification(&apos;Connected!&apos;, theme)
  })

  useEffect(() =&gt; {
    const connection = createConnection(serverUrl, roomId)
    connection.on(&apos;connected&apos;, () =&gt; {
      onConnected()
    })
    connection.connect()
    return () =&gt; connection.disconnect()
  }, [roomId])

  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;showNotification&lt;/code&gt; is a behavior, not part of the data flow, so it shouldn&apos;t destroy the stability of the Effect.
You can use &lt;code&gt;useEffectEvent&lt;/code&gt; to call &lt;code&gt;showNotification&lt;/code&gt; and use this &lt;code&gt;event&lt;/code&gt; inside &lt;code&gt;useEffect&lt;/code&gt; without adding it to the dependency array.&lt;/p&gt;
&lt;p&gt;However, note that the Effect Event function returned by &lt;code&gt;useEffectEvent&lt;/code&gt; can only be used inside Effects (like &lt;code&gt;useEffect&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The example above reflects the deepest philosophy of Modern React: let everything return to its origin. The dependency array should &lt;strong&gt;honestly&lt;/strong&gt; declare reactive dependencies, and developers shouldn&apos;t use clever hacks to achieve what are actually common goals.&lt;/p&gt;
&lt;p&gt;Similarly, &lt;code&gt;useEffect&lt;/code&gt; should only be used to synchronize React state with external systems and shouldn&apos;t bear other responsibilities. Although this has been a principle declared by React from the beginning, it is only in the Modern React era that we have enough convenience to fully implement this principle. Let&apos;s look further.&lt;/p&gt;
&lt;h2&gt;Modern Data Fetching and Synchronization&lt;/h2&gt;
&lt;p&gt;If we use &lt;code&gt;useEffect&lt;/code&gt; to fetch data, we need to manually set states like &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;isLoading&lt;/code&gt;, and &lt;code&gt;error&lt;/code&gt;, and manually handle race conditions. This should be viewed as an anti-pattern.
It creates a lot of boilerplate code and requires developers to master low-level issues during business development. This not only violates the declarative paradigm but makes it impossible to handle every case perfectly in practice. Moreover, it makes it difficult to implement features essential for modern applications, like caching and debouncing.&lt;/p&gt;
&lt;p&gt;According to React philosophy, data should be a resource, not a &lt;strong&gt;side effect&lt;/strong&gt;. Consider the following two points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use libraries like &lt;a href=&quot;https://swr.vercel.app&quot;&gt;SWR&lt;/a&gt; or &lt;a href=&quot;https://tanstack.com/query&quot;&gt;TanStack Query&lt;/a&gt;. In &lt;a href=&quot;/blog/safe-consistent-swr&quot;&gt;Safe &amp;#x26; Consistent SWR&lt;/a&gt;, I tried to explain the best way to fetch and sync data using query libraries like &lt;code&gt;useSWR&lt;/code&gt;. It might be worth a look.&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://react.dev/reference/react/useSyncExternalStore&quot;&gt;&lt;code&gt;useSyncExternalStore&lt;/code&gt;&lt;/a&gt;. This is a low-level primitive introduced in React 18. When dealing with browser APIs or global state libraries, you no longer need &lt;code&gt;useEffect&lt;/code&gt; to listen to and set state.
&lt;ul&gt;
&lt;li&gt;Furthermore, it solves issues under concurrent rendering, ensuring that the external data read by the UI is consistent within any given time slice, thus paving the way for the critical feature of concurrent rendering. We will see later how this changes the way we think about component rendering.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Derived State&lt;/h2&gt;
&lt;p&gt;Have you or people around you written code like this?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function User({ firstName }) {
    const [fullname, setFullname] = useState(null)
    useEffect(() =&gt; {
        setFullname(`Kumo ${firstName}`)
    }, [firstName])

    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React teaches us that we shouldn&apos;t use &lt;code&gt;useEffect&lt;/code&gt; here; instead, directly &lt;strong&gt;derive state&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function User({ firstName }) {
    const fullname = useMemo(() =&gt; `Kumo ${firstName}`, [firstName])
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait... something is still not quite right! Every developer should know about the &lt;a href=&quot;https://react.dev/learn/react-compiler&quot;&gt;React Compiler&lt;/a&gt;. For scenarios where you only want to ensure referential stability, don&apos;t waste time manually memoizing anymore.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function User({ firstName }) {
    const fullname = `Kumo ${firstName}`
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cross-component references and complex Context passing may still cause the Compiler to fail to optimize automatically; once developers lose explicit control over references, debugging difficulty might increase if performance regression occurs.
Before switching to it, please also note the two sides of everything.&lt;/p&gt;
&lt;p&gt;If you don&apos;t need explicit control over memoization behavior, please switch to React Compiler and handle derived state directly and naturally.&lt;/p&gt;
&lt;h2&gt;Concurrent Rendering&lt;/h2&gt;
&lt;p&gt;Suppose we have a time-consuming operation. Set a &lt;code&gt;pending&lt;/code&gt; state, then show a spinner!
This is indeed a very natural thought, but abusing it leads to frequent interface flickering, which in turn requires advanced handling techniques. This raises the barrier to writing good code and forces developers to focus on things they shouldn&apos;t have to.&lt;/p&gt;
&lt;p&gt;In the concurrent rendering mode, UI updates can be prioritized and can be &quot;suspended.&quot;&lt;/p&gt;
&lt;p&gt;Components can use &lt;a href=&quot;https://react.dev/reference/react/use&quot;&gt;&lt;code&gt;use&lt;/code&gt;&lt;/a&gt; to unwrap a Promise. Before the Promise is resolved, rendering will fall back to the nearest &lt;a href=&quot;https://react.dev/reference/react/Suspense&quot;&gt;&lt;code&gt;Suspense&lt;/code&gt;&lt;/a&gt; boundary.
This means we can handle asynchronous data just like writing synchronous code. However, this is from a low-level perspective; for actual applications, it is still recommended to handle this via mechanisms like &lt;code&gt;useSWR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What is &lt;code&gt;Suspense&lt;/code&gt;? I recall &lt;code&gt;Suspense&lt;/code&gt; wasn&apos;t used for this before?&lt;/p&gt;
&lt;p&gt;You are right. &lt;code&gt;Suspense&lt;/code&gt; has taken on new responsibilities in Modern React, as described above. Once we start using &lt;code&gt;Suspense&lt;/code&gt; instead of handling pending states inside components, we can look back at these scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The user is typing in an input box, while the search and rendering of a Select component have a certain delay.&lt;/li&gt;
&lt;li&gt;Fetching data when opening a dropdown menu, but showing a spinner looks bad; passing data via router or context is an anti-pattern.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means UI rendering can be deferred. &lt;a href=&quot;https://react.dev/reference/react/useTransition&quot;&gt;&lt;code&gt;useTransition&lt;/code&gt;&lt;/a&gt; demonstrates how to achieve this.
For example, in the data fetching for a dropdown menu, use the &lt;code&gt;{ suspense: true }&lt;/code&gt; option for &lt;code&gt;useSWR&lt;/code&gt;, and then wrap the logic for opening the dropdown menu with &lt;code&gt;startTransition&lt;/code&gt;.
You can use &lt;code&gt;startTransition&lt;/code&gt; to render UI that requires suspense. This won&apos;t actually revert to the Suspense boundary; React will mark these updates as non-urgent and will only display the interface once they are completed, thus avoiding the screen flickering issue.&lt;/p&gt;
&lt;p&gt;Please be sure to read the Suspense documentation carefully. But trust me, this is still much simpler than the old way!&lt;/p&gt;
&lt;p&gt;Additionally, transitions can be widely applied in scenarios requiring non-blocking updates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0)
  useEffect(() =&gt; {
    const handler = () =&gt; {
      startTransition(() =&gt; setScrollY(window.scrollY))
    }
    window.addEventListener(‘scroll’, handler, { passive: true })
    return () =&gt; window.removeEventListener(‘scroll’, handler)
  }, [])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Encapsulating UI state-updating actions like &lt;code&gt;setScrollY&lt;/code&gt; within &lt;code&gt;startTransition&lt;/code&gt; ensures updates won&apos;t block the UI.&lt;/p&gt;
&lt;h2&gt;Actions are also usable for Client-Side Apps&lt;/h2&gt;
&lt;p&gt;Many people say these APIs are brought by RSC (React Server Components), but I don&apos;t agree. Even in pure client-side applications, React 19&apos;s Action API is still revolutionary.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;form onSubmit={handleSubmit}&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preventDefault&lt;/code&gt; inside &lt;code&gt;handleSubmit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setPending(true)&lt;/code&gt;, maybe considering &lt;code&gt;startTransition&lt;/code&gt; after reading the above...?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try/catch&lt;/code&gt; POST&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setPending(false)&lt;/code&gt; / &lt;code&gt;setError(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ah... right! What about optimistic updates? 🤔🤯&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I believe you are already disgusted by this boilerplate code. &lt;code&gt;submit&lt;/code&gt;, as a mutation of data, shouldn&apos;t be this complex.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useActionState&quot;&gt;&lt;code&gt;useActionState&lt;/code&gt;&lt;/a&gt; automatically manages the result and status of asynchronous operations; you just need to provide a handler function!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react-dom/hooks/useFormStatus&quot;&gt;&lt;code&gt;useFormStatus&lt;/code&gt;&lt;/a&gt; allows accessing form status in components like buttons inside the form.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useOptimistic&quot;&gt;&lt;code&gt;useOptimistic&lt;/code&gt;&lt;/a&gt; lets us embrace modern UX! Show updates on the UI immediately; if it fails, automatically rollback.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Final Comparison&lt;/h2&gt;
&lt;p&gt;Modern React should be able to build applications like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const useMessages = (roomId) =&gt; useSWR(..., { suspense: true })

function ChatRoom({ roomId }) {
  // messages will not be undefined; let&apos;s throw isLoading away
  const { data: messages } = useMessages(roomId)
  const [state, formAction, isPending] = useActionState(sendMessageAPI, null)
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) =&gt; [...state, newMessage]
  )

  const activeCount = optimisticMessages.filter(m =&gt; !m.archived).length

  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;MessageList messages={optimisticMessages} /&gt;
      &amp;#x3C;div className=&quot;status&quot;&gt;Active: {activeCount}&amp;#x3C;/div&gt;
      
      &amp;#x3C;form action={async (formData) =&gt; {
          const msg = formData.get(&apos;message&apos;)
          addOptimisticMessage({ text: msg, sending: true })
          await formAction(formData)
      }}&gt;
        &amp;#x3C;input name=&quot;message&quot; /&gt;

         {/* use useFormStatus inside; no need to pass states */}
        &amp;#x3C;SubmitButton /&gt;
      &amp;#x3C;/form&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might have forgotten that before all this, the universe was chaos, and the Creator gave up on humanity because of messy code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Ahh i&apos;m using isSubmitting :(
function SubmitButtonLegacy({ isSubmitting }) {
  return (
    &amp;#x3C;button type=&quot;submit&quot; disabled={isSubmitting}&gt;
      {isSubmitting ? &apos;Sending...&apos; : &apos;Send&apos;}
    &amp;#x3C;/button&gt;
  )
}

function ChatRoomLegacy({ roomId }) {
  // Classic Trio!
  const [messages, setMessages] = useState([])
  const [isLoading, setIsLoading] = useState(true)
  const [fetchError, setFetchError] = useState(null)

  useEffect(() =&gt; {
    let ignore = false // Handling race conditions
    
    async function fetchData() {
      setIsLoading(true)
      setFetchError(null)
      try {
        const data = await fetchMessagesAPI(roomId)
        if (!ignore) {
          setMessages(data)
        }
      } catch (err) {
        if (!ignore) setFetchError(err)
      } finally {
        if (!ignore) setIsLoading(false)
      }
    }

    fetchData() // Hmm. Honestly, I&apos;d rather use .then.catch.finally

    return () =&gt; {
      ignore = true
    }
  }, [roomId]) // My favorite dependency array 🥰

  // Here we go again.
  const [isSending, setIsSending] = useState(false)
  const [sendError, setSendError] = useState(null)

  // We need a ref to prevent closure traps, or use functional updates
  // We need to manually modify messages and rollback on failure
  const handleSendMessage = async (e) =&gt; {
    e.preventDefault() // Must prevent default behavior
    const formData = new FormData(e.target)
    const msgText = formData.get(&apos;message&apos;)
    
    if (!msgText) return

    // Step 1: Optimistic Update
    const tempId = Date.now()
    const optimisticMsg = { id: tempId, text: msgText, sending: true }
    
    setMessages((prev) =&gt; [...prev, optimisticMsg])
    setIsSending(true)
    setSendError(null)
    e.target.reset() // Manually clear the form

    try {
      // Step 2: Send Request
      const newServerMsg = await sendMessageAPI(msgText)
      
      // Step 3: Success: Replace temp data with real server data
      setMessages((prev) =&gt; 
        prev.map(m =&gt; m.id === tempId ? newServerMsg : m)
      )
    } catch (err) {
      // Step 4: Failure: Manual rollback (very easy to get wrong!)
      setSendError(err)
      setMessages((prev) =&gt; prev.filter(m =&gt; m.id !== tempId))
      // You even need to restore the input box content; omitted here because it&apos;s too much trouble
    } finally {
      setIsSending(false)
    }
  }
 
  const activeCount = useMemo(() =&gt; {
    return messages.filter(m =&gt; !m.archived).length
  }, [messages]) // Carefully writing dependencies.

  // Forgot the skeleton screen again. Whatever, I&apos;ll write a Loading text for now
  if (isLoading) return &amp;#x3C;div&gt;Loading...&amp;#x3C;/div&gt;
  if (fetchError) return &amp;#x3C;div&gt;Error: {fetchError.message}&amp;#x3C;/div&gt;

  // Finally! After such a long buildup... Wait! Why does such simple UI need such verbose logic? I don&apos;t understand 😭
  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;MessageList messages={messages} /&gt;
      &amp;#x3C;div className=&quot;status&quot;&gt;Active: {activeCount}&amp;#x3C;/div&gt;
      &amp;#x3C;form onSubmit={handleSendMessage}&gt;
        &amp;#x3C;input name=&quot;message&quot; disabled={isSending} /&gt;
        &amp;#x3C;SubmitButtonLegacy isSubmitting={isSending} /&gt;
        {sendError &amp;#x26;&amp;#x26; &amp;#x3C;div className=&quot;error&quot;&gt;Failed to send&amp;#x3C;/div&gt;}
      &amp;#x3C;/form&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In summary, in legacy React, to achieve the same functionality, developers need to manually write a massive amount of &quot;glue code&quot; to maintain state consistency.
The code volume is not only large, but every manually maintained state is a potential source of bugs.&lt;/p&gt;
&lt;p&gt;In this era where framework wars are intensifying, please use less code, greater robustness, and &lt;em&gt;easily&lt;/em&gt; do the right thing. Let us vindicate React.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>(简体中文) Modern React on Client Side</title><link>https://eflx.top/blog/modern-react-client-side/zh-hans</link><guid isPermaLink="true">https://eflx.top/blog/modern-react-client-side/zh-hans</guid><description>Let&apos;s dive into the new era of React.</description><pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;C++ 11 后，移动语义、资源管理、函数式编程等特性彻底改变了 C++ “带类的 C” 的偏见。人们称其为“Modern C++”。
其实，近年来 React 范式也开始走向成熟，我认为 React 18 之后的 React 也可以被称为“Modern React”。
这些年来 React 的更新，并非只是多了几个 Hook，而是范式的继续改革。从命令式地手动管理生命周期和状态同步，转向声明式地定义资源与行为。我们离真正的声明式可以更进一步。&lt;/p&gt;
&lt;h2&gt;传统 React&lt;/h2&gt;
&lt;p&gt;过去，我们习惯用 &lt;code&gt;useEffect&lt;/code&gt; 处理所有逻辑。为了获取“peek”状态（读取状态但不希望这是响应式的），我们可能在依赖数组中刻意忽略一些依赖，然而 linter 又会阻止我们这样做。&lt;code&gt;eslint-ignore&lt;/code&gt; 之类的东西充斥在代码中。&lt;/p&gt;
&lt;p&gt;这无疑会导致代码变得难以维护，同时更隐性的影响是，他让编码者的大脑混乱，或者说对框架的逻辑本身有着混乱的认知，这样的状态下，写出来的代码必然不可能是高质量、具有高可读性、现代化的。&lt;/p&gt;
&lt;p&gt;而且，&lt;code&gt;useEffect&lt;/code&gt; 已经偏离了其初衷：&lt;strong&gt;用于同步内部状态与外部系统&lt;/strong&gt;。甚至，如果要同步外部的 store，&lt;code&gt;useEffect&lt;/code&gt; 也完全称不上是一个好的方案。&lt;/p&gt;
&lt;p&gt;为了保护重渲染机制下的性能，&lt;code&gt;useCallback&lt;/code&gt; 和 &lt;code&gt;useMemo&lt;/code&gt; 也被大量使用，这种语法冗余的同时也加剧了依赖数组依赖 linter 的问题。&lt;/p&gt;
&lt;h2&gt;思维模型转变：以 &lt;code&gt;useEffectEvent&lt;/code&gt; 为例&lt;/h2&gt;
&lt;p&gt;在 Modern React 中，&lt;code&gt;useEffect&lt;/code&gt; 必须是响应式的。如果需要读取某个状态，而该状态变更时又不应该重新触发 &lt;code&gt;useEffect&lt;/code&gt;，你应该使用 &lt;a href=&quot;https://react.dev/reference/react/useEffectEvent&quot;&gt;&lt;code&gt;useEffectEvent&lt;/code&gt;&lt;/a&gt;。
官方文档的示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() =&gt; {
    showNotification(&apos;Connected!&apos;, theme)
  })

  useEffect(() =&gt; {
    const connection = createConnection(serverUrl, roomId)
    connection.on(&apos;connected&apos;, () =&gt; {
      onConnected()
    })
    connection.connect()
    return () =&gt; connection.disconnect()
  }, [roomId])

  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;showNotification&lt;/code&gt; 是一个行为，而非数据的一部分，因此它不应破坏 Effect 的稳定性。
可以用 &lt;code&gt;useEffectEvent&lt;/code&gt; 来调用 &lt;code&gt;showNotification&lt;/code&gt;，并在 &lt;code&gt;useEffect&lt;/code&gt; 中使用这个 &lt;code&gt;event&lt;/code&gt; 而不需要将其加入依赖数组。&lt;/p&gt;
&lt;p&gt;不过需要注意，&lt;code&gt;useEffectEvent&lt;/code&gt; 返回的 Effect Event 函数只能用在 Effects 中（如 &lt;code&gt;useEffect&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;上述例子反映了 Modern React 最底层的思想：让一切回归本源，依赖数组就应该&lt;strong&gt;诚实地&lt;/strong&gt;声明响应式的依赖，开发者不应该为实现本来常见的目的而使用奇技淫巧；&lt;/p&gt;
&lt;p&gt;同样地，&lt;code&gt;useEffect&lt;/code&gt; 也只应该被用作同步 React 状态与外部系统，而不应该承担其他的职责，尽管这是 React 自始至终都声明地原则，但只有 Modern React 时代，我们才有足够的便利实现这一原则。让我们继续往下看。&lt;/p&gt;
&lt;h2&gt;现代化的数据获取与同步&lt;/h2&gt;
&lt;p&gt;如果使用 &lt;code&gt;useEffect&lt;/code&gt; 来获取数据，我们需要手动设置 &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt; 等状态，手动处理竞态条件。这样做应该被视为一种反模式。
他造成了许多模板代码，还要求业务开发时也要熟练地掌握一些底层问题，这不仅违背了声明式的范式，在实践中要求每处处理都完美得当也是不可能的。更何况这样做还难以实现缓存、去抖动等现代应用所必须的特性。&lt;/p&gt;
&lt;p&gt;依照 React 哲学，数据理应是一种资源，而不是&lt;strong&gt;副作用&lt;/strong&gt;。考虑以下两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://swr.vercel.app&quot;&gt;SWR&lt;/a&gt;，&lt;a href=&quot;https://tanstack.com/query&quot;&gt;TanStack Query&lt;/a&gt; 等库。在 &lt;a href=&quot;/blog/safe-consistent-swr&quot;&gt;Safe &amp;#x26; Consistent SWR&lt;/a&gt; 中，我试图说明使用 &lt;code&gt;useSWR&lt;/code&gt; 等 Query 库获取与同步数据的最佳方式，不妨看看。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://react.dev/reference/react/useSyncExternalStore&quot;&gt;&lt;code&gt;useSyncExternalStore&lt;/code&gt;&lt;/a&gt;。这是 React 18 引入的底层原语。对于浏览器 API 或使用全局状态库时，不再需要用 useEffect 去监听和设置状态。
&lt;ul&gt;
&lt;li&gt;而且，它解决了并发渲染下的问题，确保 UI 在任何时间切片读取到的外部数据是一致的，从而为并发渲染这一重要特性开辟道路。在后面我们将会看到这如何改变了组件渲染的思维方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;派生状态&lt;/h2&gt;
&lt;p&gt;你或者身边的人是否写过这样的代码？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function User({ firstName }) {
    const [fullname, setFullname] = useState(null)
    useEffect(() =&gt; {
        setFullname(`Kumo ${firstName}`)
    }, [firstName])

    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 教导我们，这里不应该用 &lt;code&gt;useEffect&lt;/code&gt;，直接&lt;strong&gt;派生状态&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function User({ firstName }) {
    const fullname = useMemo(() =&gt; `Kumo ${firstName}`, [firstName])
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等等……还是不对劲！所有开发者都应该了解 &lt;a href=&quot;https://react.dev/learn/react-compiler&quot;&gt;React Compiler&lt;/a&gt;，对于仅仅为了保证引用稳定性的场景，不用再浪费时间手动 memo 了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function User({ firstName }) {
    const fullname = `Kumo ${firstName}`
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跨组件引用、复杂的 Context 传递依然可能导致 Compiler 无法自动优化；开发者失去对引用的显式控制权后，一旦出现性能回退，调试的难度反而会上升。
在改用它之前，也请你注意任何事物都具有的两面性。&lt;/p&gt;
&lt;p&gt;如果不需要明确的对 memorization 行为的控制，请改用 React Compiler 并直接自然地处理派生状态。&lt;/p&gt;
&lt;h2&gt;并发渲染&lt;/h2&gt;
&lt;p&gt;假设我们有一个耗时操作。设置一个 &lt;code&gt;pending&lt;/code&gt; 状态，然后显示一个 spinner！
这的确是很自然的想法，然而滥用会导致界面频繁闪烁，从而要求更高级的处理技巧，这又提高了写出好代码的门槛，而且让开发者的注意力集中在本不应该的事情上。&lt;/p&gt;
&lt;p&gt;在并发渲染的模式中，UI 的更新是可以分优先级的，且可以被“挂起”的。&lt;/p&gt;
&lt;p&gt;组件可以用 &lt;a href=&quot;https://react.dev/reference/react/use&quot;&gt;&lt;code&gt;use&lt;/code&gt;&lt;/a&gt; 解包 Promise，在 Promise resolved 前，渲染会回退到最近的 &lt;a href=&quot;https://react.dev/reference/react/Suspense&quot;&gt;&lt;code&gt;Suspense&lt;/code&gt;&lt;/a&gt; 边界。
这意味着我们可以像写同步代码一样处理异步数据，不过，这是从底层层面而言的；对于实际应用，还是建议通过 &lt;code&gt;useSWR&lt;/code&gt; 等机制来处理。&lt;/p&gt;
&lt;p&gt;什么是 &lt;code&gt;Suspense&lt;/code&gt;？我记得 &lt;code&gt;Suspense&lt;/code&gt; 以前不是干这个的吧？&lt;/p&gt;
&lt;p&gt;你说得没错。&lt;code&gt;Suspense&lt;/code&gt; 在 Modern React 中承担起了新的职责，正如上面所述。一旦开始使用 &lt;code&gt;Suspense&lt;/code&gt; 而不是在组件内处理 pending 状态，我们就可以回头看看这些场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户在输入框打字，然而 Select 组件的搜索和渲染带有一定延迟&lt;/li&gt;
&lt;li&gt;打开 dropdown menu 时需要抓取数据，但显示一个 spinner 显然观感不好；让 router 或 context 传递数据又是反模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着 UI 渲染是可以延后的。&lt;a href=&quot;https://react.dev/reference/react/useTransition&quot;&gt;&lt;code&gt;useTransition&lt;/code&gt;&lt;/a&gt; 展示了如何做到这一点。
例如，在 dropdown menu 的数据获取中，为 &lt;code&gt;useSWR&lt;/code&gt; 使用 &lt;code&gt;{ suspense: true }&lt;/code&gt; 选项，然后用 &lt;code&gt;startTransition&lt;/code&gt; 包裹打开 dropdown menu 的逻辑。
你可以用 &lt;code&gt;startTransition&lt;/code&gt; 来渲染需要 suspense 的 UI，而这不会真的回到 Suspense 边界；React 会标记这些更新为非紧急，只有在完成后才会显示界面，从而避免了屏幕闪烁的问题。&lt;/p&gt;
&lt;p&gt;请务必仔细阅读 Suspense 的文档。不过相信我，这仍然会比旧的方式简单很多！&lt;/p&gt;
&lt;p&gt;此外，transitions 可以广泛地应用于需要不阻塞地更新的场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0)
  useEffect(() =&gt; {
    const handler = () =&gt; {
      startTransition(() =&gt; setScrollY(window.scrollY))
    }
    window.addEventListener(&apos;scroll&apos;, handler, { passive: true })
    return () =&gt; window.removeEventListener(&apos;scroll&apos;, handler)
  }, [])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 &lt;code&gt;setScrollY&lt;/code&gt; 这种操作 UI 状态的行为封装进 &lt;code&gt;startTransition&lt;/code&gt;，就能确保更新不会 block UI 了。&lt;/p&gt;
&lt;h2&gt;Action 同样可用于客户端应用&lt;/h2&gt;
&lt;p&gt;很多人说这些 API 是 RSC 带来的，我并不认可。即使在纯客户端应用中，React 19 的 Action API 依然极具革命性。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;form onSubmit={handleSubmit}&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;handleSubmit&lt;/code&gt; 里 &lt;code&gt;preventDefault&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setPending(true)&lt;/code&gt;，或许看了前面的内容会考虑 &lt;code&gt;startTransition&lt;/code&gt;...?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try/catch&lt;/code&gt; POST&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setPending(false)&lt;/code&gt; / &lt;code&gt;setError(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;啊……对！还有乐观更新该怎么办 🤔🤯&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;相信你已经对这些模板代码感到反感。submit 作为数据的 mutation，不应该这么复杂。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useActionState&quot;&gt;&lt;code&gt;useActionState&lt;/code&gt;&lt;/a&gt; 自动管理异步操作的结果和状态，只需要提供一个处理函数就行！&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react-dom/hooks/useFormStatus&quot;&gt;&lt;code&gt;useFormStatus&lt;/code&gt;&lt;/a&gt; 在 button 等 form 内组件访问 form 状态。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useOptimistic&quot;&gt;&lt;code&gt;useOptimistic&lt;/code&gt;&lt;/a&gt; 让我们拥抱现代化 UX！立即在 UI 上显示更新；如果失败，自动回滚。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最终的对比&lt;/h2&gt;
&lt;p&gt;现代 React 应该能够这样构建应用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const useMessages = (roomId) =&gt; useSWR(..., { suspense: true })

function ChatRoom({ roomId }) {
  // messages 不会是 undefined，可以把 isLoading 扔了
  const { data: messages } = useMessages(roomId)
  const [state, formAction, isPending] = useActionState(sendMessageAPI, null)
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) =&gt; [...state, newMessage]
  )

  const activeCount = optimisticMessages.filter(m =&gt; !m.archived).length

  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;MessageList messages={optimisticMessages} /&gt;
      &amp;#x3C;div className=&quot;status&quot;&gt;Active: {activeCount}&amp;#x3C;/div&gt;
      
      &amp;#x3C;form action={async (formData) =&gt; {
          const msg = formData.get(&apos;message&apos;)
          addOptimisticMessage({ text: msg, sending: true })
          await formAction(formData)
      }}&gt;
        &amp;#x3C;input name=&quot;message&quot; /&gt;

         {/* 内部使用 useFormStatus，无需传递状态 */}
        &amp;#x3C;SubmitButton /&gt;
      &amp;#x3C;/form&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能已经忘了在这一切以前，宇宙是一片混沌，造物主因为 messy code 放弃了人类。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 继承 isSubmitting 状态 :(
function SubmitButtonLegacy({ isSubmitting }) {
  return (
    &amp;#x3C;button type=&quot;submit&quot; disabled={isSubmitting}&gt;
      {isSubmitting ? &apos;Sending...&apos; : &apos;Send&apos;}
    &amp;#x3C;/button&gt;
  )
}

function ChatRoomLegacy({ roomId }) {
  // 经典三件套！
  const [messages, setMessages] = useState([])
  const [isLoading, setIsLoading] = useState(true)
  const [fetchError, setFetchError] = useState(null)

  useEffect(() =&gt; {
    let ignore = false // 处理竞态条件
    
    async function fetchData() {
      setIsLoading(true)
      setFetchError(null)
      try {
        const data = await fetchMessagesAPI(roomId)
        if (!ignore) {
          setMessages(data)
        }
      } catch (err) {
        if (!ignore) setFetchError(err)
      } finally {
        if (!ignore) setIsLoading(false)
      }
    }

    fetchData() // 嗯。说实话我还不如用 .then.catch.finally

    return () =&gt; {
      ignore = true
    }
  }, [roomId]) // 我最爱的依赖数组 🥰

  // 又来。
  const [isSending, setIsSending] = useState(false)
  const [sendError, setSendError] = useState(null)

  // 我们需要一个 ref 来防止闭包陷阱，或者使用函数式更新
  // 我们需要手动修改 messages 并在失败时回滚  
  const handleSendMessage = async (e) =&gt; {
    e.preventDefault() // 必须阻止默认行为
    const formData = new FormData(e.target)
    const msgText = formData.get(&apos;message&apos;)
    
    if (!msgText) return

    // 第一步 乐观更新
    const tempId = Date.now()
    const optimisticMsg = { id: tempId, text: msgText, sending: true }
    
    setMessages((prev) =&gt; [...prev, optimisticMsg])
    setIsSending(true)
    setSendError(null)
    e.target.reset() // 手动清空表单

    try {
      // 第二步 发送请求
      const newServerMsg = await sendMessageAPI(msgText)
      
      // 第三步 成功：用服务器返回的真实数据替换临时数据
      setMessages((prev) =&gt; 
        prev.map(m =&gt; m.id === tempId ? newServerMsg : m)
      )
    } catch (err) {
      // 第四步 失败：手动回滚（非常容易写错！）
      setSendError(err)
      setMessages((prev) =&gt; prev.filter(m =&gt; m.id !== tempId))
      // 甚至还需要把输入框的内容恢复回去，这里省略了，太麻烦
    } finally {
      setIsSending(false)
    }
  }
 
  const activeCount = useMemo(() =&gt; {
    return messages.filter(m =&gt; !m.archived).length
  }, [messages]) // 小心翼翼写依赖。

  // 骨架屏又忘记做了。算了写个 Loading 以后再说
  if (isLoading) return &amp;#x3C;div&gt;Loading...&amp;#x3C;/div&gt;
  if (fetchError) return &amp;#x3C;div&gt;Error: {fetchError.message}&amp;#x3C;/div&gt;

  // 终于！吟唱了那么久，终于……不对！这么简单的 UI，为什么需要如此冗长的逻辑？我不理解 😭
  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;MessageList messages={messages} /&gt;
      &amp;#x3C;div className=&quot;status&quot;&gt;Active: {activeCount}&amp;#x3C;/div&gt;
      &amp;#x3C;form onSubmit={handleSendMessage}&gt;
        &amp;#x3C;input name=&quot;message&quot; disabled={isSending} /&gt;
        &amp;#x3C;SubmitButtonLegacy isSubmitting={isSending} /&gt;
        {sendError &amp;#x26;&amp;#x26; &amp;#x3C;div className=&quot;error&quot;&gt;Failed to send&amp;#x3C;/div&gt;}
      &amp;#x3C;/form&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总而言之，在传统 React 中，为了实现同样的功能，需要开发者手动编写大量的“胶水代码”来维护状态的一致性。
代码量不仅大，而且每一个手动维护的状态都是潜在的 Bug 来源。&lt;/p&gt;
&lt;p&gt;在这个框架之争愈演愈烈的时代，请用更少的代码，更强的鲁棒性，easy 地做正确的事，让我们为 React 正名。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Safe &amp; Consistent SWR / TanStack Query</title><link>https://eflx.top/blog/safe-consistent-swr</link><guid isPermaLink="true">https://eflx.top/blog/safe-consistent-swr</guid><description>Utilize client query libraries like SWR / TanStack Query in a secure, unified, and consistent manner.</description><pubDate>Sat, 17 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside, Spoiler } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;Why does this matter?&lt;/h2&gt;
&lt;p&gt;This article focuses on purely client-side applications. We aren&apos;t discussing React Server Components (RSC) or Server Functions here. We are dealing with scenarios where data fetching and mutation logic happen entirely within the browser&apos;s rendering lifecycle.&lt;/p&gt;
&lt;p&gt;In reality, many developers are still manually managing states like &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;isLoading&lt;/code&gt;, and &lt;code&gt;error&lt;/code&gt;, utilizing &lt;code&gt;useEffect&lt;/code&gt; to fetch data.
I don&apos;t need to lecture you on why that&apos;s brittle; by using libraries like &lt;code&gt;useSWR&lt;/code&gt; or others, we can implement these features more naturally while enjoying benefits like caching, global mutation, and request deduplication.&lt;/p&gt;
&lt;p&gt;For the sake of convenience, I&apos;ll use SWR for the examples below.
These libraries use a &quot;key&quot; to manage the cache. If a &quot;key&quot; already has corresponding data, all &lt;code&gt;useSWR&lt;/code&gt; hooks using that same &quot;key&quot; will share that data state.
You can &quot;mutate&quot; a specific &quot;key,&quot; causing all subscribers to re-fetch and update based on their memoization strategies.
The &quot;key&quot; can be an array or an object. This allows us to embed the parameters required for the request directly into the key, letting the &lt;code&gt;fetcher&lt;/code&gt; extract them. This handy feature saves us from constantly rebuilding callbacks via &lt;code&gt;useCallback&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The problem lies in inconsistency&lt;/h2&gt;
&lt;p&gt;Based on the features above, we can identify several potential pitfalls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Is the key strictly 1:1 with the fetcher?&lt;/strong&gt; If not, chaos ensues. Global mutations might fail silently (e.g., you think you mutated the correct key, but a component somewhere else is bound to a slightly different key structure).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is the key format standardized across the project?&lt;/strong&gt; Without a standard, maintainability plummets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is it decoupled enough?&lt;/strong&gt; If you write &lt;code&gt;(key) =&gt; updateUserName({ id: key[0], name: key[1] })&lt;/code&gt; directly as your fetcher, a simple change to the key structure or the backend API signature could trigger a massive refactoring headache.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, we need a unified component (or pattern) to manage the relationships between these keys, data fetching functions, mutation behaviors, and the targets of those mutations. It&apos;s actually easier than it sounds. (Confident.jpg)&lt;/p&gt;
&lt;h2&gt;Registering Keys and Fetchers&lt;/h2&gt;
&lt;p&gt;First, let&apos;s establish a mental model:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The source and type of the data will be called the &lt;strong&gt;endpoint&lt;/strong&gt;. If you&apos;ve written an SWR key like &lt;code&gt;[&apos;user&apos;, ...]&lt;/code&gt;, then &lt;code&gt;user&lt;/code&gt; is the endpoint.&lt;/li&gt;
&lt;li&gt;Let&apos;s say you have a &lt;code&gt;user&lt;/code&gt; endpoint. If you want to fetch a list of all users (perhaps with filters), you should separate that into a distinct endpoint like &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;userList&lt;/code&gt;, or &lt;code&gt;allUsers&lt;/code&gt;. Do not reuse the singular &lt;code&gt;user&lt;/code&gt; endpoint. Furthermore, any given endpoint should map 1:1 to a specific fetcher function. Whether you split endpoints fundamentally depends on your backend implementation.&lt;/li&gt;
&lt;li&gt;A key often includes a &lt;strong&gt;scope&lt;/strong&gt;, which refers to a specific constraint, such as a User ID.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let the battle with the TypeScript type system begin!&lt;/p&gt;
&lt;p&gt;Ideally, the frontend should be able to generate a key like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;useSWR(userKey({ scope: userId, ... }), ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, &lt;code&gt;userKey&lt;/code&gt; would be a function that accepts &lt;code&gt;TParams&lt;/code&gt; and returns &lt;code&gt;{ endpoint: &apos;user&apos; as const } &amp;#x26; TParams&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To simplify this and enforce a standard construction method, we can write a quick factory function.
Interestingly, we can inject a &lt;code&gt;$inferParams&lt;/code&gt; (and also &lt;code&gt;$inferKey&lt;/code&gt;!) property via type assertion. This allows us to access that &quot; &lt;code&gt;TParams&lt;/code&gt; &quot; type later using &lt;code&gt;typeof userKey.$inferParams&lt;/code&gt;. You&apos;ll see why this is useful in a moment.
I learn this from &lt;a href=&quot;https://orm.drizzle.team/docs/latest-releases/drizzle-orm-v0283#-added-tableinferselect--table_inferselect-and-tableinferinsert--table_inferinsert-for-more-convenient-table-model-type-inference&quot;&gt;drizzle-orm&lt;/a&gt;. Thanks!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const createKey =
  &amp;#x3C;TParams extends Record&amp;#x3C;string, unknown&gt; = { scope: string }&gt;() =&gt;
  &amp;#x3C;const T&gt;(endpoint: T) =&gt; {
    const fn = (params: TParams) =&gt; ({
      endpoint,
      ...params,
    })

    return fn as ((params: TParams) =&gt; { endpoint: T } &amp;#x26; TParams) &amp;#x26; {
      $inferParams: TParams,
      $inferKey: { endpoint: T } &amp;#x26; TParams
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When no generic parameter is provided, it defaults to requiring a &lt;code&gt;scope&lt;/code&gt; in the key construction. Usage looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const userKey = createKey()(&apos;user&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If an endpoint requires more parameters to fetch data, we define it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const userKey = createKey&amp;#x3C;{
  scope: string
  organization: string
}&gt;()(&apos;user&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we put the &lt;code&gt;endpoint&lt;/code&gt; type &lt;code&gt;const T&lt;/code&gt; at the &quot;top&quot; of the function definition alongside the params, TypeScript (I really hate these rituals to please the compiler...) fails to infer the &lt;code&gt;endpoint&lt;/code&gt; type automatically 😱.&lt;/p&gt;
&lt;p&gt;I recommend defining the key type simultaneously with the key creator. This gives us some useful union types for later use.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export type AppSWRKeys = typeof userKey.$inferKey
export type AppSWREndpoints = AppSWRKeys[&apos;endpoint&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Following SWR best practices—and to abstract the choice of fetcher away from the UI components—let&apos;s define a dedicated 👑 hook for the user. Here is where &lt;code&gt;.$inferParams&lt;/code&gt; shines by simplifying our type definitions 🥰.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const useUser = (params: typeof userKey.$inferParams, config?: SWRConfiguration) =&gt;
  useSWR(userKey(params), ({ scope: id }) =&gt; getUserById({ id }), config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s compare fetching user data before and after this transformation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Before SWR
const [data, setData] = useState&amp;#x3C;...&gt;()
const [error, setError] = useState&amp;#x3C;...&gt;()
const [loading, setIsLoading] = useState&amp;#x3C;...&gt;()
useEffect(() =&gt; {
  getUserById({ id: userId }).then(data =&gt; setData(data)).catch(...).finally(...)
}, [userId]) // ❌ Manually handling lifecycle via useEffect
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Naive SWR usage
// ❌ Closure captures external &apos;userId&apos;, avoiding useCallback/dependency array check issues, but it&apos;s brittle.
const { data, error, isLoading } = useSWR([&apos;user&apos;, userId], () =&gt; getUserById({ id: userId }))

// ❌ No unified constraints on key construction or how the fetcher receives args.
const { data, error, isLoading } = useSWR([&apos;user&apos;, userId], ([_, id]) =&gt; getUserById({ id }))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// The Refactored Way
// ✅ Auto-completion, consistent keys, no need to specify fetcher manually.
const { data, error, isLoading } = useUser({ scope: userId })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How about that? Everything just became clearer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the logic for fetching user data changes, you only update the &lt;code&gt;useUser&lt;/code&gt; definition.&lt;/li&gt;
&lt;li&gt;You don&apos;t need to dig through massive API docs to remember which endpoint to call.&lt;/li&gt;
&lt;li&gt;You don&apos;t need to think about how to structure the SWR key. Or rather, we moved that cognitive load upfront to when &lt;code&gt;useUser&lt;/code&gt; was defined.&lt;/li&gt;
&lt;li&gt;It looks clean and concise! ✨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Handling Mutation and Optimistic Updates&lt;/h2&gt;
&lt;p&gt;Directly &lt;code&gt;await&lt;/code&gt;-ing an operation function or POSTing to the backend inside your component is common behavior—but it&apos;s bad practice.
If you forget to mutate (revalidate), your UI state becomes stale. If the UI doesn&apos;t update, your user might start poking their iPad screen aggressively, potentially generating more hardware revenue for Apple!
Or perhaps they&apos;ll choose the gentle approach: spamming the refresh button... effectively launching a DDoS attack on your API 🤔?&lt;/p&gt;
&lt;p&gt;The best practice remains separation of concerns. Since we can&apos;t expect frontend developers to remember to mutate every single time, let&apos;s expose a trigger function from a hook that handles the mutation internally.&lt;/p&gt;
&lt;p&gt;Obviously, we need some utility functions first. For example, a helper to globally mutate any &lt;code&gt;useSWR&lt;/code&gt; keys that match a specific condition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type KeyMatcher = { key: Partial&amp;#x3C;AppSWRKeys&gt;; data?: unknown }

const isPartialMatch = (
  target: Record&amp;#x3C;string, unknown&gt;,
  partial: Record&amp;#x3C;string, unknown&gt;,
) =&gt; {
  if (typeof target !== &apos;object&apos; || target === null) return false
  return Object.keys(partial).every(
    (k) =&gt; k in target &amp;#x26;&amp;#x26; target[k] === partial[k],
  )
}

export const mutateGlobal = async (matchers: KeyMatcher | KeyMatcher[]) =&gt; {
  const conditions = Array.isArray(matchers) ? matchers : [matchers]
  
  await Promise.allSettled(
    conditions.map((c) =&gt;
      mutate((cacheKey: AppSWRKeys) =&gt; {
        if (typeof cacheKey !== &apos;object&apos; || cacheKey === null) return false
        return isPartialMatch(cacheKey, c.key)
      }, c.data),
    ),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await mutateGlobal({ key: ..., data: ... /* &apos;data&apos; is optional, used for optimistic updates */ })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we unified our mental model earlier, implementing the required mutation hook is now trivial.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function useUpsertUser(scope: string) {
  const { trigger } = useSWRMutation(
    userKey({ scope }),
    (
      key,
      {
        arg,
      }: {
        arg: Parameters&amp;#x3C;typeof upsertUser&gt;[0]
      },
    ) =&gt; upsertUser(arg),
    {
      onSuccess: (data) =&gt;
        mutateGlobal([
          { key: { endpoint: &apos;user&apos;, scope }, data }, 
          { key: { endpoint: &apos;users&apos; } }
        ]),
    },
  )

  return { upsertUser: trigger }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component is tightly coupled with business logic (backend API calls, function signatures, specific mutation conditions, etc.), thus we won&apos;t abstract it further.&lt;/p&gt;
&lt;p&gt;You can also add other data actions to this hook so the frontend can access them quickly.&lt;/p&gt;
&lt;p&gt;Furthermore, optimistic updates have become an essential consideration in modern app developments. Remember to use the &lt;code&gt;data&lt;/code&gt; property in the &lt;code&gt;mutateGlobal&lt;/code&gt; matcher.
Please note this relies on that the action succeeded with a returning. If a &quot;real&quot; or &quot;immediate&quot; optimistic update is needed, put the optimistic data in &lt;code&gt;trigger&lt;/code&gt; function.
Users will see the UI react instantly, faster than any shooting star 🌠 preventing them from making a wish on your loading spinner!
You can also add &lt;code&gt;onError&lt;/code&gt; handling or allow the frontend to pass optional &lt;code&gt;options&lt;/code&gt; that get piped through to &lt;code&gt;useSWRMutation&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The key to everything lies in establishing a unified, consistent mental model. Isolate the messy logic into a separate layer and leverage the power of React hooks.
Let me wish this helps you build safe, sustainable code before LLMs ruin your c...ode.... bro that&apos;s not funny.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>