<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Raffi Hovhannesian</title>
    <description>The latest articles on DEV Community by Raffi Hovhannesian (@raffi001).</description>
    <link>https://dev.to/raffi001</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F213192%2Fbbfbd504-afdb-4268-bda6-57e03a13ea4b.jpeg</url>
      <title>DEV Community: Raffi Hovhannesian</title>
      <link>https://dev.to/raffi001</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raffi001"/>
    <language>en</language>
    <item>
      <title>Model Collapse Isn't New, Nor Is It Specific to AI</title>
      <dc:creator>Raffi Hovhannesian</dc:creator>
      <pubDate>Thu, 08 Oct 2026 20:20:13 +0000</pubDate>
      <link>https://dev.to/raffi001/model-collapse-isnt-new-nor-is-it-specific-to-ai-3ik9</link>
      <guid>https://dev.to/raffi001/model-collapse-isnt-new-nor-is-it-specific-to-ai-3ik9</guid>
      <description>&lt;p&gt;We've all heard the usual complaint, that AI just remixes open-source code it was trained on. That's true, and it's been argued to death, but something quieter is happening, and I think it matters more.&lt;/p&gt;

&lt;p&gt;You open the chat and type "Help me build a note-taking app". AI suggests features, then a name, a color palette, a pricing model and a launch strategy. You pick the ones that you like and feel like the architect.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with that, until you zoom out and realize you didn't design anything. &lt;strong&gt;You just chose from a menu someone else wrote.&lt;/strong&gt; In some cases, your original goal has quietly been replaced by something you might or might not need.&lt;/p&gt;

&lt;p&gt;And it doesn't stop at your product. In a study of 1,506 people, those who wrote with an opinionated AI assistant didn't just write differently. Their own opinions shifted toward the AI's.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;When one entity has processed nearly all the information in the world, it develops a very specific sense of what's "correct": the best practice, the balanced take, the optimal answer. And it doesn't just advise you. It advises everybody.&lt;/p&gt;

&lt;p&gt;Millions of people, one oracle, one answer. Then they publish it, and it becomes tomorrow's training data.&lt;/p&gt;

&lt;p&gt;Researchers have already measured what happens next, at least inside the machines. In a 2024 &lt;em&gt;Nature&lt;/em&gt; paper, Ilia Shumailov and his colleagues found that training AI on AI-generated content &lt;em&gt;"causes irreversible defects in the resulting models, in which tails of the original content distribution disappear."&lt;/em&gt; They called it &lt;strong&gt;model collapse&lt;/strong&gt;.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The "tails" are the rare stuff: the unusual, the weird, the one-in-a-million. In other words, exactly the things that drive progress.&lt;/p&gt;

&lt;p&gt;The answer isn't wrong. In many ways, &lt;strong&gt;it &lt;em&gt;is&lt;/em&gt; the truth. That's the problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is an averaged truth. It is correct. It is polished. It is optimized for agreeableness. But, it lacks the dynamo of real life, it lacks &lt;strong&gt;the mistake that turned into a discovery&lt;/strong&gt; that no other entity in the world has discovered before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9aglzo0gksv4ns1w2gzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9aglzo0gksv4ns1w2gzv.png" alt="Model Collapse in Car Race" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We've seen this before, long before the machines.&lt;/p&gt;

&lt;p&gt;For centuries, much of Europe ran on a single source of truth: a sacred text, and an institution that interpreted it.&lt;sup id="fnref3"&gt;3&lt;/sup&gt; You didn't need to work things out for yourself. You asked, and you received the answer, the same answer everyone else received. This isn't a judgment on faith, which has given people meaning, community and a moral compass for thousands of years. It's about what happens to thinking when everyone consults the same source. Questions start to look like heresy, and variation starts to look like error. Interpretation becomes stable, coherent and very, very slow.&lt;sup id="fnref4"&gt;4&lt;/sup&gt; The tails disappear.&lt;/p&gt;

&lt;p&gt;The sacred text was never the problem (internet data). The monopoly on interpreting it was (AI version of truth).&lt;/p&gt;

&lt;p&gt;Then something broke the monopoly. The printing press put the text in more hands than any institution could supervise. People read it for themselves, disagreed and argued, and some of them were burned for it. The tails came back, and with them came the Reformation, the scientific revolution and the messy, noisy, self-correcting world we live in today.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Difference
&lt;/h2&gt;

&lt;p&gt;This time, in the age of AI, there's a twist.&lt;/p&gt;

&lt;p&gt;Nobody forced this oracle on us. No institution demands our obedience, and there's no penalty for heresy. You can close the tab whenever you like.&lt;/p&gt;

&lt;p&gt;We chose it, because it's convenient.&lt;/p&gt;

&lt;p&gt;And convenience is a far more effective authority than force. Nobody rebels against something that saves them time. There's nothing to smuggle when nothing is forbidden. There's just one very helpful answer. Each of us gets a better answer, and together we all get the same one.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The printing press broke a monopoly that was imposed on people. But nobody has invented a printing press for a new monopoly we all signed up for. Not yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Way Out
&lt;/h2&gt;

&lt;p&gt;The way out isn't to stop using AI. Use it, build with it and let it save you time.&lt;/p&gt;

&lt;p&gt;Just don't let it be the only voice in the room.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seek the second opinion:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
When the AI gives you the answer, ask a human. Better still, ask someone who disagrees with you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find the stranger:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Show your idea to someone outside your field, your feed and your industry. They haven't been trained on the same data you have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a heretic around:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Every team needs the person who says &lt;em&gt;"that's the best practice, so let's try the opposite."&lt;/em&gt; &lt;strong&gt;They'll be wrong most of the time. The time they're right is the discovery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protect your original goal:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Before you open the chat, write down what you actually wanted to build. When you're done, compare. &lt;strong&gt;If the AI's version won, make sure it won on merit and not on convenience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take advice, but don't outsource your taste:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
AI can tell you what's correct. Only you can decide what's &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Model collapse isn't new. Humanity has lived through it before and found its way out, through stubborn, inconvenient people who refused to take the same answer as everyone else.&lt;/p&gt;

&lt;p&gt;Be inconvenient.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Jakesch, M., Bhat, A., Buschek, D., Zalmanson, L., &amp;amp; Naaman, M. (2023). Co-Writing with Opinionated Language Models Affects Users' Views. &lt;em&gt;Proceedings of the 2023 CHI Conference on Human Factors in Computing Systems.&lt;/em&gt; &lt;a href="https://doi.org/10.1145/3544548.3581196" rel="noopener noreferrer"&gt;https://doi.org/10.1145/3544548.3581196&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Shumailov, I., Shumaylov, Z., Zhao, Y., Papernot, N., Anderson, R., &amp;amp; Gal, Y. (2024). AI models collapse when trained on recursively generated data. &lt;em&gt;Nature&lt;/em&gt;, 631, 755–759. &lt;a href="https://doi.org/10.1038/s41586-024-07566-y" rel="noopener noreferrer"&gt;https://doi.org/10.1038/s41586-024-07566-y&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Harari, Y. N. (2024). &lt;em&gt;Nexus: A Brief History of Information Networks from the Stone Age to AI.&lt;/em&gt; Random House. Harari argues that treating a holy book as infallible doesn't eliminate human fallibility. It shifts authority to its interpreters, and he contrasts this with science's self-correcting mechanisms.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Historians rightly caution against the "Dark Ages" myth. The medieval Church also preserved texts, founded universities and supported early science. The point here is narrower: what happens when a single source holds the monopoly on interpretation. See Numbers, R. L. (Ed.). (2009). &lt;em&gt;Galileo Goes to Jail and Other Myths about Science and Religion.&lt;/em&gt; Harvard University Press.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Eisenstein, E. L. (1979). &lt;em&gt;The Printing Press as an Agent of Change.&lt;/em&gt; Cambridge University Press.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Doshi, A. R., &amp;amp; Hauser, O. P. (2024). Generative AI enhances individual creativity but reduces the collective diversity of novel content. &lt;em&gt;Science Advances&lt;/em&gt;, 10(28), eadn5290. &lt;a href="https://doi.org/10.1126/sciadv.adn5290" rel="noopener noreferrer"&gt;https://doi.org/10.1126/sciadv.adn5290&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>software</category>
    </item>
    <item>
      <title>Optimistic UI is an architectural decision, not a minor UX tweak</title>
      <dc:creator>Raffi Hovhannesian</dc:creator>
      <pubDate>Sun, 27 Sep 2026 13:03:27 +0000</pubDate>
      <link>https://dev.to/raffi001/optimistic-ui-is-an-architectural-decision-not-a-minor-ux-tweak-576n</link>
      <guid>https://dev.to/raffi001/optimistic-ui-is-an-architectural-decision-not-a-minor-ux-tweak-576n</guid>
      <description>&lt;p&gt;While building Rulrmail, an open-source, email-first CRM, I wanted one small thing: when you star an email, the star should light up now, not after a round trip to a mail server somewhere.&lt;/p&gt;

&lt;p&gt;The technique has a friendly name: optimistic UI. It means the interface acts on what you asked for straight away, as if the server had already said yes. The request goes off in the background, and in the rare case the server says no, the interface quietly undoes the change and tells you why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhup1m2vi59fdzdz50xvy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhup1m2vi59fdzdz50xvy.png" alt="Restaurant waiter and optimistic UI" width="800" height="419"&gt;&lt;/a&gt;&lt;br&gt;
Same slow kitchen. You just didn't have to watch it. (Out of salmon? He comes back and says sorry. That's the rollback.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsgf96rnvrcqv8ws4ouy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsgf96rnvrcqv8ws4ouy8.png" alt="Waiting UI vs Optimistic UI" width="798" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most frameworks make it look like a one-liner. In case of Inertia (Laravel + Vue) it really is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/mailbox/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/actions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;star&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;optimistic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;starred&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uids&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it looks like a UI tweak. It isn't. It's the moment your app stops doing one thing at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpmm10xxfexqah0rtefp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpmm10xxfexqah0rtefp.png" alt="Where instant cracked layer by layer" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Clicks start overlapping
&lt;/h2&gt;

&lt;p&gt;Before, every click waited its turn. Now you can star three messages in a second. By default, a new visit cancels the one in flight, and the screen briefly "un-stars" the first message.&lt;/p&gt;

&lt;p&gt;The fix is to let requests run side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// don't cancel the other clicks&lt;/span&gt;
    &lt;span class="na"&gt;preserveState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// keep selection, scroll, open panes&lt;/span&gt;
    &lt;span class="na"&gt;optimistic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Feedback lags behind the change
&lt;/h2&gt;

&lt;p&gt;The star was instant, but the little "Starred · Undo" toast still waited for the server. The screen said done while the confirmation said wait.&lt;/p&gt;

&lt;p&gt;The fix flips who's in charge. The page shows the toast itself, with an id, and sends that id along. The server answers with the same id, so its toast updates the one on screen instead of adding a second one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toastId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;toasts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toastId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Starred 1 message.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toast_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toastId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The server's toast replaces the page's, or turns it into an error.&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;back&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'toast'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toastId&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Users act before the server has
&lt;/h2&gt;

&lt;p&gt;With an instant Undo, people click it before the original change has even reached the server. There's nothing to undo yet.&lt;/p&gt;

&lt;p&gt;So the page puts things back immediately, remembers the request, and sends the real undo the moment the server's answer arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;waitingForToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// send it when the answer comes&lt;/span&gt;
    &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Shared state starts racing
&lt;/h2&gt;

&lt;p&gt;Two requests in flight at once meant a message the server had stored in the session "for the next page load" got picked up by the wrong request and vanished. Anything that quietly assumed one request at a time broke. The cure is the same as above: the page keeps what it needs instead of relying on server leftovers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xv6xka14kbf2zudwtr9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xv6xka14kbf2zudwtr9.png" alt="Shared state explained at the kitchen pass" width="800" height="419"&gt;&lt;/a&gt;&lt;br&gt;
"For whoever asks next" only works when one person is asking. (The fix: write the table number on the plate. That's why every toast now has an id.)&lt;/p&gt;
&lt;h2&gt;
  
  
  5. The database notices
&lt;/h2&gt;

&lt;p&gt;Even the local database complained. SQLite allows one writer at a time, and our parallel requests collided until database is locked showed up in the logs.&lt;/p&gt;

&lt;p&gt;You might say: who cares about SQLite, that's just local development. Well, I do. It's a red flag anyway: if two requests can collide on my laptop, they can collide in production too, just less often and much harder to reproduce. Local is where that kind of bug is cheapest to catch. A few settings fixed it: wait for the lock instead of failing, let reads and writes happen side by side, and take the write lock up front.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/database.php&lt;/span&gt;
&lt;span class="s1"&gt;'sqlite'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// …&lt;/span&gt;
    &lt;span class="s1"&gt;'busy_timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// wait for the lock, don't fail&lt;/span&gt;
    &lt;span class="s1"&gt;'journal_mode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'wal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// readers and the writer don't block each other&lt;/span&gt;
    &lt;span class="s1"&gt;'synchronous'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'normal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'transaction_mode'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'IMMEDIATE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// a read that becomes a write can't wait&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. The last details: the illusion is fragile
&lt;/h2&gt;

&lt;p&gt;A loading bar for a change that's already on screen tells users you don't believe your own UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;router.post(url, data, { showProgress: false, optimistic: () =&amp;gt; changes })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a browser API that only exists on HTTPS (crypto.randomUUID()) silently broke every action on a plain-HTTP dev domain, while the tests, running on 127.0.0.1 (which counts as secure), stayed green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// works on plain HTTP too&lt;/span&gt;
    &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x40&lt;/span&gt;
    &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x3f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;Optimistic UI isn't a coat of paint. The moment the screen stops waiting for the server, you've built a system where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requests overlap,&lt;/li&gt;
&lt;li&gt;the screen and the server briefly disagree,&lt;/li&gt;
&lt;li&gt;users act on things that don't exist yet,&lt;/li&gt;
&lt;li&gt;and every layer, from the browser to the database, has to cope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plan it like an architecture change, not a feature. Decide early who owns the truth at each moment, how you roll back, and what happens when two things happen at once.&lt;/p&gt;

&lt;p&gt;Done well, nobody notices any of this. The star just lights up. That's the point.&lt;/p&gt;

</description>
      <category>optmiisticui</category>
      <category>vue</category>
      <category>inertiajs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A fully local Mailtrap style inbox for Laravel</title>
      <dc:creator>Raffi Hovhannesian</dc:creator>
      <pubDate>Mon, 05 Jan 2026 15:15:00 +0000</pubDate>
      <link>https://dev.to/raffi001/a-fully-local-mailtrap-style-inbox-for-laravel-1c64</link>
      <guid>https://dev.to/raffi001/a-fully-local-mailtrap-style-inbox-for-laravel-1c64</guid>
      <description>&lt;p&gt;I just released Mailpot, a local, dev-only mail inbox for Laravel. And now its installed on all my Laravel projects.&lt;/p&gt;

&lt;p&gt;It intercepts and stores sent emails so you can inspect them inside your app, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no Docker&lt;/li&gt;
&lt;li&gt;no SMTP setup&lt;/li&gt;
&lt;li&gt;no external services&lt;/li&gt;
&lt;li&gt;unlimited emails for free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think Mailtrap / Mailhog, but fully embedded in Laravel.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/rulr-dev/laravel-mailpot" rel="noopener noreferrer"&gt;https://github.com/rulr-dev/laravel-mailpot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d really appreciate feedback, issues, or feature ideas from other Laravel devs&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
