<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[angelo.dini.dev Blog]]></title><description><![CDATA[Developer Portfolio]]></description><link>https://angelo.dini.dev</link><generator>Next.js</generator><lastBuildDate>Mon, 12 Oct 2026 13:10:56 GMT</lastBuildDate><item><title><![CDATA[Fretfall Player, a Note Highway in Plain JavaScript]]></title><description><![CDATA[How Fretfall Player works: a 3D note highway for Guitar Pro tabs in plain ES modules, worked-out fingering, a recording lined up by FFT, and practice loops.]]></description><link>https://angelo.dini.dev/blog/fretfall-player/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/fretfall-player/</guid><pubDate>Sat, 26 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;notes-that-fall-towards-you&quot;&gt;Notes that fall towards you&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;A tab tells you which fret to press on which string. It's bad at telling you when, and it says nothing at all about which finger. You can read every number on the page and still play the song like someone reading a recipe out loud.&lt;/p&gt;&lt;p&gt;I play guitar. It was on the list of habits I tracked &lt;a href=&quot;/blog/necessary-apps/&quot;&gt;back in 2020&lt;/a&gt;. What a static page of tab can’t give you is the song and the next note at the same moment. Arcade-style note highways do exactly that: the notes come down a lane towards you and you play them when they reach the line. So I built one that opens ordinary tab files, the Guitar Pro and MusicXML kind that are already everywhere.&lt;/p&gt;
&lt;p&gt;It’s called &lt;a href=&quot;/projects/fretfall-player/&quot;&gt;Fretfall Player&lt;/a&gt;. It runs at &lt;a href=&quot;https://www.fretfall.com/&quot;&gt;fretfall.com&lt;/a&gt;, the code is on &lt;a href=&quot;https://github.com/fretfall/player&quot;&gt;GitHub&lt;/a&gt; under MIT, and it’s published to npm as &lt;code&gt;@fretfall/player&lt;/code&gt;. This post is a tour of how it works under the hood, because the interesting parts turned out to be the maths and not the UI.&lt;/p&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What it does&lt;/h3&gt;
&lt;p&gt;You open a Guitar Pro file (&lt;code&gt;.gp&lt;/code&gt;, &lt;code&gt;.gp3&lt;/code&gt; to &lt;code&gt;.gp5&lt;/code&gt;, &lt;code&gt;.gpx&lt;/code&gt;) or MusicXML (&lt;code&gt;.musicxml&lt;/code&gt;, &lt;code&gt;.mxl&lt;/code&gt;) and the notes start falling down a 3D fretboard. Hand positions are drawn on the neck, every note carries the finger that should play it, chord names sit beside the shapes, and the fret numbers are painted on the floor of the highway like road markings.&lt;/p&gt;
&lt;p&gt;Then you can drop the band’s recording on it (&lt;code&gt;.mp3&lt;/code&gt;, &lt;code&gt;.m4a&lt;/code&gt;, &lt;code&gt;.ogg&lt;/code&gt;, &lt;code&gt;.wav&lt;/code&gt; or &lt;code&gt;.flac&lt;/code&gt;) and the player works out on its own where in that recording the tab starts. From then on you play along with the real song instead of a synth.&lt;/p&gt;
&lt;p&gt;Beyond the highway there are two more views: a tablature, scrolling or a page at a time, with the rhythm written over the staff; and a notation page, which sets the tab as systems of whole bars with the technique marks, chord names and lyrics. For practice there are phrase loops, a speed control with a speed trainer, and a metronome. And there’s a band mode that opens every other part of the song in a window of its own, all playing in time.&lt;/p&gt;
&lt;h3 id=&quot;the-house-rules&quot;&gt;The house rules&lt;/h3&gt;
&lt;p&gt;Two rules are written in the README and enforced by CI, and they shape everything else:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No framework and no runtime dependencies.&lt;/strong&gt; Plain HTML and ES modules. The whole player is about 70 kB gzipped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No pull request may make it slower or bigger.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The second one is checked properly. &lt;code&gt;scripts/bench.mjs&lt;/code&gt; builds the player and the base branch, then runs both in turns on the same runner, so the machine’s ups and downs hit them alike. It measures frame time and draw calls of the highway, the tablature and the notation page over a whole song, how long lining up a recording takes, the fingering, and the gzipped size. More than 10% worse on frame or sync time, 5% on draw calls or 1% on size, and the build fails. A regression taken on purpose needs the &lt;code&gt;perf-ok&lt;/code&gt; label.&lt;/p&gt;
&lt;p&gt;The canvas in that benchmark is a &lt;code&gt;Proxy&lt;/code&gt; that draws nothing and only counts calls, so the number it reports is the time spent in the player’s own code and not in the GPU. It also means the benchmark runs in plain Node, no browser needed.&lt;/p&gt;
&lt;p&gt;There is one honest asterisk on “no dependencies”: reading and synthesising the tab files is done by &lt;a href=&quot;https://www.alphatab.net&quot;&gt;alphaTab&lt;/a&gt;, which the page loads from jsDelivr at runtime, together with its SoundFont. It’s not bundled and not in &lt;code&gt;package.json&lt;/code&gt;, but the player can’t open a Guitar Pro file without it. Writing a Guitar Pro parser from scratch was never on my list, and alphaTab does it well.&lt;/p&gt;
&lt;p&gt;The only dev dependencies are &lt;code&gt;esbuild&lt;/code&gt; and &lt;code&gt;oxlint&lt;/code&gt;. Tests are &lt;code&gt;node --test&lt;/code&gt; with plain &lt;code&gt;node:assert&lt;/code&gt;, no framework.&lt;/p&gt;
&lt;h3 id=&quot;the-modules&quot;&gt;The modules&lt;/h3&gt;
&lt;p&gt;There’s no build step for development beyond esbuild serving &lt;code&gt;src/&lt;/code&gt; on port 4410. The repository is small enough to describe in a table:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: The modules&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;player.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the page: opening files, alphaTab, the transport, practice tools, settings, band windows, the public hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;highway.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all three views drawn on a canvas: the 3D highway, the tablature and the notation page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;music.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pure helpers: tempo maps, tunings, note names, repeated chords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fingering.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hand positions and fingers for charts that don’t say&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sync.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lining a recording up with the chart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;themes.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the looks: colours, fonts, note shapes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mount.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the player in an element of someone else’s page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;music.js&lt;/code&gt;, &lt;code&gt;fingering.js&lt;/code&gt; and &lt;code&gt;sync.js&lt;/code&gt; touch no DOM at all, which is why they each have a test file next to them that runs in plain Node. The two big ones are &lt;code&gt;player.js&lt;/code&gt;, the glue, at about 3,500 lines, and &lt;code&gt;highway.js&lt;/code&gt; at about 2,800.&lt;/p&gt;
&lt;h3 id=&quot;the-highway-is-a-canvas-not-webgl&quot;&gt;The highway is a canvas, not WebGL&lt;/h3&gt;
&lt;p&gt;The 3D highway is a 2D canvas with a hand-rolled perspective camera. The comment at the top of &lt;code&gt;highway.js&lt;/code&gt; describes it better than I can: a camera looks down the highway from just behind the strike line, so the strings there read as a fretboard and the notes rise out of the distance. World units are fret widths, and time runs along the z axis.&lt;/p&gt;
&lt;p&gt;Every frame, a few constants turn time into distance. Notes travel 28 fret widths per second, and the highway shows 84 fret widths ahead of the strike line, so with the default settings you see three seconds of music coming at you:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;VH&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;900&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;HIGHWAY&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;84&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;NOTE_SPEED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;28&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;lookAhead&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;HIGHWAY&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;view&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;drawDistance &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NOTE_SPEED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;view&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;noteSpeed &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The projection itself is a one-liner per point: subtract the eye, take the dot product with the camera’s forward vector, and divide by it. Because that function runs thousands of times a frame, the camera vectors are unpacked into plain local variables before the loop instead of being read out of arrays each time. It’s the kind of thing you’d never write in a tutorial and it shows up straight away in the benchmark.&lt;/p&gt;
&lt;p&gt;The camera follows the hand. Each hand position coming up in the next three seconds gets framed, a big move zooms out to show both the old and the new position, and then it eases back in. The easing is a critically damped spring, the same maths as Unity’s &lt;code&gt;SmoothDamp&lt;/code&gt;, so the camera never lurches even when the target jumps.&lt;/p&gt;
&lt;h4 id=&quot;things-a-canvas-is-slow-at&quot;&gt;Things a canvas is slow at&lt;/h4&gt;
&lt;p&gt;Most of the comments in &lt;code&gt;highway.js&lt;/code&gt; are about what was too slow, which is my favourite kind of comment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shadow blur.&lt;/strong&gt; A glow with &lt;code&gt;shadowBlur&lt;/code&gt; is a separate pass for every shape, and it halved the frame rate. A glowing shape now gets its outline stroked three times underneath it, wider and fainter each time, which falls off the way a blur does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Big gradients.&lt;/strong&gt; Filling the floor with a gradient was the slowest thing to draw without a GPU. The floor is now an image one pixel wide with bands of flat colour, stretched over the floor without smoothing: one draw call where there used to be 128.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text.&lt;/strong&gt; Short text is set at quarter-pixel sizes and measured once per font and string, because a font at a size the canvas hasn’t seen before is slow to set up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading state back.&lt;/strong&gt; Reading &lt;code&gt;g.font&lt;/code&gt; back from the context is slow, so the code keeps track of what it last set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is clever on its own. It’s just the list of things a profiler finds once you have one, and adding &lt;code&gt;?perf&lt;/code&gt; to the URL turns on a frame profiler for exactly that reason.&lt;/p&gt;
&lt;h3 id=&quot;fingering-worked-out&quot;&gt;Fingering, worked out&lt;/h3&gt;
&lt;p&gt;Most tabs say which fret, but not which finger, and a note highway without fingers is only half the help. &lt;code&gt;fingering.js&lt;/code&gt; fills that in with about 60 lines of dynamic programming.&lt;/p&gt;
&lt;p&gt;The model is the classic one-finger-per-fret position. The index finger rests on the position fret and fingers one to four cover four frets:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;SPAN&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;fingerFor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fret&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fret &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; position &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The harder question is which position to be in. Notes are grouped by what’s played together (within five milliseconds of each other), open strings are skipped because they don’t pin the hand, and each group gets the list of positions that would reach all of its notes. Then the whole song is solved at once: every possible position of every group gets a cost, a shift costs &lt;code&gt;1 + 0.3&lt;/code&gt; per fret moved, a shift during a rest of more than 0.4 seconds costs half, and there’s a small preference for putting the index finger on the lowest note when it fits. Walk the cheapest path back from the end and you have a hand position for every moment of the song.&lt;/p&gt;
&lt;p&gt;It isn’t how a teacher would finger every passage, and I wouldn’t claim it is. But it keeps shifts few and short, which is most of what I want when I’m learning something new, and a chart that does say which finger wins over the guess.&lt;/p&gt;
&lt;h3 id=&quot;lining-up-the-recording&quot;&gt;Lining up the recording&lt;/h3&gt;
&lt;p&gt;This is the part I’m most pleased with. You drop an MP3 on the player and it has to answer two questions: at what second of the recording does the tab start, and is the band playing at exactly the tempo the tab says? Live recordings drift, tabs are transcribed by people, and a count-in or an intro of applause moves everything.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sync.js&lt;/code&gt; models the answer as a straight line:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;audio time = offset + ratio × chart time&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and looks for the &lt;code&gt;offset&lt;/code&gt; and &lt;code&gt;ratio&lt;/code&gt; that put the chart’s notes on the recording’s attacks. It goes like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Decode small.&lt;/strong&gt; The file is decoded with an &lt;code&gt;OfflineAudioContext&lt;/code&gt; at 11,025 Hz and mixed to mono. The comment says 11 kHz is plenty to find attacks, and it is.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build an onset envelope.&lt;/strong&gt; The samples are cut into frames of 1,024 with a hop of 128, each frame goes through a Hann window and a radix-2 FFT (written inline, about 25 lines), and the spectral flux between 150 Hz and 5 kHz says how sharply the sound changes at each moment. Whatever sticks out above the surrounding 0.8 seconds is an attack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coarse search.&lt;/strong&gt; For every tempo ratio from 0.9 to 1.1 in steps of 0.0025, the chart’s onsets become spikes on a grid, and their cross-correlation with the envelope is computed with FFTs. One pass scores every possible offset at once, which is the trick that makes it fast enough to run in a browser while you wait.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fine search.&lt;/strong&gt; Around the best coarse result, ratio and offset are searched directly in much smaller steps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence.&lt;/strong&gt; How far the best fit stands above the envelope’s average. Below about 2, the recording probably isn’t the song the tab describes, and the player says so instead of pretending.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The player then wraps the &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element in a small clock object, so everything else in the app (the loop, a seek, the band windows) asks for chart time and never sees the recording’s own:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;el&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;currentTime &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; offset&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; ratio &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; songOffset &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token function-variable function&quot;&gt;seek&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  el&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;currentTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; offset &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; ratio &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; songOffset &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;songOffset&lt;/code&gt; is a nudge in milliseconds on top, for when the fit is close but your ears disagree. &lt;code&gt;fretfall.snap()&lt;/code&gt; sets it automatically: it puts the tab’s first note on the loudest attack within 0.3 seconds of where the fit expects it, or on the recording’s first loud sound if the fit wasn’t sure. The comment admits that a count-in can win that second case, which is true and a little annoying.&lt;/p&gt;
&lt;p&gt;The audio element has &lt;code&gt;preservesPitch&lt;/code&gt; on, so slowing the recording down for practice doesn’t drop it into a lower key.&lt;/p&gt;
&lt;h3 id=&quot;practice-tools&quot;&gt;Practice tools&lt;/h3&gt;
&lt;p&gt;The practice tools are the point of the whole thing, and each of them is small.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phrase loops.&lt;/strong&gt; Above the highway, every bar of the song is a bar in a strip, and its height is how many notes are in it. Busy bars stand out at a glance, which is usually where a loop is needed. Drag across the strip and the loop snaps to whole bars (hold Shift for a free range). When playback passes its end, it jumps back to the start with one second of run-up, so you’re not dropped straight onto the first note.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Speed and the speed trainer.&lt;/strong&gt; Speed goes from 10% to 150% in steps of 10%. The speed trainer only switches on with a loop and a speed of 90% or less, and then it adds 10% every time the loop comes round, up to 100%, where it switches itself off. That’s the whole feature:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;loop &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; player&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;playing &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; loop&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;end &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;following&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  player&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;seek&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; loop&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;start &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// one second of run-up&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;training&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setSpeed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;speed &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// the speed trainer: faster each time round&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The metronome.&lt;/strong&gt; It clicks on the chart’s beats, not on a fixed tempo, so it follows tempo changes in the song. The clicks are short oscillator blips (1,600 Hz on the first beat of a bar, 1,000 Hz otherwise) scheduled on the Web Audio clock a little ahead of when they sound. The frame loop asks for the beats in the next tenth of a second and books them in advance, so the click keeps time however the frames fall. It follows the volume but not the mute, so you can mute the song and play against the click alone.&lt;/p&gt;
&lt;h3 id=&quot;band-mode&quot;&gt;Band mode&lt;/h3&gt;
&lt;p&gt;Band mode opens every other part of the song in a window of its own, and one window leads. The windows talk over a &lt;code&gt;BroadcastChannel&lt;/code&gt; called &lt;code&gt;fretfall-band&lt;/code&gt;: the leader sends the song, play, pause, seek, speed and its clock, and the others follow. It’s the simplest thing that works, and the code says so in a comment, with the obvious next step if it ever has to reach another machine:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ponytail: windows of one browser on one computer (BroadcastChannel), a server if players join from elsewhere&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; band &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BroadcastChannel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fretfall-band&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nobody has asked for a server yet, so there isn’t one.&lt;/p&gt;
&lt;h3 id=&quot;building-on-it&quot;&gt;Building on it&lt;/h3&gt;
&lt;p&gt;The player starts with no song of its own. The page around it opens the first one, which is what the demo’s front door does with its demo tab (an original practice piece of about two minutes that uses every notation the highway draws, from palm mutes to golpe). Once it’s ready, the player exposes &lt;code&gt;window.fretfall&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fretfall&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;ready&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'fretfall:ready'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ready&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'fretfall:song'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;detail&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;detail&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fretfall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; recording &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
fretfall&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;part &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most of what the controls do is settable on that object (&lt;code&gt;playing&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;speed&lt;/code&gt;, &lt;code&gt;loop&lt;/code&gt;, &lt;code&gt;metronome&lt;/code&gt;, &lt;code&gt;view&lt;/code&gt;) and one &lt;code&gt;fretfall:controls&lt;/code&gt; event fires whenever any of it changes. For a page that isn’t the player’s, a React app say, &lt;code&gt;mount(element)&lt;/code&gt; from &lt;code&gt;@fretfall/player/mount.js&lt;/code&gt; puts the whole player into an element, with its styles scoped to it, and &lt;code&gt;fretfall.dock(element)&lt;/code&gt; moves the header’s controls into the page’s own bar. There’s no unmount: the modules run once per page, so you hide the element instead of removing it. That’s written down in the README, so nobody has to discover it the hard way.&lt;/p&gt;
&lt;p&gt;Desktop apps are a different story, as I found out with &lt;a href=&quot;/blog/tauri-desktop-app/&quot;&gt;Tauri&lt;/a&gt;. This one is a static folder: &lt;code&gt;npx serve node_modules/@fretfall/player&lt;/code&gt; and you have it.&lt;/p&gt;
&lt;h3 id=&quot;what-still-bugs-me&quot;&gt;What still bugs me&lt;/h3&gt;
&lt;p&gt;The fingering is a model and not a teacher. It counts frets, so it knows nothing about stretches that are fine high up the neck and painful at the nut, or about a hand that would rather stay put and reach. The sync fits one straight line through the whole song, one offset and one tempo ratio, so a band that speeds up in the chorus is something it can only average over. And because alphaTab and its SoundFont come from a CDN, the first file you open waits on that download.&lt;/p&gt;
&lt;p&gt;The README says a bug report with a tab that reproduces it is as useful as a patch, and I mean it. If you play and have Guitar Pro files lying around, try it at &lt;a href=&quot;https://www.fretfall.com/&quot;&gt;fretfall.com&lt;/a&gt; and send me the tab that breaks it.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[A Desktop App with Tauri 2]]></title><description><![CDATA[Building a desktop app with Tauri 2, React and Rust. The command boundary, capabilities, bundling a 200 MB Java engine, licence gates, and the release pipeline.]]></description><link>https://angelo.dini.dev/blog/tauri-desktop-app/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/tauri-desktop-app/</guid><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-browser-was-never-going-to-do-this&quot;&gt;The browser was never going to do this&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Take a photo of a printed sheet-music part, get it back transposed into a different key and clef, with every accent and repeat intact, and print it. That's the whole product. It needs a 200 MB recognition engine written in Java, a PDF rasteriser, a local web server for the phone, and the ability to spawn and kill processes. None of that runs in a tab.&lt;/p&gt;&lt;p&gt;So I built a desktop application. It’s called Semitone, it’s not public yet, and it’s a React app inside Tauri 2 with a Rust backend that does everything the browser can’t. This post is the Tauri half: what a React developer needs to know, what I’d do again, and the bits that ate whole days.&lt;/p&gt;
&lt;p&gt;The example that started the project is a &lt;code&gt;1. Flügelhorn in B&lt;/code&gt; part from a brass-band folder that has to be played from bass clef in C. Today the options are transposing by hand, re-entering it in a notation editor, or not playing. I’ll leave the music side for another post, because the desktop side is plenty.&lt;/p&gt;
&lt;h3 id=&quot;what-tauri-actually-is&quot;&gt;What Tauri actually is&lt;/h3&gt;
&lt;p&gt;Tauri gives you a native window with a system webview in it (WebKit on macOS, WebView2 on Windows) and a Rust process behind it. Your frontend is whatever you’d build for the web. Mine is React 19, Vite, &lt;a href=&quot;/blog/tailwind-css-v4/&quot;&gt;Tailwind v4&lt;/a&gt; and shadcn/ui, with Vitest for tests, and if you removed Tauri from the repository it would still start in a browser and render every screen. The Rust side exposes functions the frontend can call, and that’s the entire contract.&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; create tauri-app@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That scaffolds both halves. The frontend lives in &lt;code&gt;src/&lt;/code&gt;, the native side in &lt;code&gt;src-tauri/&lt;/code&gt;, and &lt;code&gt;tauri.conf.json&lt;/code&gt; describes the window, the bundle and the security policy. The dev loop is &lt;code&gt;npm run tauri dev&lt;/code&gt;, which starts Vite on a fixed port and opens a window pointed at it. Hot reload works. Rust changes recompile and restart the window, which takes a few seconds and is the one place where the loop is slower than a web project.&lt;/p&gt;
&lt;p&gt;Two Vite settings matter and the template sets both. The port is fixed and strict, because Tauri expects to find the dev server where it said it would be, and Vite is told to ignore &lt;code&gt;src-tauri/&lt;/code&gt; so a Rust edit doesn’t trigger a frontend rebuild.&lt;/p&gt;
&lt;h3 id=&quot;tauri-or-electron&quot;&gt;Tauri or Electron&lt;/h3&gt;
&lt;p&gt;The question every React developer asks first, so here is the answer before the details. I chose Tauri and would again, but not for the reasons the comparison articles lead with.&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Tauri or Electron&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tauri 2&lt;/th&gt;
&lt;th&gt;Electron&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Renders with&lt;/td&gt;
&lt;td&gt;the webview the OS already has: WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux&lt;/td&gt;
&lt;td&gt;its own copy of Chromium, bundled into every app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;a Rust process, functions exposed as commands&lt;/td&gt;
&lt;td&gt;a Node.js process, the same JavaScript as the frontend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you ship&lt;/td&gt;
&lt;td&gt;your app and a thin runtime; Semitone’s own binary, frontend, fonts and engraver come to 8.4 MiB compressed&lt;/td&gt;
&lt;td&gt;your app plus a browser and a Node runtime, so the installer is heavy before you have written a line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering&lt;/td&gt;
&lt;td&gt;can differ per platform, because three engines&lt;/td&gt;
&lt;td&gt;identical everywhere, because one engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;iOS and Android builds from the same project&lt;/td&gt;
&lt;td&gt;desktop only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;denied by default, granted per window in a capabilities file&lt;/td&gt;
&lt;td&gt;the renderer can be locked down, but you do the locking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Age&lt;/td&gt;
&lt;td&gt;younger; signing, updater and plugins work, and you will read their source&lt;/td&gt;
&lt;td&gt;older; the signing and update story has been walked by thousands of apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Size and memory are the numbers everyone quotes, and they are real: Electron carries a copy of Chromium and Node in every app, Tauri borrows the browser the OS already has. For Semitone the argument barely registers. A 200 MB recognition engine sits next to the binary either way, and nine tenths of every download is Java. That table would have looked much the same with Electron, give or take a browser.&lt;/p&gt;
&lt;p&gt;What decided it was the backend. Everything hard in this app is process management, file handling and a PDF rasteriser, and I wanted that in a language whose compiler takes ownership seriously and refuses to build a data race. Rust was the point, not a consequence of picking Tauri.&lt;/p&gt;
&lt;p&gt;Where I would pick Electron: a UI that must look identical on every machine, because three webviews are three renderers and the CSS fix you make on macOS is a new bug on Windows; or a backend that leans on the Node ecosystem, where the library you need exists in npm and not on crates.io. Neither applied here. The rendering differences are the honest downside in that table, and they are why the web build of this app is a plausible future rather than a promise.&lt;/p&gt;
&lt;h3 id=&quot;commands-the-door-between-the-two-worlds&quot;&gt;Commands, the door between the two worlds&lt;/h3&gt;
&lt;p&gt;A command is a Rust function with an attribute on it:&lt;/p&gt;
&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token attribute attr-name&quot;&gt;#[tauri::command]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;host_info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HostInfo&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;HostInfo&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        platform&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;std&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;consts&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;OS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        app_version&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token macro property&quot;&gt;env!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CARGO_PKG_VERSION&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register it in the builder, and the frontend calls it by name:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; invoke &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@tauri-apps/api/core'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; info &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;invoke&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;HostInfo&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'host_info'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Arguments and return values go through JSON, so anything that derives &lt;code&gt;Serialize&lt;/code&gt; on the Rust side arrives as a plain object. The naming convention differs between the two languages, so nearly every struct I return carries &lt;code&gt;#[serde(rename_all = &amp;quot;camelCase&amp;quot;)]&lt;/code&gt;, and errors are enums tagged with a &lt;code&gt;code&lt;/code&gt; field so the frontend can switch on them rather than parse sentences.&lt;/p&gt;
&lt;p&gt;Long work must not run on the thread that owns the window. Marking a command &lt;code&gt;async&lt;/code&gt; puts it on Tauri’s runtime, and CPU-bound work goes one step further to the blocking pool:&lt;/p&gt;
&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token attribute attr-name&quot;&gt;#[tauri::command]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;export_pdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;svg_pages&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Vec&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; destination&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token namespace&quot;&gt;tauri&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;async_runtime&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;spawn_blocking&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;move&lt;/span&gt; &lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; pdf &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;svg_pages_to_pdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;svg_pages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map_err&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;e&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token namespace&quot;&gt;std&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;destination&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pdf&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map_err&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;e&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map_err&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;e&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laying out a twenty-page part is not something to do on a thread other commands are waiting on, and the frontend’s progress bar is one of those commands.&lt;/p&gt;
&lt;p&gt;For the other direction, Rust to frontend, there are events. The recognition engine runs for minutes and writes a log, so the Rust side follows the log and emits progress events that the React side subscribes to with &lt;code&gt;listen&lt;/code&gt;. Same mechanism when the phone submits a photo, and when a second document is double-clicked while the app is already open.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-id-keep-even-if-i-threw-everything-else-away&quot;&gt;The rule I’d keep even if I threw everything else away&lt;/h3&gt;
&lt;p&gt;Nothing in the frontend imports &lt;code&gt;@tauri-apps/*&lt;/code&gt; except one folder, &lt;code&gt;src/platform/&lt;/code&gt;. It exports a &lt;code&gt;Host&lt;/code&gt; interface, every &lt;code&gt;invoke&lt;/code&gt; in the application lives behind it, and the rest of the app asks for &lt;code&gt;getHost()&lt;/code&gt; and doesn’t know what answers.&lt;/p&gt;
&lt;p&gt;The reason is that a web version is a plausible future, and with this boundary it is one module swap: an HTTP-backed host instead of a Tauri-backed one. Without the boundary it’s a rewrite. So it is enforced by ESLint rather than remembered:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'src/**/*.{ts,tsx}'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;ignores&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'src/platform/**'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;'no-restricted-imports'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'error'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;patterns&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'@tauri-apps/*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Reach the host through src/platform instead of calling Tauri directly.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second lint rule in that config is the one I’d recommend to anyone shipping a product in two languages. &lt;code&gt;react/jsx-no-literals&lt;/code&gt; with &lt;code&gt;noStrings: true&lt;/code&gt; makes a hardcoded user-facing string a lint error. Every string comes from a catalog through &lt;code&gt;t()&lt;/code&gt;, the English catalog is the authoritative set of keys, and the German type is derived from it, so a missing translation is a type error rather than a blank label found by a user. I wrote about switching to &lt;a href=&quot;/blog/biome/&quot;&gt;Biome&lt;/a&gt; last year and I still like it. This project stayed on ESLint because both of those rules are the reason the codebase has the shape it has, and I wasn’t going to give them up for a faster linter.&lt;/p&gt;
&lt;h3 id=&quot;capabilities-or-why-your-dialog-doesnt-open&quot;&gt;Capabilities, or why your dialog doesn’t open&lt;/h3&gt;
&lt;p&gt;Tauri 2 replaced the old allowlist with capabilities. A JSON file per window says which permissions the webview gets, and by default it gets almost nothing:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;identifier&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;default&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;windows&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;permissions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;core:default&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;dialog:allow-save&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;dialog:allow-open&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;dialog:allow-ask&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;dialog:allow-message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;core:window:allow-destroy&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the first thing to look at when a plugin call silently does nothing. Your own commands don’t need entries here, but every plugin API does, and the error when you forget is not always loud. The model is sound: the webview is running content you control today and possibly content you don’t tomorrow, and a save dialog it wasn’t granted is a save dialog it can’t open.&lt;/p&gt;
&lt;p&gt;The content security policy lives next to it in &lt;code&gt;tauri.conf.json&lt;/code&gt;. Mine needed &lt;code&gt;&amp;#39;wasm-unsafe-eval&amp;#39;&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt; because the engraving library is WebAssembly, &lt;code&gt;blob:&lt;/code&gt; in &lt;code&gt;worker-src&lt;/code&gt; because it runs in a worker, and the &lt;code&gt;asset:&lt;/code&gt; protocol in &lt;code&gt;img-src&lt;/code&gt; so the app can show a page it wrote to disk. Each of those fails silently when it’s missing: a blank pane and nothing in the console.&lt;/p&gt;
&lt;h3 id=&quot;bundling-a-200-mb-java-program-you-are-not-allowed-to-touch&quot;&gt;Bundling a 200 MB Java program you are not allowed to touch&lt;/h3&gt;
&lt;p&gt;Recognition is done by Audiveris, an open-source optical music recognition engine. It is written in Java, it ships with its own trimmed runtime, and it is AGPL. Semitone is not.&lt;/p&gt;
&lt;p&gt;That combination is workable only under strict process separation, and I wrote the rules down before writing the code. Audiveris runs as a subprocess. Arguments go in on the command line, MusicXML comes back out of a directory, and that is the whole interface. No JVM linked into my process, no calls into its classes, no parsing its project files, no source patches, no forks. The launcher and its runtime are vendored into the bundle by a script that downloads the exact upstream release, checks it against the digest upstream publishes, and unpacks it without modification.&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run audiveris:vendor&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is deliberately absent from the licence inventory, because the inventory describes what is compiled or bundled &lt;em&gt;into&lt;/em&gt; Semitone and the whole position rests on the answer being “none of it”. The position itself is written up for a lawyer to read before anyone pays for this. I’m a developer, not a lawyer, and the honest version of that sentence is that I built the process boundary as narrow as it can be and then stopped guessing.&lt;/p&gt;
&lt;p&gt;On the Rust side the interesting problems are the ones every subprocess has and nobody plans for. A run takes minutes, so the log is followed to make the wait legible. The user can cancel, so the child is in its own process group and the signal reaches the whole tree; on Windows that means &lt;code&gt;taskkill&lt;/code&gt;, an external program that can be missing, with &lt;code&gt;Child::kill&lt;/code&gt; as the fallback. Quitting during a run must not leave an orphan JVM behind. And the working directory is removed on every path out, so a cancelled job leaves nothing on disk.&lt;/p&gt;
&lt;h3 id=&quot;the-engraver-and-a-different-licence&quot;&gt;The engraver, and a different licence&lt;/h3&gt;
&lt;p&gt;Engraving, turning MusicXML into a picture of music, is done by Verovio, compiled to WebAssembly and running in a Web Worker so the UI stays responsive. Verovio is LGPL, and its WASM is inlined into its JavaScript as a string, so bundling it into the app chunk would be a lot closer to static linking than to dynamic.&lt;/p&gt;
&lt;p&gt;The answer is to not bundle it. A script copies the two files verbatim out of &lt;code&gt;node_modules&lt;/code&gt; into &lt;code&gt;public/&lt;/code&gt;, where Vite treats them as static assets loaded at runtime, and it fails the build if the version in &lt;code&gt;package.json&lt;/code&gt; isn’t pinned exactly. It runs on &lt;code&gt;predev&lt;/code&gt; and &lt;code&gt;prebuild&lt;/code&gt;, so nobody has to remember it.&lt;/p&gt;
&lt;p&gt;I mention both of these because licensing is the part of a desktop app that a web developer has never had to think about. On the web you serve, you don’t distribute. The moment you ship a binary, every dependency is something you’re conveying, and the difference between “an aggregate” and “a combined work” decides whether you have to publish your own source.&lt;/p&gt;
&lt;h3 id=&quot;the-licence-gate&quot;&gt;The licence gate&lt;/h3&gt;
&lt;p&gt;Which is why there’s a CI job for it. Two scripts generate an inventory of every npm package and every Rust crate that ends up in the bundle, with its licence text, and a third checks them against a list of what we permit. Not what we recognise, what we permit. AGPL, SSPL, Commons Clause, non-commercial terms and packages with no licence declared all fail the build, and so does anything nobody has thought of yet, because it isn’t on the list.&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run licences:npm
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run licences:cargo
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run licences:check&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The committed inventories have to match the tree, so CI regenerates them and diffs. The app shows the whole inventory under a Licences entry in the header, which is about 750 kB of licence text in a chunk of its own that Vite would otherwise warn about on every build.&lt;/p&gt;
&lt;p&gt;It also shaped the Rust dependencies more than I expected. The PDF rasteriser is &lt;code&gt;hayro&lt;/code&gt;, a pure Rust crate, because the obvious choices are MuPDF and Poppler and both are copyleft, and the third is a C++ blob I’d have to vendor per platform. Most crates have &lt;code&gt;default-features = false&lt;/code&gt;, partly for size and partly because the default feature set of an image library is twenty decoders, every one of them a parser running on a file a stranger sent you.&lt;/p&gt;
&lt;h3 id=&quot;things-i-measured-instead-of-assumed&quot;&gt;Things I measured instead of assumed&lt;/h3&gt;
&lt;p&gt;The release profile has &lt;code&gt;panic = &amp;quot;unwind&amp;quot;&lt;/code&gt; where the template had &lt;code&gt;abort&lt;/code&gt;. Abort is smaller. But the app runs two parsers over files that arrive from outside, images and PDFs, and the “we show a message rather than crash” behaviour only exists if a panicking task can be caught. With abort, the same panic takes the process down with the user’s corrected part in it, unsaved since the last thirty-second autosave. I measured the cost: 9.27 MB to 11.05 MB, plus 19 percent on my binary. Against the 200 MB engine sitting next to it, that’s under one percent of the download. Crash safety on hostile input is worth nine tenths of one percent.&lt;/p&gt;
&lt;p&gt;The updater was the other one. Tauri’s updater doesn’t patch, it replaces the whole bundle. So an update from 0.1.0 to 0.2.0 downloads 101.5 MiB, of which 93 MiB is a recognition engine that hasn’t changed. Semitone’s own binary, frontend, fonts and engraver come to 8.4 MiB compressed. The update is twelve times larger than it needs to be, and for now that stands, because splitting the engine into its own installable piece is a real build and the release cadence doesn’t justify it yet. The updater itself added 4.6 MiB to the binary, which is what an HTTP client, a TLS stack and a tar reader weigh. It’s written down with the numbers so the next person doesn’t re-measure it.&lt;/p&gt;
&lt;h3 id=&quot;ci-and-the-release-pipeline-that-runs-with-no-secrets&quot;&gt;CI, and the release pipeline that runs with no secrets&lt;/h3&gt;
&lt;p&gt;Four jobs. A fast one on Ubuntu with no Rust toolchain: format, lint, typecheck, tests, the fixture and inventory checks, the licence gate. A Rust one, also on Ubuntu because it’s the cheapest place to run Clippy with &lt;code&gt;-D warnings&lt;/code&gt; and the unit tests, including a golden-PDF test that catches print regressions nobody would otherwise notice until they printed. A build matrix for the two platforms we ship, macOS on Apple Silicon and Windows x64, which vendors the engine, builds the installer, weighs it against a committed size budget and uploads it. And a nightly job on macOS that runs the real engine on a real picture of music and checks that MusicXML comes out, because that’s too slow and too large for every push. The Windows build only exists in CI. You cannot build it on a Mac.&lt;/p&gt;
&lt;p&gt;I’ve written about &lt;a href=&quot;/blog/docker/&quot;&gt;containerising a Next.js app&lt;/a&gt; and the shape here is the same one: the artefact you test is the artefact you ship, or the test is theatre.&lt;/p&gt;
&lt;p&gt;The release workflow is the part I’m most pleased with, for a reason that has nothing to do with code. It runs end to end with &lt;strong&gt;no secrets configured at all&lt;/strong&gt;. No signing certificate, no Apple account, no updater key. It builds both installers, weighs them, labels them &lt;code&gt;UNSIGNED&lt;/code&gt; in the artefact name, and refuses to publish. A release pipeline nobody can exercise until the certificates arrive is a pipeline nobody has tested, and the day the certificates arrive is the worst possible day to find out it doesn’t work. The label has three states, not two, because &lt;code&gt;signed-not-notarized&lt;/code&gt; on macOS still stops at Gatekeeper with a different message and the same outcome.&lt;/p&gt;
&lt;p&gt;Two small things from that pipeline that I’d have paid to know in advance. The macOS runner’s bash is 3.2, where expanding an empty array under &lt;code&gt;set -u&lt;/code&gt; is an error, which the modern bash on your own machine will never show you. And Tauri’s &lt;code&gt;.dmg&lt;/code&gt; step drives Finder over AppleScript to lay out the window, which stalls on a permission prompt the first time you run it on a machine. &lt;code&gt;CI=true&lt;/code&gt; skips that cosmetic step, and the runner sets it for you, so this only ever bites locally.&lt;/p&gt;
&lt;h3 id=&quot;the-phone&quot;&gt;The phone&lt;/h3&gt;
&lt;p&gt;One feature that’s worth a paragraph because the obvious approach is wrong. You can photograph a part with your phone and have it land in the app: the app starts a small web server on the LAN, shows a QR code, and the phone opens the page. My first instinct was a live camera preview with &lt;code&gt;getUserMedia&lt;/code&gt;. That requires a secure context, a phone hitting &lt;code&gt;http://192.168.x.x:port&lt;/code&gt; doesn’t have one, and a self-signed certificate produces a full-page warning that reads as a broken app. A plain &lt;code&gt;&amp;lt;input type=&amp;quot;file&amp;quot; capture=&amp;quot;environment&amp;quot;&amp;gt;&lt;/code&gt; needs no secure context and uses the phone’s own, better, camera app. Less code and a nicer photo 🎺.&lt;/p&gt;
&lt;h3 id=&quot;what-isnt-done&quot;&gt;What isn’t done&lt;/h3&gt;
&lt;p&gt;The thing I should say plainly: the go/no-go for this whole product, whether correcting recognition output on real photographs actually beats transposing by hand, has not been run. It needs eight to ten real photographed band parts and a half-day against a protocol that’s written and ready. Everything downstream of it is built. That order is backwards and I know it. The MusicXML-in, transposed-part-out path stands on its own if the answer is no, which is the excuse I’m using.&lt;/p&gt;
&lt;p&gt;Two release blockers are open and named: the updater has no public key yet, and the AGPL written offer inside the bundle still names a placeholder address. Both need a person, not a build.&lt;/p&gt;
&lt;p&gt;Would I use Tauri again? Yes, without much hesitation. The window is the OS’s own webview, the binary is small, the command boundary is simple, and Rust on the other side of it is a good deal more pleasant than I expected for a job that is mostly “run this program and read its output”. The parts that cost time were never Tauri. They were the parts of shipping software to strangers’ machines that the web had let me forget about.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Godot for Web Developers]]></title><description><![CDATA[A week into building an RTS in Godot 4 as a web developer. GDScript with static typing, headless tests, and why the simulation must not know the engine exists.]]></description><link>https://angelo.dini.dev/blog/godot-for-web-developers/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/godot-for-web-developers/</guid><pubDate>Fri, 31 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-genre-i-had-no-business-starting-with&quot;&gt;A genre I had no business starting with&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;On the 23rd of July I opened an empty folder and decided to build a real-time strategy game. Not a prototype of a mechanic, an actual RTS: base building, an economy, fog of war, an AI opponent that has to find you. I have written a lot of software. None of it had a frame budget.&lt;/p&gt;&lt;p&gt;I picked Godot 4, which I had never used, and GDScript, which I had never written. Eight days later the repository has 316 commits, 91 test files and a match you can lose. This post is what a web developer learns in the first week, written while it’s still fresh and I still remember what confused me.&lt;/p&gt;
&lt;p&gt;The game is called &lt;a href=&quot;https://github.com/FinalAngel/hammerfest&quot;&gt;Hammerfest&lt;/a&gt;. It’s flat, isometric and single player, in the spirit of the Command &amp;amp; Conquer games I grew up on and the Kingdom Rush look I still like. The plan is that it eventually ships on Steam. The plan for the first slice is much smaller: one level, and an honest answer to whether expanding a base under pressure is fun for twenty minutes.&lt;/p&gt;
&lt;h3 id=&quot;why-godot-and-not-a-browser&quot;&gt;Why Godot and not a browser&lt;/h3&gt;
&lt;p&gt;My instinct was TypeScript and a canvas library, because that is what I know and because everything would be text. I talked myself out of it in an afternoon.&lt;/p&gt;
&lt;p&gt;Godot hands you the parts I would otherwise write badly: an editor for maps, audio, particles, a profiler, and an export to desktop that is one button. I estimated two to four months of plumbing saved, and given how slow I am at anything involving a GPU, that is probably optimistic in my favour. The cost is an editor that a lot of your work goes through, which I’ll come back to, because it turned out to matter less than I feared.&lt;/p&gt;
&lt;p&gt;GDScript over C# was easier. It has no compile step, it’s what almost all Godot 4 material is written in, and with static typing switched on it stops being the loose scripting language people remember from Godot 3.&lt;/p&gt;
&lt;pre class=&quot;language-gdscript&quot;&gt;&lt;code class=&quot;language-gdscript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; speed&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.0&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; goal_cell&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Vector2i&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Vector2i&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ZERO&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;world&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;World&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Type every declaration. The project settings have warnings for untyped declarations and unsafe property access, and I set both to error on day one. GDScript’s type inference is fine, but an untyped &lt;code&gt;var&lt;/code&gt; in a function that runs thirty times a second for every unit is a Variant box being allocated and torn down thirty times a second for every unit, and the profiler showed me that within a day. More on that below.&lt;/p&gt;
&lt;p&gt;One thing that surprised me: GDScript is indented with tabs by convention, and the editor inserts them. I gave in. It’s a tab project now.&lt;/p&gt;
&lt;h3 id=&quot;the-one-rule-the-whole-thing-hangs-on&quot;&gt;The one rule the whole thing hangs on&lt;/h3&gt;
&lt;p&gt;If I had to keep a single decision from the first week it would be this: &lt;strong&gt;the simulation does not know Godot exists.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Everything in &lt;code&gt;sim/&lt;/code&gt; is a plain &lt;code&gt;RefCounted&lt;/code&gt; class. No &lt;code&gt;Node&lt;/code&gt;, no scene tree, no physics, no input, no wall clock, no unseeded randomness. Godot’s node classes read from the simulation and draw it. They never own state. In web terms it’s “the business logic knows nothing about React”, which every React developer nods at and few of us actually enforce.&lt;/p&gt;
&lt;p&gt;I enforce it with a shell script in CI. It greps &lt;code&gt;sim/&lt;/code&gt; for the banned symbols (&lt;code&gt;PhysicsServer&lt;/code&gt;, &lt;code&gt;CharacterBody&lt;/code&gt;, &lt;code&gt;NavigationServer&lt;/code&gt;, &lt;code&gt;Input.&lt;/code&gt;, &lt;code&gt;Time.get_ticks&lt;/code&gt;, &lt;code&gt;randi&lt;/code&gt;, and so on), and it checks that every &lt;code&gt;extends&lt;/code&gt; in that folder resolves to &lt;code&gt;RefCounted&lt;/code&gt;, &lt;code&gt;Object&lt;/code&gt;, or a class declared inside &lt;code&gt;sim/&lt;/code&gt; itself. Anything else fails the build. Banning &lt;code&gt;Node&lt;/code&gt; subclasses by name is a losing battle, Godot has dozens, so the allowlist runs the other way. There’s an escape hatch comment, &lt;code&gt;# purity-ignore&lt;/code&gt;, and it has been used zero times.&lt;/p&gt;
&lt;p&gt;An invariant nobody enforces is a promise, and this one is very easy to break by reflex. &lt;code&gt;Area2D&lt;/code&gt; overlap queries are right there, they are how every tutorial finds “what is near this unit”, and they are exactly the thing you cannot use if you want the next section to be true.&lt;/p&gt;
&lt;h4 id=&quot;what-the-split-buys&quot;&gt;What the split buys&lt;/h4&gt;
&lt;p&gt;It sounds like architectural fussiness. It is the cheapest thing in the project, and it bought four concrete things in week one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests run headless, in milliseconds, with no window. The whole suite runs on every commit on a Linux runner with no GPU.&lt;/li&gt;
&lt;li&gt;Save and load is “serialise the world”. There is no per-feature save code, and there never will be.&lt;/li&gt;
&lt;li&gt;Two AIs can play each other at a few hundred times real time, which turns “is the AI any good” from an opinion into a number.&lt;/li&gt;
&lt;li&gt;The renderer got swapped twice, and &lt;code&gt;sim/&lt;/code&gt; did not change by one line either time. I’ll get to that.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;fixed-timestep-and-why-your-game-is-otherwise-a-different-game-on-every-machine&quot;&gt;Fixed timestep, and why your game is otherwise a different game on every machine&lt;/h3&gt;
&lt;p&gt;Web developers meet &lt;code&gt;requestAnimationFrame&lt;/code&gt; and learn to multiply movement by the delta since last frame. That works for a spinner. It’s wrong for a simulation.&lt;/p&gt;
&lt;p&gt;If the world advances by however much real time has passed, a 144 Hz machine and a 40 Hz machine run different games. Floating point doesn’t associate, so the positions drift apart, so the fights resolve differently, so a save from one laptop loads into a slightly different match on another. Replays are impossible. Bugs are irreproducible by construction.&lt;/p&gt;
&lt;p&gt;The simulation in Hammerfest advances in fixed ticks, thirty per second, and never by a delta. A &lt;code&gt;Node&lt;/code&gt; in the presentation layer accumulates real elapsed time and spends it in whole ticks:&lt;/p&gt;
&lt;pre class=&quot;language-gdscript&quot;&gt;&lt;code class=&quot;language-gdscript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class_name&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MatchRunner&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Node&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MAX_TICKS_PER_FRAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; world&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;World&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; _pending&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Array&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; _accumulator&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;_process&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;delta&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	_accumulator &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; delta
	&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; ticks&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; _accumulator &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; World&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;TICK_DELTA&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; ticks &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MAX_TICKS_PER_FRAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
		world&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_pending&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		_pending&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
		_accumulator &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; World&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;TICK_DELTA&lt;/span&gt;
		ticks &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pending array is the commands issued since the last tick, which is the only way anything gets into the world. More on that in a moment.&lt;/p&gt;
&lt;p&gt;The cap on ticks per frame is not tidiness. Without it one long stall queues a burst of ticks, which itself takes long enough to queue more, and the game freezes forever trying to catch up. The usual name for this is the spiral of death. Dropping simulated time instead means a bad frame turns into a moment of slow motion, which nobody can see.&lt;/p&gt;
&lt;p&gt;The rendering runs at whatever rate the machine manages and interpolates between ticks. At thirty ticks a second you do notice if it doesn’t.&lt;/p&gt;
&lt;h3 id=&quot;everything-is-a-command&quot;&gt;Everything is a command&lt;/h3&gt;
&lt;p&gt;The second rule: &lt;strong&gt;nothing mutates the simulation except a command.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A click becomes a &lt;code&gt;MoveCommand&lt;/code&gt;. The AI’s decision becomes a &lt;code&gt;MoveCommand&lt;/code&gt;, the same class, through the same door. The debug panel that spawns a thousand units for a stress test issues &lt;code&gt;SpawnEntityCommand&lt;/code&gt;s. There is no back door for tooling, because a back door added “just for debugging” is a back door the AI could use, and I’d never be sure it wasn’t.&lt;/p&gt;
&lt;pre class=&quot;language-gdscript&quot;&gt;&lt;code class=&quot;language-gdscript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class_name&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RefCounted&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;type_code&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;push_error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Command.type_code() must be overridden&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_world&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;World&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;push_error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Command.apply() must be overridden&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;to_array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;push_error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Command.to_array() must be overridden&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every command is serialised to a positional array with an integer type id, appended to a log with its tick number. &lt;code&gt;seed + ordered command log&lt;/code&gt; reconstructs any match exactly, and there is a test that plays a match, replays the log into a fresh world, and asserts the two state hashes are equal. That test is the backbone of the project, because determinism breaks silently. A stray &lt;code&gt;randi()&lt;/code&gt; or an iteration over a Dictionary whose order you assumed will not throw. It will quietly make every save, replay and benchmark wrong, and you find out weeks later.&lt;/p&gt;
&lt;p&gt;Two habits keep it honest. All randomness goes through one seeded generator owned by the world, and I never iterate an unordered collection where the order affects the result. Entities are processed in ascending slot order, always.&lt;/p&gt;
&lt;p&gt;The AI drives the same command interface as the player and reads the world through a filtered view that only contains what its units can currently see. No cheating, and more usefully, no &lt;em&gt;accidental&lt;/em&gt; cheating, which is the kind that actually happens.&lt;/p&gt;
&lt;h3 id=&quot;handles-not-references&quot;&gt;Handles, not references&lt;/h3&gt;
&lt;p&gt;Every RTS hits the dangling-target bug. Unit A is attacking unit B. B dies, its slot is reused for a harvester, and A cheerfully keeps shooting at a harvester it has never seen, possibly its own.&lt;/p&gt;
&lt;p&gt;The fix is old and simple: entities are stored in dense arrays and every cross-entity reference is a generational handle, an index plus a generation counter packed into one 64-bit integer. When a slot is reused, the generation increments. A stale handle resolves to null instead of resolving to the wrong thing.&lt;/p&gt;
&lt;pre class=&quot;language-gdscript&quot;&gt;&lt;code class=&quot;language-gdscript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;make&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; generation&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;generation &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;index &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;index_of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; handle &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0xFFFFFFFF&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generations start at 1 so that zero is never a valid handle. So far it has done its job, which for a first RTS feels like cheating.&lt;/p&gt;
&lt;h3 id=&quot;pathfinding-is-where-rts-projects-die&quot;&gt;Pathfinding is where RTS projects die&lt;/h3&gt;
&lt;p&gt;The obvious approach is A* per unit. Order fifty units to one spot and you get fifty near-identical searches, then fifty units trying to walk the same line and shoving each other into walls.&lt;/p&gt;
&lt;p&gt;A flow field inverts it. Once per destination, compute a direction arrow for every passable cell, flooding outward from the goal. Any number of units then read the arrow in their cell and follow it. One computation serves the whole group, and it gets relatively cheaper the more units share it. The flood is integer breadth-first search, so the routing input is exactly reproducible. Units bumping into each other is a separate, local problem, and for now it’s a plain “push away from close neighbours” force.&lt;/p&gt;
&lt;p&gt;The first version built the field at full resolution, 256 by 256 cells. That is 65,536 cells, and GDScript took &lt;strong&gt;330 ms&lt;/strong&gt; to flood them. Inlining the hot loop got it to 117 ms. Against a 5 ms tick budget that is still unusable by a factor of twenty, and it is not an algorithm bug. The search is correctly linear in cells. It’s the interpreter’s per-operation cost over a lot of cells, and inlining is the floor.&lt;/p&gt;
&lt;p&gt;The fix was a coarse routing grid, 64 by 64, decoupled from the fine collision grid: about 7 ms per build, inside a frame. Routing coarse and collision fine is how actual RTS engines separate the two problems, which I did not know until I’d rediscovered it the expensive way.&lt;/p&gt;
&lt;h3 id=&quot;the-performance-gate-that-lied-to-me&quot;&gt;The performance gate that lied to me&lt;/h3&gt;
&lt;p&gt;This is the part of the week worth reading twice, because the bug was in the instrument.&lt;/p&gt;
&lt;p&gt;I had a gate that spawned 600 units, sent them across the map, and checked the tick against the 5 ms budget. It printed &lt;strong&gt;4.18 ms, PASS&lt;/strong&gt;. Comfortable.&lt;/p&gt;
&lt;p&gt;But look at what those 600 units were. Bare entities: no player, no weapons, no cargo. So combat hit its early-out and returned. Vision stamped one player’s fog. The economy found nothing to do. The gate measured movement honestly and everything else not at all, then printed a verdict about the whole tick. The same 600 units actually fighting cost &lt;strong&gt;17.1 ms&lt;/strong&gt;, three and a half times the budget.&lt;/p&gt;
&lt;p&gt;A gate that measures a subset and reports on the whole is worse than no gate, because a green light gets believed. The fix was not to optimise. It was a probe that runs the real systems on the real level, two armies meeting mid-map with both economies running, and times each system separately:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: The performance gate that lied to me&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Before&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Combat&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;8.16 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Economy&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2.40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2.36 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Movement&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1.94 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Combat costing four times what movement cost was not what I would have guessed, and every instinct I had about where a simulation spends its time was wrong about half the time. The biggest single win was embarrassing: iterating the world allocated a fresh &lt;code&gt;Array[Entity]&lt;/code&gt; on every call, and the systems call it about seventeen times a tick. Caching it took the economy from 2.40 ms to 0.86 ms.&lt;/p&gt;
&lt;p&gt;What GDScript actually costs, in one sentence: the wins come from deleting calls and lookups, not from cleverer arithmetic. &lt;code&gt;Packed*Array&lt;/code&gt; is the only unboxed storage the language has, and anything a loop touches every tick belongs in one.&lt;/p&gt;
&lt;h3 id=&quot;a-rock-was-scouting-for-you&quot;&gt;A rock was scouting for you&lt;/h3&gt;
&lt;p&gt;My favourite bug of the week. Harvesters, freshly spawned, walked straight to the nearest contested resource node even though nobody had scouted it yet.&lt;/p&gt;
&lt;p&gt;The obvious cause was the AI’s target search ignoring what its owner had seen. Adding that check did nothing. The real cause was one layer down: resource nodes are entities, entities need an owner to spawn, so the nodes were owned by player 0 “because nothing reads it”. Vision read it. The fog pass stamped a sight disc for every entity, rocks included, so every node on the map revealed itself to player 0 from tick zero, permanently. Player 0 could see every deposit for free. Player 1, the AI, could see none.&lt;/p&gt;
&lt;p&gt;Silently asymmetric, and in the play scene player 0 is the human. It showed up in the numbers the moment it was fixed: contested ore mined across a match went from 170 against 20 to 170 against 155. Deposits now have a vision range of zero. A rock is scenery, not a scout.&lt;/p&gt;
&lt;h3 id=&quot;the-renderer-changed-twice-and-the-simulation-never-noticed&quot;&gt;The renderer changed twice and the simulation never noticed&lt;/h3&gt;
&lt;p&gt;On the 26th the game rendered in 2D. On the 27th I ported it to 3D under an orthographic isometric camera, because I thought I wanted real height and painted buildings as billboards. On the 28th a tileset I had bought settled the question the other way: cliffs are tiles, buildings are placeables, the whole thing is a 2D tilemap. So the 3D renderer was deleted. 7,157 lines removed, 305 added.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sim/&lt;/code&gt; did not change. Not one line, in either direction. The entire UI layer used exactly three things from the camera, a ground-point lookup, a to-screen projection and the zoom, so the minimap, HUD and overlays each needed one type name changed. That’s the bill a narrow interface pays, and it is legible in the diff. I wrote the invariant down on day one hoping it would hold. It has now been tested twice, which is better than asserted.&lt;/p&gt;
&lt;h3 id=&quot;testing&quot;&gt;Testing&lt;/h3&gt;
&lt;p&gt;gdUnit4 is the test framework, vendored into &lt;code&gt;addons/&lt;/code&gt;. Tests mirror the source layout, extend &lt;code&gt;GdUnitTestSuite&lt;/code&gt;, and run headless:&lt;/p&gt;
&lt;pre class=&quot;language-gdscript&quot;&gt;&lt;code class=&quot;language-gdscript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;GdUnitTestSuite&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_stale_handle_does_not_resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; store&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;EntityStore&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; EntityStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; handle&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;spawn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Entity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;despawn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;assert_object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;is_null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;./tools/ci/run_tests.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One sharp edge is worth knowing about. Godot’s registry of &lt;code&gt;class_name&lt;/code&gt; declarations lives under the gitignored &lt;code&gt;.godot/&lt;/code&gt; folder and is populated by a filesystem scan that &lt;code&gt;godot --headless -s script.gd&lt;/code&gt; does &lt;em&gt;not&lt;/em&gt; perform. On a fresh clone the test runner failed with “Could not find type GdUnitTestCIRunner”, and worse, Godot’s script runner still exited 0, silently running zero tests. The runner script now imports the project first, unconditionally, and it also fails the run if no tests actually executed. Green with zero tests is the worst colour.&lt;/p&gt;
&lt;p&gt;Most of the suite is simulation tests that run in milliseconds. The ones I trust most are the determinism tests: play a match, replay its command log into a fresh world, compare the hashes. Every system that landed this week landed with one of those.&lt;/p&gt;
&lt;h3 id=&quot;every-mechanic-must-be-visible&quot;&gt;Every mechanic must be visible&lt;/h3&gt;
&lt;p&gt;Headless tests prove correctness. They don’t find the problems you didn’t think to test for, and in a real-time game that’s most of them.&lt;/p&gt;
&lt;p&gt;So every system ships with a debug overlay you can toggle at runtime (flow-field arrows, paths, collision radii, aggro ranges, the AI’s current stance) and with a sandbox scene where that one mechanic can be poked in isolation. Spawn units, click, drop a wall, watch. The single most useful key in the whole project is &lt;code&gt;.&lt;/code&gt;, which advances the simulation by one tick while paused. Most gameplay bugs are one bad tick, invisible at thirty a second.&lt;/p&gt;
&lt;p&gt;They feel like extra credit next to the “real” work. They aren’t. The rock above was found by watching harvesters walk, not by a test.&lt;/p&gt;
&lt;h3 id=&quot;about-the-editor&quot;&gt;About the editor&lt;/h3&gt;
&lt;p&gt;The thing I feared most about Godot was that a chunk of the work would live inside a GUI, away from text. In practice, almost everything is a &lt;code&gt;.gd&lt;/code&gt; file and the scenes are plain text &lt;code&gt;.tscn&lt;/code&gt; files that diff acceptably. The editor is where I lay out a level and look at the result. It is not where the logic lives, and it would be a bad place for it.&lt;/p&gt;
&lt;p&gt;Two things worth knowing. Godot writes &lt;code&gt;.uid&lt;/code&gt; sidecar files next to every script and they belong in version control, which the commit history shows me forgetting at least once. And it will happily rewrite &lt;code&gt;project.godot&lt;/code&gt; when you touch a setting, so commit that separately or you’ll never find your own change in the diff.&lt;/p&gt;
&lt;h3 id=&quot;who-typed-all-this&quot;&gt;Who typed all this&lt;/h3&gt;
&lt;p&gt;Not all me, and I’d rather say so. I’ve written about &lt;a href=&quot;/blog/claude-code/&quot;&gt;Claude Code&lt;/a&gt; twice this year, and this project is where I’ve leaned on it hardest. The repository has a &lt;code&gt;CLAUDE.md&lt;/code&gt; whose first instruction is “you are a critical collaborator, not a cheerleader”, with a standing order to argue the case against my plan before helping with it. There are three advisor agents, a designer, an engineer and a director who resolves the two, and I go to the director for decisions. Each day ends with a journal entry in &lt;code&gt;docs/journal/&lt;/code&gt;, and one of the branches merged this week is literally called &lt;code&gt;overnight-autonomous&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What I do is decide, review, play and reject. The sim purity script, the perf gate that lied and the probe that replaced it, the rock: all of those came out of me reading what was built and pushing back. The 316 commits are a measure of how fast the loop runs when one side of it never sleeps, not of how much I typed.&lt;/p&gt;
&lt;h3 id=&quot;whats-next&quot;&gt;What’s next&lt;/h3&gt;
&lt;p&gt;The 20-minute fun gate is still open. Matches currently last about fourteen minutes against a target of twenty, and the AI needs to attack rather than trickle. The map editor exists but is two days old. I have not drawn a single piece of art I like.&lt;/p&gt;
&lt;p&gt;I’m also aware that a lot of this reads like confidence. It’s not. I’ve been in this engine for eight days and I have a folder of daily journal entries whose most common heading is some variation of “what I got wrong”. Godot has been pleasant, GDScript is faster to write and slower to run than I expected, and the architecture is holding. Ask me again after the fun gate.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[AI Skills]]></title><description><![CDATA[Agent Skills are folders with a SKILL.md that any agent can read. What the spec actually requires, how to write one, and how the skill registries work.]]></description><link>https://angelo.dini.dev/blog/ai-skills/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/ai-skills/</guid><pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-folder-my-agent-knows-how-to-read&quot;&gt;A folder my agent knows how to read&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;For a couple of years, teaching an AI assistant your workflow meant pasting the same paragraph of instructions into a different box for every tool you used. Rewrite it for the terminal agent, rewrite it again for the editor, watch both drift out of date. Agent Skills fix that by being boring: a folder, a Markdown file, and a spec that a couple of dozen tools have agreed to read.&lt;/p&gt;&lt;p&gt;I wrote about &lt;a href=&quot;/blog/claude-code/&quot;&gt;Claude Code skills&lt;/a&gt; a couple of months ago, back when they still looked like a Claude Code feature. They aren’t one any more. Anthropic published the format as an open standard, the rest of the industry picked it up faster than anyone expected, and there is now a registry layer sitting on top of it. That changes the calculation. A skill you write today is not a bet on one vendor’s roadmap.&lt;/p&gt;
&lt;p&gt;This post is about the format itself: what the spec requires, what it merely suggests, how the loading model works, and where the sharp edges are.&lt;/p&gt;
&lt;h3 id=&quot;what-is-actually-in-one&quot;&gt;What is actually in one&lt;/h3&gt;
&lt;p&gt;A skill is a directory with a &lt;code&gt;SKILL.md&lt;/code&gt; in it. The file starts with YAML frontmatter and continues with plain instructions. That is the entire required surface.&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; changelog
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Generate a changelog entry from staged git changes.
  Use when the user asks to write a changelog or a release note.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Writing a changelog entry&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; Run &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`git diff --staged`&lt;/span&gt; to see what changed.
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Group changes under Added, Changed, Fixed, Removed.
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Write each entry in past tense, user-facing language.
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; Skip internal refactors that don't affect behaviour.
&lt;span class=&quot;token list punctuation&quot;&gt;5.&lt;/span&gt; Prepend the entry to &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`CHANGELOG.md`&lt;/span&gt; under an &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`## [Unreleased]`&lt;/span&gt; heading.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a working skill. No SDK, no build step, nothing to register. The agent reads the description, decides the skill applies, loads the instructions and follows them.&lt;/p&gt;
&lt;p&gt;A skill can carry more than the one file, and the spec suggests three conventional directories for it:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;my-skill/
├── SKILL.md          # required: metadata and instructions
├── scripts/          # executable code the agent can run
├── references/       # documentation loaded on demand
└── assets/           # templates, schemas, images&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of those are mandatory, and the agent will happily read anything else you put in the folder. The convention exists so that a skill someone else wrote is legible at a glance. That bundling is the real difference between a skill and a saved prompt: a prompt is text, a skill is text plus the script that does the fiddly part and the reference file you would otherwise have to summarise badly.&lt;/p&gt;
&lt;h3 id=&quot;the-frontmatter-field-by-field&quot;&gt;The frontmatter, field by field&lt;/h3&gt;
&lt;p&gt;Two fields are required and four are optional. It is a short spec, which I appreciate.&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: The frontmatter, field by field&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Max 64 characters, lowercase letters, numbers and hyphens. Must match the parent directory name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Max 1024 characters. What it does &lt;em&gt;and&lt;/em&gt; when to use it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;license&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;A licence name, or a pointer to a bundled licence file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compatibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Max 500 characters. Environment requirements: needs Docker, needs network access, and so on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Arbitrary string-to-string map for anything the spec doesn’t cover.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Space-separated list of pre-approved tools. Still experimental, support varies.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;name&lt;/code&gt; rules trip people up more than they should. No uppercase, no leading or trailing hyphen, no consecutive hyphens, and it has to match the folder. &lt;code&gt;pdf--processing&lt;/code&gt; is invalid and the error message you get depends entirely on which agent you are running.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; is the field to spend time on, because it is the only part of your skill the agent sees until it decides to use it. “Helps with testing” tells it nothing. “Use when the user asks to write or fix a Vitest unit test, or when a test file is open and failing” gives it a trigger it can match against. Write it for a matching algorithm, not for a README.&lt;/p&gt;
&lt;p&gt;Individual agents add their own fields on top. Claude Code has &lt;code&gt;disable-model-invocation&lt;/code&gt;, &lt;code&gt;argument-hint&lt;/code&gt; and a few others; those are not in the spec and other tools will ignore them. That is fine as long as you know which half of your frontmatter is portable.&lt;/p&gt;
&lt;h3 id=&quot;why-fifty-skills-dont-cost-fifty-skills&quot;&gt;Why fifty skills don’t cost fifty skills&lt;/h3&gt;
&lt;p&gt;Skills load in three stages, and this is the design decision that makes the whole thing viable.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; At startup the agent reads only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; for every installed skill. Budget roughly a hundred tokens each.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Activation.&lt;/strong&gt; When a task matches, the full &lt;code&gt;SKILL.md&lt;/code&gt; body is read into context. The spec recommends staying under 5000 tokens, or about 500 lines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution.&lt;/strong&gt; Anything in &lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;references/&lt;/code&gt; or &lt;code&gt;assets/&lt;/code&gt; is pulled in only when the instructions point at it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So a skill you never use costs you a sentence. That is why you can have several dozen installed without the context window turning to soup, and it is also why the “keep &lt;code&gt;SKILL.md&lt;/code&gt; short, push detail into reference files” advice is about cost rather than tidiness. Every token sitting in an activated skill is a token not being spent on your actual code.&lt;/p&gt;
&lt;p&gt;One practical rule from the spec that is easy to miss: keep file references one level deep from &lt;code&gt;SKILL.md&lt;/code&gt; and use relative paths. Chains of references that point at further references are exactly as fun to debug as they sound.&lt;/p&gt;
&lt;h3 id=&quot;who-reads-them&quot;&gt;Who reads them&lt;/h3&gt;
&lt;p&gt;The format came out of Anthropic and was released as an open standard at &lt;a href=&quot;https://agentskills.io&quot;&gt;agentskills.io&lt;/a&gt;, with the specification and a reference validator developed in the open on GitHub. The spec document is not the interesting part. Adoption is.&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Who reads them&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Skills support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Claude Code and Claude&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT and Codex&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code and GitHub Copilot&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;Anysphere&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kiro&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Junie&lt;/td&gt;
&lt;td&gt;JetBrains&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goose&lt;/td&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode, Roo Code, Amp, Factory&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The client showcase on agentskills.io runs to several dozen entries at this point, including some genuinely unexpected ones (Laravel Boost, Spring AI, Snowflake’s Cortex Code). Competitors who agree on very little agreed on this within about two days of the spec landing, which tells you how much everybody wanted the problem to go away.&lt;/p&gt;
&lt;p&gt;The practical upshot is mundane and useful. If your team encodes its deployment checklist as a skill, it works whether a given colleague lives in a terminal or an IDE, and it keeps working when half of them switch tools next quarter. Your ChatGPT custom instructions have never done that for you.&lt;/p&gt;
&lt;h3 id=&quot;getting-them-onto-your-machine&quot;&gt;Getting them onto your machine&lt;/h3&gt;
&lt;p&gt;Agents scan conventional directories. Paths vary slightly, but the Claude Code layout is representative:&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Getting them onto your machine&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Applies to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Personal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everything you work on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Personal skills are for habits that belong to you: how you like commit messages written, how you want code explained. Project skills belong in version control, which is the part I care about more. Commit them and every fresh checkout arrives with the team’s playbooks already attached. Nobody has to be told about the deployment procedure, because it is sitting in the repository.&lt;/p&gt;
&lt;p&gt;Installing a skill by hand means dropping a folder in the right place. That works fine until you want to share one.&lt;/p&gt;
&lt;h3 id=&quot;running-them&quot;&gt;Running them&lt;/h3&gt;
&lt;p&gt;Two ways in.&lt;/p&gt;
&lt;p&gt;The first is explicit: type the skill’s name as a command, &lt;code&gt;/changelog&lt;/code&gt;, and the agent follows that playbook and no other. The second is automatic: the agent reads descriptions, decides yours matches what you just asked for, and loads it without being told. Most of the time the automatic path is the one you want, and it is also the one that makes a good description pay for itself.&lt;/p&gt;
&lt;p&gt;For anything with side effects, turn the automatic path off. Deploying, deleting, sending a message to a customer: these should only ever happen because you typed the command. In Claude Code that is a frontmatter flag:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploys the app to production.
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read-only skills can be automatic. Destructive ones should be explicit. I have not found a case where the reverse was defensible.&lt;/p&gt;
&lt;h3 id=&quot;writing-one&quot;&gt;Writing one&lt;/h3&gt;
&lt;p&gt;The best first skill is the workflow you have explained to an agent more than twice. Open a folder, write the file, and keep it lean.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lead with a description that says what and when. It is a trigger, not a summary.&lt;/li&gt;
&lt;li&gt;Write imperative numbered steps. Agents follow a list more reliably than they follow a paragraph.&lt;/li&gt;
&lt;li&gt;Bundle the script or the template instead of describing it in prose.&lt;/li&gt;
&lt;li&gt;Validate before you publish. The reference library will check your frontmatter and naming for you: &lt;code&gt;skills-ref validate ./my-skill&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mental adjustment that helped me most: you are not prompt-engineering, you are writing onboarding documentation for a colleague who reads very quickly and remembers nothing between sessions.&lt;/p&gt;
&lt;h3 id=&quot;the-registry-layer&quot;&gt;The registry layer&lt;/h3&gt;
&lt;p&gt;Once skills became portable folders, somebody was always going to build npm for them. Vercel did, in January 2026, and &lt;a href=&quot;https://www.skills.sh/&quot;&gt;skills.sh&lt;/a&gt; is the one you will run into first. It is a directory with leaderboards: all-time installs, what is trending today, which agents a given skill is compatible with. Installation is a one-liner.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;owner/repo&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The directory itself is open source, and the compatibility tags cover a couple of dozen agents. Treating skills as packages rather than as folders you email around is what makes any of this scale to a team, let alone an ecosystem.&lt;/p&gt;
&lt;p&gt;Now the caveat, because it matters more than the convenience. A registry is not a quality gate. These catalogues are open, anyone can publish, and a skill is instructions &lt;em&gt;plus executable scripts your agent will run on your machine&lt;/em&gt;. That is a real trust surface and a fairly appealing one to attack. Before installing anything from a stranger, read the &lt;code&gt;SKILL.md&lt;/code&gt;, open whatever is in &lt;code&gt;scripts/&lt;/code&gt;, and treat the install count as weak evidence rather than an endorsement. It is the same caution you would apply to an npm package with a postinstall hook, and for the same reason.&lt;/p&gt;
&lt;h3 id=&quot;where-a-skill-ends-and-a-context-file-begins&quot;&gt;Where a skill ends and a context file begins&lt;/h3&gt;
&lt;p&gt;The two mechanisms get muddled constantly, so: persistent project knowledge goes in an always-loaded context file, and on-demand procedures go in skills.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;CLAUDE.md&lt;/code&gt; in this site’s repository is a decent example of the first kind. It says which npm scripts exist, where blog posts live on disk, that webpack resolves &lt;code&gt;src/&lt;/code&gt; as a module root so imports look unusual, and which SCSS conventions the project follows. All of that is true in every session, so it should be loaded in every session.&lt;/p&gt;
&lt;p&gt;A skill answers a narrower question. How do I scaffold a new post. How do I write a commit message here. What does our release checklist look like. Those are only relevant while you are doing that specific thing, and paying for them the rest of the time is waste.&lt;/p&gt;
&lt;p&gt;If you find yourself unsure which bucket something belongs in, ask whether an agent would need it to answer “what is this project?”. If yes, context file. If it only helps once you have already decided what to do, skill.&lt;/p&gt;
&lt;h3 id=&quot;what-id-write-first&quot;&gt;What I’d write first&lt;/h3&gt;
&lt;p&gt;Pick the one thing you re-explain most often. Not the most impressive workflow, the most repeated one. Write it, use it for a week, and then go back and sharpen the description until the agent reaches for it at the right moments without prompting. That second pass is where most of the value is, and it is the step people skip.&lt;/p&gt;
&lt;p&gt;After that, go and look at what other people have published before writing your own version of a changelog generator. The good ones are already there.&lt;/p&gt;
&lt;p&gt;The bit I am still unconvinced by is discovery. Leaderboards rank by installs, installs reward whoever published first and tweeted loudest, and there is currently no way to tell a carefully maintained skill from one that was generated in bulk and never opened again. That will need solving, and a star count is not going to be the thing that solves it&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Building AI-Powered Apps with the Vercel AI SDK]]></title><description><![CDATA[A tour of the Vercel AI SDK as it stands in 2026, covering model calls, tool loops, useChat after the v5 rewrite, the gateway and the one layer to skip.]]></description><link>https://angelo.dini.dev/blog/vercel-ai-sdk/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/vercel-ai-sdk/</guid><pubDate>Sat, 23 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;making-the-model-a-parameter&quot;&gt;Making the model a parameter&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every provider ships its own SDK, and every one of them has a different opinion about what a stream looks like, how a tool call is shaped, and what comes back when the model refuses to answer. Write against OpenAI directly and you have written against OpenAI forever. The Vercel AI SDK is the layer that turns the provider into a parameter instead of an architecture decision you have to live with.&lt;/p&gt;&lt;p&gt;The catch is that the SDK moves. Version 5 rewrote the React hooks from the ground up, version 6 deprecated the function most people learned first, and one of its three layers has carried an “experimental” label for its entire life. A post that reprints the quickstart would be stale by autumn. So what follows is a map of the pieces, with an opinion attached wherever I have one about which of them I would actually build on.&lt;/p&gt;
&lt;p&gt;If you have been following the rise of AI agents, this is the plumbing underneath that story. Nothing here is conceptually hard. Most of the difficulty is in knowing which API survived the last major version.&lt;/p&gt;
&lt;h3 id=&quot;three-layers-one-of-which-i-ignore&quot;&gt;Three layers, one of which I ignore&lt;/h3&gt;
&lt;p&gt;The SDK is really three packages wearing a trench coat.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI SDK Core&lt;/strong&gt; is the part that matters. &lt;code&gt;generateText&lt;/code&gt;, &lt;code&gt;streamText&lt;/code&gt;, &lt;code&gt;embed&lt;/code&gt;, &lt;code&gt;embedMany&lt;/code&gt; and friends behave identically no matter which model you point them at. It runs anywhere TypeScript runs: a route handler, a cron job, a CLI, a worker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI SDK UI&lt;/strong&gt; is the framework layer. &lt;code&gt;useChat&lt;/code&gt; for conversations, &lt;code&gt;useObject&lt;/code&gt; for progressively filling structured data. React, Svelte and Vue are supported; &lt;code&gt;useObject&lt;/code&gt; in particular is still flagged experimental and is not available everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI SDK RSC&lt;/strong&gt; streams React Server Components straight from the server as the model produces them. It is the flashiest of the three and the one I would leave alone. More on why later.&lt;/p&gt;
&lt;p&gt;You install what you need, nothing more:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ai zod&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is genuinely all it takes now. The provider packages (&lt;code&gt;@ai-sdk/openai&lt;/code&gt;, &lt;code&gt;@ai-sdk/anthropic&lt;/code&gt; and the rest) are thin adapters, and since the AI Gateway became the default provider you can skip them entirely for the common models and pass a plain string instead.&lt;/p&gt;
&lt;h3 id=&quot;calling-a-model&quot;&gt;Calling a model&lt;/h3&gt;
&lt;p&gt;The smallest useful call gives you a model and a prompt and hands back text:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; generateText &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; usage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; finishReason &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'openai/gpt-5'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  prompt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Explain quantum entanglement in two sentences.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Used &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;usage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;totalTokens&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; tokens&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving to Anthropic is a string edit:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'anthropic/claude-opus-4-7'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  prompt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Explain quantum entanglement in two sentences.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;provider/model&lt;/code&gt; strings resolve through Vercel’s AI Gateway, which reads &lt;code&gt;AI_GATEWAY_API_KEY&lt;/code&gt; from the environment. If you would rather talk to a provider directly, import its package and pass &lt;code&gt;openai(&amp;#39;gpt-5&amp;#39;)&lt;/code&gt; instead. Both routes land on the same call signature, and the shape that comes back (&lt;code&gt;text&lt;/code&gt;, &lt;code&gt;usage&lt;/code&gt;, &lt;code&gt;finishReason&lt;/code&gt;, &lt;code&gt;warnings&lt;/code&gt;) does not change either. System prompts, message history, temperature and stop sequences all work the way you would expect.&lt;/p&gt;
&lt;h4 id=&quot;streaming&quot;&gt;Streaming&lt;/h4&gt;
&lt;p&gt;For anything a human is waiting on, &lt;code&gt;streamText&lt;/code&gt; replaces &lt;code&gt;generateText&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; streamText &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;streamText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'openai/gpt-5'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    system&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'You are a helpful assistant.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toUIMessageStreamResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;toUIMessageStreamResponse()&lt;/code&gt; returns a &lt;code&gt;Response&lt;/code&gt; whose body speaks the SDK’s own stream protocol, which is what the client hook knows how to parse. If you want the raw material instead, &lt;code&gt;result.textStream&lt;/code&gt; gives you nothing but text deltas, and &lt;code&gt;result.fullStream&lt;/code&gt; gives you every event that passes through: text, tool calls, tool results, finish reasons.&lt;/p&gt;
&lt;h3 id=&quot;structured-output-and-the-rug-pull&quot;&gt;Structured output, and the rug pull&lt;/h3&gt;
&lt;p&gt;Getting typed JSON out of a model is the single most useful thing the SDK does, and it is also where version 6 caused the most grumbling. &lt;code&gt;generateObject&lt;/code&gt; and &lt;code&gt;streamObject&lt;/code&gt; were the headline feature for two years. They are now deprecated in favour of an &lt;code&gt;output&lt;/code&gt; option on the ordinary text functions:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; generateText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Output &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; output &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'openai/gpt-5'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  output&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Output&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    schema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      ingredients&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; amount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      steps&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      prepMinutes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  prompt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Generate a recipe for sourdough focaccia.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; item &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; output&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ingredients&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;amount&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a defensible reason for the change. &lt;code&gt;Output.object()&lt;/code&gt; composes with tool calling, whereas &lt;code&gt;generateObject&lt;/code&gt; was a dead end that could not do both at once. &lt;code&gt;Output.array()&lt;/code&gt;, &lt;code&gt;Output.choice()&lt;/code&gt; and &lt;code&gt;Output.json()&lt;/code&gt; round out the set, and the streaming equivalent renamed &lt;code&gt;partialObjectStream&lt;/code&gt; to &lt;code&gt;partialOutputStream&lt;/code&gt;. Fine. It is still a lot of churn for a function that was doing its job, and if you follow a tutorial written before December 2025 you will be writing deprecated code without any warning.&lt;/p&gt;
&lt;p&gt;Underneath, the SDK still uses whatever native structured-output mode the provider offers and validates the result against your schema. A response that does not fit throws, so wrap it and retry rather than assuming the model behaved.&lt;/p&gt;
&lt;h3 id=&quot;tools&quot;&gt;Tools&lt;/h3&gt;
&lt;p&gt;A tool is a function you let the model call. The SDK normalises the shape across providers so you write it once:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; streamText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stepCountIs&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; tool &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;streamText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'openai/gpt-5'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tools&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    getWeather&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Get the current weather for a city'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      inputSchema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        city&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'The city name, for example &quot;Zurich&quot;'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        units&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'celsius'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fahrenheit'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'celsius'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token function-variable function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; city&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; units &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchWeather&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;city&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; units&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    refundOrder&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Refund an order in full'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      inputSchema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; orderId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      needsApproval&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token function-variable function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; orderId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;orderId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  stopWhen&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;stepCountIs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model reads the descriptions and the schemas, picks a tool, and the SDK runs your &lt;code&gt;execute&lt;/code&gt; and feeds the return value back as a tool result. &lt;code&gt;stopWhen&lt;/code&gt; caps how many rounds of that the model gets. Version 6 defaults it to twenty steps, which is a much better default than the old one but is still twenty chances to call a paid API in a loop, so set it deliberately.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;needsApproval&lt;/code&gt; is the piece I would not skip. Mark any tool that spends money, deletes something or sends a message, and the SDK pauses instead of executing, handing you an approval request to surface in the UI. Before version 6 you did this by leaving &lt;code&gt;execute&lt;/code&gt; off and reimplementing the round trip yourself, which worked but was fiddly enough that plenty of people quietly didn’t bother.&lt;/p&gt;
&lt;p&gt;Write the &lt;code&gt;.describe()&lt;/code&gt; calls on your schema fields, by the way. The model reads them, and vague field names are the most common reason a tool gets called with nonsense arguments.&lt;/p&gt;
&lt;h3 id=&quot;usechat-after-the-rewrite&quot;&gt;useChat, after the rewrite&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useChat&lt;/code&gt; is the workhorse of the UI layer, and version 5 changed it enough that almost every example on the internet is now wrong. The hook no longer owns your input state, &lt;code&gt;handleSubmit&lt;/code&gt; and &lt;code&gt;handleInputChange&lt;/code&gt; are gone, and the endpoint is configured through a transport rather than an &lt;code&gt;api&lt;/code&gt; string.&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useChat &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@ai-sdk/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DefaultChatTransport &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Chat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setInput&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sendMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; status&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stop &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useChat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    transport&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DefaultChatTransport&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/api/chat'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;flex flex-col gap-4&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'user'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'text-right'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'text'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'tool-getWeather'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;WeatherCall&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;part&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;

      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;sendMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;setInput&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setInput&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Ask something&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ready'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'streaming'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;button&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;stop&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
            Stop
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was annoyed by the input-state change when I first hit it, and I have come round. Owning the &lt;code&gt;useState&lt;/code&gt; yourself means you can prefill from a query string, keep a draft in local storage or drive the box from a keyboard shortcut without fighting the hook. The old convenience was hiding about four lines of code.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;parts&lt;/code&gt; array is the other thing worth internalising. A single assistant message can carry text, tool calls, tool results, reasoning and files, and each tool gets its own part type named &lt;code&gt;tool-&amp;lt;toolName&amp;gt;&lt;/code&gt;. So a weather tool produces &lt;code&gt;tool-getWeather&lt;/code&gt; parts, each with a &lt;code&gt;state&lt;/code&gt; that walks from streaming input through to &lt;code&gt;output-available&lt;/code&gt; or &lt;code&gt;output-error&lt;/code&gt;. Render by part type and by state, and the awkward intermediate moments (a tool that has been called but has not returned) stop being a special case.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status&lt;/code&gt; moves through &lt;code&gt;submitted&lt;/code&gt;, &lt;code&gt;streaming&lt;/code&gt;, &lt;code&gt;ready&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;. Use it for the disabled state and for a stop button, because long responses that cannot be interrupted feel broken even when they are working.&lt;/p&gt;
&lt;p&gt;To restore a saved conversation, pass &lt;code&gt;id&lt;/code&gt; and an initial &lt;code&gt;messages&lt;/code&gt; array from the server. The old &lt;code&gt;initialMessages&lt;/code&gt; prop is gone.&lt;/p&gt;
&lt;h3 id=&quot;generative-ui-and-why-i-skip-it&quot;&gt;Generative UI, and why I skip it&lt;/h3&gt;
&lt;p&gt;The RSC layer lets the model choose a React component, and the server streams that component to the browser. &lt;code&gt;streamUI&lt;/code&gt; picks a tool, the tool’s &lt;code&gt;generate&lt;/code&gt; function yields a skeleton, fetches data, then yields the real thing. Written down it sounds wonderful, and the demos are genuinely impressive.&lt;/p&gt;
&lt;p&gt;It is also, still, experimental, and Vercel’s own documentation says as much and points you at AI SDK UI for production. The known problems are not cosmetic: components remounting and flickering mid-stream, Suspense boundaries falling over, and data transfer that grows quadratically as the stream goes on. The API surface has also shifted between releases, and error recovery is nowhere near as mature as the client-hook path.&lt;/p&gt;
&lt;p&gt;My take is that generative UI is a real idea implemented too early. You can get most of the benefit without RSC by having the model call a tool, rendering that tool’s part on the client with a component you chose, and keeping the server doing what servers are good at. That is one indirection more than &lt;code&gt;streamUI&lt;/code&gt; and roughly infinitely fewer surprises at 2am.&lt;/p&gt;
&lt;h3 id=&quot;embeddings&quot;&gt;Embeddings&lt;/h3&gt;
&lt;p&gt;The SDK is not only for chat. &lt;code&gt;embedMany&lt;/code&gt; is the least ceremonious way I know to get vectors:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; embedMany &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; openai &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@ai-sdk/openai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; embeddings &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;embedMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; openai&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;embedding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'text-embedding-3-large'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  values&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; docs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;doc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; doc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documents&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insertMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  docs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;doc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;doc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; embedding&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; embeddings&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;embed&lt;/code&gt; for a single query vector and run a similarity search against your store. Both Drizzle and &lt;a href=&quot;/blog/prisma/&quot;&gt;Prisma&lt;/a&gt; handle &lt;code&gt;pgvector&lt;/code&gt; columns now, so a normal Postgres database is usually enough and you can put off adding a dedicated vector service until you actually have the volume to justify one.&lt;/p&gt;
&lt;h3 id=&quot;the-gateway&quot;&gt;The gateway&lt;/h3&gt;
&lt;p&gt;Because every call takes the same shape, choosing a model can be ordinary application logic:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pickModel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;task&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fast'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'smart'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'cheap'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;task&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fast'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'google/gemini-2.5-flash'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'smart'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'anthropic/claude-opus-4-7'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'cheap'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'openai/gpt-5-mini'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pickModel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'smart'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prompt &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those strings all go through the AI Gateway, which gives you one API key instead of four, per-request analytics, one invoice, and failover to a model you nominate when the upstream provider falls over. That last point is the one that sold me. Provider outages are frequent enough to plan for, and turning one into a logged warning rather than a paged engineer is worth a small amount of added latency.&lt;/p&gt;
&lt;p&gt;If you would rather not route through Vercel, nothing forces you to. Keep the provider packages and the rest of your code is unchanged.&lt;/p&gt;
&lt;h3 id=&quot;agent-loops&quot;&gt;Agent loops&lt;/h3&gt;
&lt;p&gt;When a task needs several rounds of tool use, &lt;code&gt;ToolLoopAgent&lt;/code&gt; packages the loop:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ToolLoopAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stepCountIs &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; supportAgent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ToolLoopAgent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'anthropic/claude-opus-4-7'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  instructions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'You are a support engineer. Investigate with the tools before answering.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tools&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; searchTickets&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; readLogs&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; queryDatabase &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  stopWhen&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;stepCountIs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; steps &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; supportAgent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  prompt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Investigate why customer 4271 cannot log in.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you used this in version 5 it was called &lt;code&gt;Experimental_Agent&lt;/code&gt; and took &lt;code&gt;system&lt;/code&gt; rather than &lt;code&gt;instructions&lt;/code&gt;. Same idea, renamed.&lt;/p&gt;
&lt;p&gt;The genuinely valuable return value is &lt;code&gt;steps&lt;/code&gt;. Each entry holds the reasoning, the tool calls and their results for one round of the loop. Log it. When an agent does something baffling in production, that array is the difference between a debugging session and a shrug.&lt;/p&gt;
&lt;h3 id=&quot;middleware-and-telemetry&quot;&gt;Middleware and telemetry&lt;/h3&gt;
&lt;p&gt;Every model call can be wrapped, which is where caching, redaction and logging belong:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; wrapLanguageModel &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ai'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; model &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wrapLanguageModel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  model&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'openai/gpt-5'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  middleware&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;wrapGenerate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; doGenerate&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hashParams&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;params&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cached &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cached&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cached&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;doGenerate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'EX'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3600&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Middleware can also implement &lt;code&gt;transformParams&lt;/code&gt; to rewrite a call before it leaves, and &lt;code&gt;wrapStream&lt;/code&gt; for the streaming path.&lt;/p&gt;
&lt;p&gt;For telemetry, pass &lt;code&gt;experimental_telemetry: { isEnabled: true }&lt;/code&gt; and the SDK emits OpenTelemetry spans covering the request, the tool calls and the token usage. Point those at whatever you already run. I send mine to &lt;a href=&quot;/blog/posthog/&quot;&gt;PostHog&lt;/a&gt;, which has an LLM analytics view that turns the spans into per-feature cost, and it took about ten minutes.&lt;/p&gt;
&lt;h3 id=&quot;is-the-layer-worth-it&quot;&gt;Is the layer worth it?&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Is the layer worth it?&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Provider SDK directly&lt;/th&gt;
&lt;th&gt;Vercel AI SDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Changing model&lt;/td&gt;
&lt;td&gt;Rewrite the call site&lt;/td&gt;
&lt;td&gt;Edit a string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Provider-specific SSE&lt;/td&gt;
&lt;td&gt;One protocol, one parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calling&lt;/td&gt;
&lt;td&gt;A different shape per provider&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;tool()&lt;/code&gt; helper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;Hand-rolled schema handling&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Output.object()&lt;/code&gt; with Zod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approvals&lt;/td&gt;
&lt;td&gt;Build the round trip&lt;/td&gt;
&lt;td&gt;&lt;code&gt;needsApproval: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;Write the hook yourself&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useChat&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-step loops&lt;/td&gt;
&lt;td&gt;Write the loop yourself&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ToolLoopAgent&lt;/code&gt; + &lt;code&gt;stopWhen&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The cost is an abstraction between you and the model, and a maintainer who has shown they will deprecate things. The benefit is that almost none of your application code cares which model is behind it.&lt;/p&gt;
&lt;p&gt;For most projects that trade is easy. Where I would think twice: research code that needs a provider feature the week it ships, and a one-file script that calls one model once and exits. The raw &lt;code&gt;openai&lt;/code&gt; package is perfectly pleasant for that, and adding a framework to it is a waste of everyone’s afternoon.&lt;/p&gt;
&lt;h3 id=&quot;a-handful-of-habits&quot;&gt;A handful of habits&lt;/h3&gt;
&lt;p&gt;Things I would tell myself before the first project rather than after it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;stopWhen&lt;/code&gt; explicitly on every tool-enabled call, even though the default is now sane.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;needsApproval: true&lt;/code&gt; on anything destructive before you write the happy path, not after the first accident.&lt;/li&gt;
&lt;li&gt;Show tool calls in the interface instead of hiding them. People trust an agent they can watch.&lt;/li&gt;
&lt;li&gt;Persist messages server-side from &lt;code&gt;onFinish&lt;/code&gt; in the route handler. Do not trust the client to hand back a complete transcript.&lt;/li&gt;
&lt;li&gt;Turn telemetry on from day one. Working out where the money went is much harder retroactively.&lt;/li&gt;
&lt;li&gt;Cache anything whose prompt does not depend on user input. A summarisation call behind a middleware cache costs a fraction of what it did unwrapped.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-id-watch-next&quot;&gt;What I’d watch next&lt;/h3&gt;
&lt;p&gt;The two things I want are boring ones. Better long-term stability, because three major versions in eighteen months is a lot of migration work for a dependency this central. And an honest decision about the RSC layer, which has been experimental long enough that it should either graduate or be retired rather than sitting in the docs tempting people.&lt;/p&gt;
&lt;p&gt;Neither of those is a reason not to use it. Core alone earns the install, and &lt;code&gt;useChat&lt;/code&gt; saves an afternoon per project. Just pin your version, read the migration guide before you upgrade, and treat anything labelled experimental as exactly that&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Writing Skills for Claude Code]]></title><description><![CDATA[How skills work in Claude Code: the SKILL.md format, frontmatter fields, arguments, live shell injection, subagents, and where the whole idea falls down.]]></description><link>https://angelo.dini.dev/blog/claude-code/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/claude-code/</guid><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;explaining-the-same-thing-every-morning&quot;&gt;Explaining the same thing every morning&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;You start a session, and before anything useful happens you type the same paragraph you typed yesterday. Our commits look like this. The migrations live over there. Run the linter before you push, and no, not that linter. It is the software equivalent of repeating your coffee order to someone who has been making it for a year.&lt;/p&gt;&lt;p&gt;Claude Code has a mechanism for this, and it’s about as low-tech as it could be: a folder with a markdown file in it. Write the instructions down once, and they get loaded when they’re relevant. That’s a skill.&lt;/p&gt;
&lt;p&gt;I’ve written &lt;a href=&quot;/blog/how-ai-changes-coding/&quot;&gt;about AI in the editor&lt;/a&gt; before, back when the argument was still whether autocomplete counted as cheating. That argument is over. The interesting question now is how much of your team’s tacit knowledge you can hand over, and skills are the sharpest tool I’ve found for it.&lt;/p&gt;
&lt;h3 id=&quot;what-the-thing-is-briefly&quot;&gt;What the thing is, briefly&lt;/h3&gt;
&lt;p&gt;Claude Code is Anthropic’s agentic coding tool. You start it in a project directory:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;claude&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then talk to it. The word doing the work is agentic: it doesn’t propose a diff for you to apply, it reads files, edits them, runs the tests, and comes back with what happened. One request routinely becomes thirty tool calls.&lt;/p&gt;
&lt;p&gt;What it can do, roughly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read and navigate the whole project, including how the pieces connect&lt;/li&gt;
&lt;li&gt;Edit across as many files as a change touches, so renaming a component updates the imports&lt;/li&gt;
&lt;li&gt;Run builds, tests, linters, and anything else in your shell&lt;/li&gt;
&lt;li&gt;Stage, commit, branch, and open pull requests&lt;/li&gt;
&lt;li&gt;Spawn parallel subagents for work that splits cleanly, with a lead agent coordinating&lt;/li&gt;
&lt;li&gt;Run in the terminal, in VS Code and JetBrains, in a desktop app, and in the browser&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also reads &lt;code&gt;CLAUDE.md&lt;/code&gt; from your project at the start of every session. That file is the standing brief: architecture, conventions, the commands that matter. Skills are the opposite shape, loaded only when needed.&lt;/p&gt;
&lt;h3 id=&quot;a-skill-is-a-folder-with-a-markdown-file-in-it&quot;&gt;A skill is a folder with a markdown file in it&lt;/h3&gt;
&lt;p&gt;Minimum viable skill: a directory containing &lt;code&gt;SKILL.md&lt;/code&gt;, which is YAML frontmatter followed by instructions in plain English.&lt;/p&gt;
&lt;p&gt;The mechanism that makes this scale is what the spec calls progressive disclosure. At startup Claude reads only each skill’s name and description. The body stays on disk until the skill is actually used. So thirty skills cost you thirty short descriptions of context, not thirty documents, and there’s no reason to be precious about how many you install.&lt;/p&gt;
&lt;p&gt;Two ways in. You type &lt;code&gt;/skill-name&lt;/code&gt; and it loads. Or Claude reads the description, decides it’s relevant to what you just asked for, and loads it itself. Both routes matter, and as we’ll see, sometimes you want to close one of them.&lt;/p&gt;
&lt;h4 id=&quot;where-they-live&quot;&gt;Where they live&lt;/h4&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Where they live&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Applies to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Personal&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All your projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This project only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;plugin&amp;gt;/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wherever the plugin is enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Personal skills are for habits that follow you around: how you like commits written, how you want code explained back to you. Project skills go in version control and encode things the team agreed on, which means a new colleague inherits them by cloning the repository rather than by asking someone.&lt;/p&gt;
&lt;p&gt;When the same name exists at more than one level, personal wins over project, and either wins over a skill that ships with Claude Code. A &lt;code&gt;code-review&lt;/code&gt; skill in your project directory replaces the built-in one. Plugin skills are namespaced as &lt;code&gt;plugin-name:skill-name&lt;/code&gt; and can’t collide.&lt;/p&gt;
&lt;p&gt;In a monorepo, skills also load from nested &lt;code&gt;.claude/skills/&lt;/code&gt; directories underneath your working directory, so a package can carry its own. If the names clash, both stay available and the nested one gets a qualified name like &lt;code&gt;apps/web:deploy&lt;/code&gt;. Handy, and also the sort of thing you discover by accident when the wrong deploy skill fires.&lt;/p&gt;
&lt;h3 id=&quot;the-frontmatter&quot;&gt;The frontmatter&lt;/h3&gt;
&lt;p&gt;Here’s a skill that does something concrete:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; review&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;component
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Reviews React components for accessibility&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; performance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  and adherence to project conventions. Use when reviewing PRs or
  checking component quality.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

When reviewing a React component, check the following areas:

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Accessibility&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; All interactive elements have appropriate ARIA attributes
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Images have alt text
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Colour is not the only means of conveying information
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Keyboard navigation works correctly

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Performance&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; No unnecessary re-renders (check dependency arrays)
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Large lists use virtualisation
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Images are optimised and lazy-loaded

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Conventions&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Component uses TypeScript with proper prop types
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Follows the project's naming conventions
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Uses Tailwind utility classes, not inline styles
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Tests cover the main interaction paths

Provide specific line references for any issues found.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two fields is often all you need. The rest are there for control:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;production
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploys the application to production. Runs tests&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  builds the Docker image&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; and pushes to the container registry.
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Bash(npm run &lt;span class=&quot;token important&quot;&gt;*)&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;argument-hint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[environment]&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inherit
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;description&lt;/code&gt; is the field that decides whether a skill ever gets used. Claude only sees this line when choosing, so it has to say both what the skill does and when to reach for it. “Helps with components” is useless. Keep it tight, too: long descriptions get truncated in the listing, and the truncated half is the part you cared about.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;name&lt;/code&gt; is less important than it looks. For a personal or project skill it’s a display label; the command you type comes from the directory name. &lt;code&gt;~/.claude/skills/deploy-staging/SKILL.md&lt;/code&gt; gives you &lt;code&gt;/deploy-staging&lt;/code&gt; whatever the frontmatter says. Only in plugin skills does &lt;code&gt;name&lt;/code&gt; set the final segment of the command.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;disable-model-invocation: true&lt;/code&gt; stops Claude from loading the skill on its own. Anything with side effects wants this, and I’d argue for it more broadly than most people do. The failure mode isn’t dramatic, it’s just that a skill you wrote for occasional use fires during unrelated work because the description happened to match.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;allowed-tools&lt;/code&gt; pre-approves tools for the turn that invoked the skill, so you’re not clicking through permission prompts mid-workflow. The grant expires when you send your next message. You can scope it narrowly, as &lt;code&gt;Bash(gh *)&lt;/code&gt; rather than all of &lt;code&gt;Bash&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;argument-hint&lt;/code&gt; shows up in autocomplete. &lt;code&gt;model&lt;/code&gt; accepts the same values as &lt;code&gt;/model&lt;/code&gt;, plus &lt;code&gt;inherit&lt;/code&gt; to leave the session model alone. Reaching for a cheaper model on a mechanical skill is a reasonable instinct, though I’d measure before assuming it saves anything.&lt;/p&gt;
&lt;h4 id=&quot;who-can-invoke-what&quot;&gt;Who can invoke what&lt;/h4&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Who can invoke what&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;You&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disable-model-invocation: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user-invocable: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The third row is for background knowledge that shouldn’t clutter the &lt;code&gt;/&lt;/code&gt; menu: house API conventions, a naming policy, something Claude should know when it’s relevant and you’d never type by hand.&lt;/p&gt;
&lt;h3 id=&quot;arguments&quot;&gt;Arguments&lt;/h3&gt;
&lt;p&gt;Whatever follows the skill name lands in &lt;code&gt;$ARGUMENTS&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fix&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;issue
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Reads a GitHub issue and implements the fix
&lt;span class=&quot;token key atrule&quot;&gt;argument-hint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[issue-number]&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Issue context&lt;/span&gt;
Read GitHub issue #$ARGUMENTS and understand the problem.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Implementation&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; Find the relevant code
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Implement the fix
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Write tests
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; Create a commit with message: &quot;fix: resolve #$ARGUMENTS&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/fix-issue 42&lt;/code&gt; substitutes &lt;code&gt;42&lt;/code&gt; everywhere the placeholder appears. If the skill doesn’t contain &lt;code&gt;$ARGUMENTS&lt;/code&gt; at all, what you typed gets appended at the end anyway, so nothing is silently dropped.&lt;/p&gt;
&lt;p&gt;For several arguments there’s positional access, &lt;code&gt;$ARGUMENTS[0]&lt;/code&gt; or the shorter &lt;code&gt;$0&lt;/code&gt;, &lt;code&gt;$1&lt;/code&gt; and so on. Multi-word values need quoting the way a shell would expect: &lt;code&gt;/migrate-component &amp;quot;Search Bar&amp;quot; React Vue&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;injecting-live-context&quot;&gt;Injecting live context&lt;/h3&gt;
&lt;p&gt;This is the feature that changed how I write skills. The &lt;code&gt;!`command`&lt;/code&gt; syntax runs a shell command before the skill is handed to Claude, and the output takes the placeholder’s place:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pr&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;summary
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Summarises the current pull request
&lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fork
&lt;span class=&quot;token key atrule&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Explore
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Bash(gh &lt;span class=&quot;token important&quot;&gt;*)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Pull request context&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Changed files: !&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`gh pr diff --name-only`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; PR description: !&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`gh pr view --json body -q .body`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Review comments: !&lt;span class=&quot;token code-snippet code keyword&quot;&gt;`gh pr view --comments`&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Task&lt;/span&gt;
Summarise this pull request. Focus on:
&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; What changed and why
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Any concerns raised in review comments
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Suggested improvements&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worth being precise about what’s happening, because it’s easy to misread. This is preprocessing. The commands run first, their output is pasted in, and Claude receives a finished prompt containing the actual diff. It never sees the commands, and it isn’t deciding to run them.&lt;/p&gt;
&lt;p&gt;Some sharp edges. Substitution happens once over the file, so output containing another placeholder won’t be expanded again. The inline form only triggers when &lt;code&gt;!&lt;/code&gt; starts a line or follows whitespace, which means &lt;code&gt;KEY=!`cmd`&lt;/code&gt; is left as literal text and quietly does nothing. For several commands, open a fenced block with a &lt;code&gt;!&lt;/code&gt; right after the backticks instead of stringing inline placeholders together.&lt;/p&gt;
&lt;p&gt;There’s also a &lt;code&gt;disableSkillShellExecution&lt;/code&gt; setting that turns the whole feature off, replacing each command with a note that policy blocked it. If you’re introducing skills somewhere with a security team, know that this exists before they ask.&lt;/p&gt;
&lt;h3 id=&quot;running-a-skill-in-a-subagent&quot;&gt;Running a skill in a subagent&lt;/h3&gt;
&lt;p&gt;Research burns context. A skill that reads forty files to answer one question will leave your main conversation full of file contents you’ll never refer to again. &lt;code&gt;context: fork&lt;/code&gt; runs it somewhere else:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deep&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;research
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Performs thorough codebase research on a topic
&lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fork
&lt;span class=&quot;token key atrule&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Explore
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;Research the following topic in this codebase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $ARGUMENTS

Trace through all relevant code paths. Map the architecture.
Identify patterns&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; abstractions&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; and dependencies.
Return a summary with the 10 most important files
to understand this area of the codebase.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The skill body becomes the subagent’s prompt. It gets the codebase and the tools, but not your conversation history, and only its answer comes back.&lt;/p&gt;
&lt;p&gt;That last point has a consequence worth knowing: the built-in &lt;code&gt;Explore&lt;/code&gt; and &lt;code&gt;Plan&lt;/code&gt; agents deliberately skip &lt;code&gt;CLAUDE.md&lt;/code&gt; to keep their context small. So a forked skill using &lt;code&gt;agent: Explore&lt;/code&gt; sees your skill content and nothing else about the project. If it needs a convention to do its job, the convention has to be written into the skill.&lt;/p&gt;
&lt;p&gt;The other trap is forking a skill that has no task in it. If your skill is a page of guidelines with no instruction, the subagent receives the guidelines, has nothing to do, and returns nothing useful. &lt;code&gt;context: fork&lt;/code&gt; is for skills that ask for something.&lt;/p&gt;
&lt;h3 id=&quot;more-than-one-file&quot;&gt;More than one file&lt;/h3&gt;
&lt;p&gt;A skill doesn’t have to be a single document. It can be a directory of supporting material:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;review-security/
├── SKILL.md              # Main instructions
├── owasp-checklist.md    # Reference material
├── examples/
│   ├── good-review.md
│   └── bad-review.md
└── scripts/
    └── scan.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SKILL.md&lt;/code&gt; stays short and points at the rest, which gets read only if it’s needed. Use &lt;code&gt;${CLAUDE_SKILL_DIR}&lt;/code&gt; when referring to bundled files so the paths don’t depend on where the session started.&lt;/p&gt;
&lt;p&gt;Templates work well for anything where the shape of the output matters more than the wording:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- template.md --&gt;&lt;/span&gt;
&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Security review: {{component_name}}&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Summary&lt;/span&gt;
{{one_paragraph_summary}}

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Findings&lt;/span&gt;
{{findings_table}}

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Risk level&lt;/span&gt;
{{low|medium|high|critical}}

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Recommended actions&lt;/span&gt;
{{numbered_action_items}}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in &lt;code&gt;SKILL.md&lt;/code&gt;, a line telling it to fill in &lt;code&gt;template.md&lt;/code&gt; and leave nothing blank. A filled template is far more reliable than a paragraph asking for consistent formatting.&lt;/p&gt;
&lt;h3 id=&quot;the-ones-that-ship-with-it&quot;&gt;The ones that ship with it&lt;/h3&gt;
&lt;p&gt;Claude Code comes with bundled skills you can read for reference. &lt;code&gt;/code-review&lt;/code&gt; and &lt;code&gt;/batch&lt;/code&gt; and &lt;code&gt;/loop&lt;/code&gt; are the ones I’ve opened most. &lt;code&gt;/batch&lt;/code&gt; in particular is a decent lesson in decomposition: it splits a large change into independent units, runs them in separate git worktrees, and opens a pull request per unit. &lt;code&gt;/loop&lt;/code&gt; re-runs a prompt on an interval, which is a surprisingly good way to babysit a deploy.&lt;/p&gt;
&lt;h3 id=&quot;three-that-earn-their-keep&quot;&gt;Three that earn their keep&lt;/h3&gt;
&lt;h4 id=&quot;commits&quot;&gt;Commits&lt;/h4&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; commit
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Creates a conventional commit from staged changes.
  Analyses the diff&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; determines the commit type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; and writes a
  descriptive message.
&lt;span class=&quot;token key atrule&quot;&gt;disable-model-invocation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

Create a git commit following the Conventional Commits specification.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Steps&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; Run &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`git diff --staged`&lt;/span&gt; to see what's being committed
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Analyse the changes to determine the type:
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`feat`&lt;/span&gt;: New feature
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`fix`&lt;/span&gt;: Bug fix
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`refactor`&lt;/span&gt;: Code restructuring
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`docs`&lt;/span&gt;: Documentation only
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`test`&lt;/span&gt;: Adding or updating tests
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`chore`&lt;/span&gt;: Maintenance tasks
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; Write a commit message: &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`type(scope): description`&lt;/span&gt;
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Scope is the primary area affected (component name, module)
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Description is imperative mood, lowercase, no full stop
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Mark a breaking change with &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`!`&lt;/span&gt; after the type, as in &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`feat!:`&lt;/span&gt;,
     or with a &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`BREAKING CHANGE:`&lt;/span&gt; footer
   &lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Add a body if the &quot;why&quot; isn't obvious from the diff
&lt;span class=&quot;token list punctuation&quot;&gt;4.&lt;/span&gt; Include &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`Co-Authored-By: Claude &amp;lt;noreply@anthropic.com&gt;`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;5.&lt;/span&gt; Show the message and ask for confirmation before committing&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is &lt;a href=&quot;/blog/conventional-commits/&quot;&gt;conventional commits&lt;/a&gt; written down where something else can follow them. Note step 5: skills that touch git should show their work before doing it.&lt;/p&gt;
&lt;h4 id=&quot;scaffolding-a-component&quot;&gt;Scaffolding a component&lt;/h4&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; create&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;component
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Generates a new React component with TypeScript&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tests&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; and a Storybook story following project conventions.
&lt;span class=&quot;token key atrule&quot;&gt;argument-hint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[ComponentName]&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

Generate a new React component named &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`$ARGUMENTS`&lt;/span&gt;.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Files&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`src/components/$ARGUMENTS/$ARGUMENTS.tsx`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`src/components/$ARGUMENTS/$ARGUMENTS.test.tsx`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`src/components/$ARGUMENTS/$ARGUMENTS.stories.tsx`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`src/components/$ARGUMENTS/index.ts`&lt;/span&gt; (barrel export)

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Component conventions&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; TypeScript with an exported Props interface
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`forwardRef`&lt;/span&gt; for components that render DOM elements
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Tailwind for styling, no CSS modules
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; JSDoc comment on the Props interface
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Named export, not default

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Story conventions&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; CSF3 with &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`satisfies Meta&amp;lt;typeof Component&gt;`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Include &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`tags: ['autodocs']`&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Stories for Default, WithProps, EdgeCase&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every project has this shape written down somewhere nobody reads. Here it produces files. The story conventions line up with what I covered in the &lt;a href=&quot;/blog/storybook/&quot;&gt;Storybook post&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;a-read-only-explorer&quot;&gt;A read-only explorer&lt;/h4&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token front-matter-block&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token front-matter yaml language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; explore
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;only codebase exploration. Use when you want to
  understand code without any risk of modification.
&lt;span class=&quot;token key atrule&quot;&gt;allowed-tools&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Read&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Grep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Glob
&lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fork
&lt;span class=&quot;token key atrule&quot;&gt;agent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Explore&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/span&gt;

Explore this codebase to answer: $ARGUMENTS

You are in read-only mode. Search files, read code, and trace
execution paths. Do not suggest changes. Focus entirely on
understanding and explaining what exists.

Return:
&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt; A clear explanation of what you found
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt; Key files involved, with paths and line numbers
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt; An ASCII architecture diagram if it helps&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one I use most, and the one I’d write first on an unfamiliar codebase.&lt;/p&gt;
&lt;h3 id=&quot;claudemd-or-a-skill&quot;&gt;CLAUDE.md or a skill?&lt;/h3&gt;
&lt;p&gt;The split is simple enough. &lt;code&gt;CLAUDE.md&lt;/code&gt; is loaded every session and should contain facts that are always true: what this project is, how it’s laid out, which commands to run. A skill is loaded on demand and should contain a procedure.&lt;/p&gt;
&lt;p&gt;The test I use: if it reads as a paragraph of description, it belongs in &lt;code&gt;CLAUDE.md&lt;/code&gt;. If it reads as numbered steps, it’s a skill. When a section of &lt;code&gt;CLAUDE.md&lt;/code&gt; grows a numbered list, that’s usually the moment to move it out. &lt;code&gt;/init&lt;/code&gt; will generate a first &lt;code&gt;CLAUDE.md&lt;/code&gt; from your codebase, which is a reasonable starting point and a poor finishing one.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;project/
├── CLAUDE.md
└── .claude/
    └── skills/
        ├── commit/SKILL.md
        ├── review-pr/SKILL.md
        └── create-component/SKILL.md&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-makes-a-skill-work&quot;&gt;What makes a skill work&lt;/h3&gt;
&lt;p&gt;One job per skill. A &lt;code&gt;deploy-and-notify-and-update-docs&lt;/code&gt; skill is three skills that would compose better separately, and it’s harder to tell which third of it went wrong.&lt;/p&gt;
&lt;p&gt;Keep &lt;code&gt;SKILL.md&lt;/code&gt; short, a few hundred lines at most, and push detail into files it can reference. Everything in the skill is context that isn’t being spent on your actual code.&lt;/p&gt;
&lt;p&gt;Write the description for the moment of selection, not for a catalogue. It should answer “when would I want this?”, because that’s the question being asked of it.&lt;/p&gt;
&lt;p&gt;Test with more than one model. A skill that a larger model infers its way through can leave a smaller one guessing, and the fix is usually explicitness rather than length.&lt;/p&gt;
&lt;h3 id=&quot;portability-with-an-asterisk&quot;&gt;Portability, with an asterisk&lt;/h3&gt;
&lt;p&gt;Skills follow the &lt;a href=&quot;https://agentskills.io&quot;&gt;Agent Skills&lt;/a&gt; spec, which Anthropic released as an open standard and which a long list of tools have since adopted, Cursor, Gemini CLI, Codex and GitHub Copilot among them. That’s genuinely useful. A folder with a &lt;code&gt;SKILL.md&lt;/code&gt; in it travels.&lt;/p&gt;
&lt;p&gt;The asterisk is that the spec is narrower than Claude Code’s implementation. It defines six fields: &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;license&lt;/code&gt;, &lt;code&gt;compatibility&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt; and &lt;code&gt;allowed-tools&lt;/code&gt;. Everything else in this post, &lt;code&gt;argument-hint&lt;/code&gt;, &lt;code&gt;context: fork&lt;/code&gt;, &lt;code&gt;disable-model-invocation&lt;/code&gt;, is Claude Code’s own. Packaging a skill for the Skills API or uploading it to claude.ai with those fields present fails outright rather than ignoring them:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Unexpected key(s) in SKILL.md frontmatter: argument-hint.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The body features don’t travel either. &lt;code&gt;!`command`&lt;/code&gt; injection is Claude Code preprocessing, and elsewhere it’s just text. So “write once, run anywhere” holds for the plain instructional skills and not for the clever ones. Worth knowing before you build a library on the assumption.&lt;/p&gt;
&lt;h3 id=&quot;the-part-that-gets-left-out&quot;&gt;The part that gets left out&lt;/h3&gt;
&lt;p&gt;A skill is a prompt, not a program. Nothing enforces it. Claude reads your numbered steps and generally follows them, and occasionally it decides step three doesn’t apply today and it is, annoyingly, sometimes right about that. If a step must happen every time without exception, it belongs in a shell script that the skill calls, or in a git hook, not in prose asking nicely.&lt;/p&gt;
&lt;p&gt;That’s not a complaint so much as the thing to calibrate against. Skills are good at capturing judgement, the stuff that’s hard to encode: which cases matter in a review, what a decent commit message looks like here, where the bodies are buried in this module. They’re bad at guarantees. Anything you’d write a test for, write a test for.&lt;/p&gt;
&lt;p&gt;I’m still poking at the edges of the forked-subagent behaviour, mostly trying to work out how much project context a skill has to restate when &lt;code&gt;agent: Explore&lt;/code&gt; skips &lt;code&gt;CLAUDE.md&lt;/code&gt;. Not a clean answer yet, so I’ll leave it there.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Docker for Frontend Developers]]></title><description><![CDATA[Containerising a Next.js app: standalone output, multi-stage builds, Compose for the local stack, hot reload on macOS, CI pipelines, and when to skip Docker.]]></description><link>https://angelo.dini.dev/blog/docker/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/docker/</guid><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-frontend-grew-a-database&quot;&gt;The frontend grew a database&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;There was a time when handing a frontend project to someone meant sending them a zip file and telling them which version of Node to install. Then the frontend grew a database, a cache, a queue and four environment variables that are different on everybody's laptop, and &quot;works on my machine&quot; stopped being a joke and started being a Tuesday.&lt;/p&gt;&lt;p&gt;This site has shipped in a container since 2020, when I wrote up &lt;a href=&quot;/blog/hosting-on-divio-cloud/&quot;&gt;putting it on Divio Cloud&lt;/a&gt;. That Dockerfile was one stage, installed the Gatsby CLI globally and copied everything in one go. It worked. It was also about a gigabyte, and I didn’t think about it again for three years.&lt;/p&gt;
&lt;p&gt;What changed isn’t Docker. It’s that a Next.js app is now a server, and servers have neighbours. This post covers containerising one properly: the production image, the local stack, the file-watching mess on macOS, and the case for not bothering.&lt;/p&gt;
&lt;h3 id=&quot;the-four-pieces&quot;&gt;The four pieces&lt;/h3&gt;
&lt;p&gt;Four concepts and then we can move on.&lt;/p&gt;
&lt;p&gt;An &lt;strong&gt;image&lt;/strong&gt; is a read-only filesystem plus the metadata to start a process in it. A &lt;strong&gt;container&lt;/strong&gt; is one running instance of an image. Nothing more mysterious than a class and an object.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;Dockerfile&lt;/strong&gt; is the recipe for an image. Every instruction produces a layer, and layers are cached by content, which is the single fact that determines whether your builds take ten seconds or ten minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compose&lt;/strong&gt; is the thing that starts several containers together with a network between them, described in a &lt;code&gt;compose.yml&lt;/code&gt;. For local development it’s the piece you’ll actually live in.&lt;/p&gt;
&lt;p&gt;One choice worth making early: base your images on &lt;code&gt;node:22-alpine&lt;/code&gt; rather than plain &lt;code&gt;node:22&lt;/code&gt;. Alpine is a minimal Linux distribution built around musl instead of glibc, and it drops the image from roughly 1.1 GB to roughly 150 MB before you’ve added a single file of your own.&lt;/p&gt;
&lt;h3 id=&quot;building-the-image&quot;&gt;Building the image&lt;/h3&gt;
&lt;h4 id=&quot;standalone-output&quot;&gt;Standalone output&lt;/h4&gt;
&lt;p&gt;Before the Dockerfile, tell Next.js to produce a self-contained build:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  output&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'standalone'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; nextConfig&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;standalone&lt;/code&gt;, Next.js runs &lt;code&gt;@vercel/nft&lt;/code&gt; over your build to trace which files are genuinely reachable at runtime, then writes them into &lt;code&gt;.next/standalone&lt;/code&gt; along with a small &lt;code&gt;server.js&lt;/code&gt;. The point is that you no longer need &lt;code&gt;node_modules&lt;/code&gt; in the production image. Only the modules your code actually imports come along.&lt;/p&gt;
&lt;p&gt;Two directories are left out of the trace deliberately, because they’re normally served from a CDN: &lt;code&gt;public/&lt;/code&gt; and &lt;code&gt;.next/static/&lt;/code&gt;. If you forget to copy them the app boots fine and then serves a page with no CSS and no images, which is a confusing five minutes.&lt;/p&gt;
&lt;h4 id=&quot;the-production-dockerfile&quot;&gt;The production Dockerfile&lt;/h4&gt;
&lt;p&gt;Three stages: fetch dependencies, build, then assemble a runtime image that contains neither the dependencies nor the build tools.&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Stage 1: Install dependencies&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:22-alpine &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; deps&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; apk add --no-cache libc6-compat&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm ci&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Stage 2: Build the application&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:22-alpine &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; builder&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;token options&quot;&gt;&lt;span class=&quot;token property&quot;&gt;--from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;deps&lt;/span&gt;&lt;/span&gt; /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm run build&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Stage 3: Production runner&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:22-alpine &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; runner&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; NEXT_TELEMETRY_DISABLED=1&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; addgroup --system --gid 1001 nodejs&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; adduser --system --uid 1001 nextjs&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; mkdir .next&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; chown nextjs:nodejs .next&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;token options&quot;&gt;&lt;span class=&quot;token property&quot;&gt;--from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;builder&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;--chown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;nextjs:nodejs&lt;/span&gt;&lt;/span&gt; /app/.next/standalone ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;token options&quot;&gt;&lt;span class=&quot;token property&quot;&gt;--from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;builder&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;--chown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;nextjs:nodejs&lt;/span&gt;&lt;/span&gt; /app/.next/static ./.next/static&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;token options&quot;&gt;&lt;span class=&quot;token property&quot;&gt;--from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;builder&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;--chown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;nextjs:nodejs&lt;/span&gt;&lt;/span&gt; /app/public ./public&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;USER&lt;/span&gt; nextjs&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 3000&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; PORT=3000&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; HOSTNAME=&lt;span class=&quot;token string&quot;&gt;&quot;0.0.0.0&quot;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CMD&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;node&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;server.js&quot;&lt;/span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few of those lines are load-bearing and not obvious:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;libc6-compat&lt;/code&gt; is the glibc compatibility shim. Some native packages, &lt;code&gt;sharp&lt;/code&gt; in particular (which &lt;code&gt;next/image&lt;/code&gt; uses for image optimisation), ship binaries built against glibc and fall over on musl without it. This is the standard Alpine tax.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;npm ci&lt;/code&gt; rather than &lt;code&gt;npm install&lt;/code&gt;. It installs strictly from the lockfile and wipes &lt;code&gt;node_modules&lt;/code&gt; first, so the build can’t quietly pick up a floating minor version and produce something that doesn’t match what you tested.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HOSTNAME=&amp;quot;0.0.0.0&amp;quot;&lt;/code&gt; is the one that catches people. Without it the server binds to loopback inside the container, the port mapping looks right, and nothing answers. I have lost time to this more than once.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;nextjs&lt;/code&gt; user exists because a process running as root inside a container is one escape away from root on the host. It costs four lines.&lt;/p&gt;
&lt;h4 id=&quot;a-development-image&quot;&gt;A development image&lt;/h4&gt;
&lt;p&gt;Development wants the opposite of all that. Keep it in the same Dockerfile as an extra stage so Compose can target it:&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:22-alpine &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; dev&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; apk add --no-cache libc6-compat&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm ci&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 3000&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CMD&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;npm&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;run&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;dev&quot;&lt;/span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No standalone output, no pruning, devDependencies included. You want the dev server, not a small image.&lt;/p&gt;
&lt;h3 id=&quot;the-local-stack&quot;&gt;The local stack&lt;/h3&gt;
&lt;p&gt;This is where Docker earns its place for frontend work. One file describes the app, the database and the cache, and they come up together on a network where they can find each other by name:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
      &lt;span class=&quot;token key atrule&quot;&gt;dockerfile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Dockerfile
      &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3000:3000&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; DATABASE_URL=postgresql&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//postgres&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;postgres@db&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;5432/myapp
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; REDIS_URL=redis&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//redis&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6379&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; WATCHPACK_POLLING=true
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/app
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /app/node_modules
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /app/.next
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_healthy
      &lt;span class=&quot;token key atrule&quot;&gt;redis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_healthy

  &lt;span class=&quot;token key atrule&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;16&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;5432:5432&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_USER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_DB&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; myapp
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; postgres&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/lib/postgresql/data
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD-SHELL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pg_isready -U postgres&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped

  &lt;span class=&quot;token key atrule&quot;&gt;redis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; redis&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;7&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;6379:6379&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; redis&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/data
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;redis-cli&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ping&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped

&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;postgres-data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  redis&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice there’s no &lt;code&gt;version:&lt;/code&gt; key at the top. Compose has ignored it for years and now warns about it, so if you’re copying an old file around (mine on this site still carries a &lt;code&gt;version: &amp;#39;2.3&amp;#39;&lt;/code&gt; from 2020) you can delete that line.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;DATABASE_URL&lt;/code&gt; points at &lt;code&gt;db:5432&lt;/code&gt;, not &lt;code&gt;localhost&lt;/code&gt;. Inside the Compose network every service is reachable by its service name, and this trips up everyone once, usually while wondering why Prisma can’t connect. If you’re wiring up the database side, I covered that in &lt;a href=&quot;/blog/prisma/&quot;&gt;using Prisma with Next.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;condition: service_healthy&lt;/code&gt; is the part I’d argue is mandatory rather than nice to have. Plain &lt;code&gt;depends_on&lt;/code&gt; only waits for the container to start, and Postgres takes a couple of seconds after starting before it accepts connections. Without the health check your app boots into a connection refused error roughly one time in three, which is exactly the kind of intermittent failure that eats an afternoon.&lt;/p&gt;
&lt;p&gt;The named volumes keep your data across restarts. Leave them out and every &lt;code&gt;docker compose down&lt;/code&gt; wipes the database, which is either a disaster or a feature depending on the day.&lt;/p&gt;
&lt;p&gt;Then the whole thing is:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No installing Postgres, no matching Node versions, no page of README instructions that went stale in November.&lt;/p&gt;
&lt;h3 id=&quot;hot-reload-and-why-macos-makes-it-hurt&quot;&gt;Hot reload, and why macOS makes it hurt&lt;/h3&gt;
&lt;p&gt;Getting file changes to reach the dev server inside a container is the part of this that genuinely annoys me. On Linux it mostly just works. On macOS, Docker runs inside a virtual machine, your source lives on the host, and filesystem events have to cross that boundary. They often don’t.&lt;/p&gt;
&lt;h4 id=&quot;the-three-volumes&quot;&gt;The three volumes&lt;/h4&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/app              &lt;span class=&quot;token comment&quot;&gt;# source code from the host&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /app/node_modules   &lt;span class=&quot;token comment&quot;&gt;# keep the container's copy&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /app/.next          &lt;span class=&quot;token comment&quot;&gt;# keep the container's build cache&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line is the useful one. The other two are anonymous volumes that exist purely to stop the host from shadowing directories that must stay Linux-native. Your macOS &lt;code&gt;node_modules&lt;/code&gt; contains a &lt;code&gt;sharp&lt;/code&gt; built for Darwin on arm64; mount that over the container’s and the app dies on startup with an error about missing bindings.&lt;/p&gt;
&lt;h4 id=&quot;polling&quot;&gt;Polling&lt;/h4&gt;
&lt;p&gt;Next.js watches files through Watchpack, which prefers native filesystem events. When those don’t arrive, you edit a file and nothing happens at all, with no error to tell you why. The fix is to stop waiting for events and ask instead:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; WATCHPACK_POLLING=true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Polling costs some CPU and adds a small delay, usually a few hundred milliseconds. That’s fine. Silent non-reloading is not fine.&lt;/p&gt;
&lt;h4 id=&quot;compose-watch&quot;&gt;Compose Watch&lt;/h4&gt;
&lt;p&gt;Since Compose v2.22 there’s a better answer than bind mounts, which is to let Compose push changes in rather than having the container watch for them:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
      &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dev
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3000:3000&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;develop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sync
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./src
          &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /app/src
          &lt;span class=&quot;token key atrule&quot;&gt;ignore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node_modules/
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; rebuild
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; package.json
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sync+restart
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; next.config.ts
          &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /app/next.config.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sync&lt;/code&gt; copies changed files into the container and lets Next.js handle the reload. &lt;code&gt;rebuild&lt;/code&gt; rebuilds the image, which is what you want when &lt;code&gt;package.json&lt;/code&gt; changes. &lt;code&gt;sync+restart&lt;/code&gt; copies the file and bounces the container, for config that’s only read at boot.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose &lt;span class=&quot;token function&quot;&gt;watch&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On macOS this is noticeably quicker than a bind mount, because it’s a targeted file copy instead of a shared filesystem doing translation on every stat call.&lt;/p&gt;
&lt;h3 id=&quot;how-small-it-gets&quot;&gt;How small it gets&lt;/h3&gt;
&lt;p&gt;Multi-stage builds are the whole game for image size. Each &lt;code&gt;FROM&lt;/code&gt; starts fresh and only the final stage ships, so everything you needed to build the app is thrown away.&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: How small it gets&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;What survives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Install dependencies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node_modules&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;builder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compile the app&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.next&lt;/code&gt; output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runner&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Serve in production&lt;/td&gt;
&lt;td&gt;Standalone server, static and public files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;And the base image choice, for context:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: How small it gets (2)&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Base image&lt;/th&gt;
&lt;th&gt;Approximate size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:22&lt;/code&gt; (Debian)&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:22-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;220 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:22-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;150 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Alpine plus standalone output plus three stages usually lands a Next.js app somewhere around 150 to 200 MB, against well over a gigabyte for the copy-everything approach. Worth doing, though be honest about why: it’s mostly about how fast the image pulls on deploy, not about disk space, which is free.&lt;/p&gt;
&lt;h3 id=&quot;ci-with-github-actions&quot;&gt;CI with GitHub Actions&lt;/h3&gt;
&lt;p&gt;The same Dockerfile builds in CI unchanged, which is most of the appeal:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and Push

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;main&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read
      &lt;span class=&quot;token key atrule&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write

    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Set up Docker Buildx
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;buildx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v4

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Log in to GitHub Container Registry
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/login&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;registry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io
          &lt;span class=&quot;token key atrule&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.actor &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.GITHUB_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Extract metadata
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; meta
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/metadata&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v5
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; github.repository &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
            type=sha
            type=ref,event=branch&lt;/span&gt;

      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and push
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;push&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v7
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
          &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps.meta.outputs.tags &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps.meta.outputs.labels &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;cache-from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; type=gha
          &lt;span class=&quot;token key atrule&quot;&gt;cache-to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; type=gha&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;mode=max&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two cache lines are what separate a two-minute build from a nine-minute one. Runners are ephemeral, so without them every push reinstalls every dependency from scratch. With &lt;code&gt;type=gha&lt;/code&gt; the layers go into GitHub’s cache, and a commit that only touches source code reuses the &lt;code&gt;deps&lt;/code&gt; layer.&lt;/p&gt;
&lt;p&gt;Build-time values, the &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; sort that get baked into the client bundle, have to be passed in as build arguments, because they’re needed while the image is being built rather than when it runs:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build and push
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker/build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;push&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v7
  &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
    &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps.meta.outputs.tags &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build-args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      NEXT_PUBLIC_API_URL=${{ vars.API_URL }}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anything secret stays out of build args. They’re visible in the image history.&lt;/p&gt;
&lt;h3 id=&quot;things-worth-getting-right&quot;&gt;Things worth getting right&lt;/h3&gt;
&lt;h4 id=&quot;instruction-order&quot;&gt;Instruction order&lt;/h4&gt;
&lt;p&gt;Docker caches layers in sequence, and one invalidated layer invalidates everything below it. So put what rarely changes first:&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. Base image (rarely changes)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:22-alpine&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. Dependencies (change when package.json changes)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm ci&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3. Source code (changes constantly)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm run build&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Move &lt;code&gt;COPY . .&lt;/code&gt; above &lt;code&gt;npm ci&lt;/code&gt; and every typo you fix triggers a full dependency reinstall. This is the most common Dockerfile mistake I see, and it’s the one with the largest daily cost.&lt;/p&gt;
&lt;h4 id=&quot;dockerignore&quot;&gt;.dockerignore&lt;/h4&gt;
&lt;p&gt;Everything in the build context gets shipped to the daemon before the build starts. Without a &lt;code&gt;.dockerignore&lt;/code&gt; that includes your &lt;code&gt;node_modules&lt;/code&gt;, your entire git history and any &lt;code&gt;.env&lt;/code&gt; file lying around:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;node_modules
.next
.git
.env*
*.md
coverage
.vscode
.DS_Store
docker-compose*.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Faster builds, and, more importantly, secrets that don’t accidentally end up in a layer that someone can &lt;code&gt;docker history&lt;/code&gt; their way into later.&lt;/p&gt;
&lt;h4 id=&quot;security&quot;&gt;Security&lt;/h4&gt;
&lt;p&gt;Run as a non-root user, as in the Dockerfile above. Pin base images to something specific: &lt;code&gt;node:22.14.0-alpine3.21&lt;/code&gt; rather than &lt;code&gt;node:22-alpine&lt;/code&gt;, which quietly moves under you. Keep credentials in runtime environment variables and never in the image. And put a scanner in the pipeline, either &lt;code&gt;docker scout cves &amp;lt;image&amp;gt;&lt;/code&gt; or Trivy, because your base image will pick up CVEs whether or not you touched it.&lt;/p&gt;
&lt;p&gt;Setting &lt;code&gt;NEXT_TELEMETRY_DISABLED=1&lt;/code&gt; is also worth doing, if only so your CI containers stop phoning home on every build.&lt;/p&gt;
&lt;h3 id=&quot;docker-desktop-and-the-alternatives&quot;&gt;Docker Desktop and the alternatives&lt;/h3&gt;
&lt;p&gt;Docker Desktop is the default and it’s fine, but it needs a paid licence for commercial use once a company passes 250 employees or 10 million dollars in revenue, and on macOS it’s a heavy neighbour.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OrbStack&lt;/strong&gt; is what I’d point macOS users at first. It starts in a couple of seconds, sits well under a gigabyte of memory, and behaves like a native app. Free for personal use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Colima&lt;/strong&gt; runs from the terminal with no GUI at all, idles around 400 MB, and is fully open source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Podman&lt;/strong&gt; is daemonless and rootless by default, and CLI-compatible enough that aliasing &lt;code&gt;docker&lt;/code&gt; to &lt;code&gt;podman&lt;/code&gt; works for most everyday commands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of them run standard Dockerfiles and Compose files, so this isn’t a decision you have to get right first time.&lt;/p&gt;
&lt;h3 id=&quot;when-id-skip-it&quot;&gt;When I’d skip it&lt;/h3&gt;
&lt;p&gt;Docker is not free, and pretending otherwise is how teams end up with a slow, resented development setup.&lt;/p&gt;
&lt;p&gt;Reach for it when your app has neighbours: a database, a cache, a queue, a worker. Reach for it when your CI builds images anyway, so local and production share one definition. Reach for it when new people join often enough that onboarding time is a real cost.&lt;/p&gt;
&lt;p&gt;Skip it for a static site or a pure SPA that talks to an API somebody else runs. Skip it when you’re one person on one machine and the setup is five minutes of &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What I actually do most of the time is neither: Postgres and Redis in Compose, the Next.js dev server running natively on the host, pointed at &lt;code&gt;localhost:5432&lt;/code&gt;. The services are the part that’s annoying to install and easy to containerise. The dev server is the part that’s trivial to run and painful to containerise. Splitting them that way gets you the consistency without ever thinking about Watchpack again.&lt;/p&gt;
&lt;p&gt;For production, of course, everything goes in the image. That’s the one place the container is unambiguously the right answer.&lt;/p&gt;
&lt;h3 id=&quot;where-this-leaves-my-own-setup&quot;&gt;Where this leaves my own setup&lt;/h3&gt;
&lt;p&gt;The Dockerfile for this site is still a two-stage build that produces static files and hands them to nginx, which is about as simple as it gets and needs none of the standalone machinery above. I’ve been meaning to pin the base image properly for a while now, having just written a whole section telling you to do exactly that 🙃&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Component-Driven Development with Storybook]]></title><description><![CDATA[Setting up Storybook in a Next.js project: writing stories, controls, play functions, visual and accessibility testing, and what the setup really costs you.]]></description><link>https://angelo.dini.dev/blog/storybook/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/storybook/</guid><pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-button-you-can-never-get-to&quot;&gt;The button you can never get to&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every frontend project eventually grows a component you cannot look at. It only renders when a request fails, or when the user has no permissions, or when the list comes back empty on page four of the results. To see it you log in as the right user, click through five screens and then hand-edit the network response in devtools. Storybook exists so you can just open it instead.&lt;/p&gt;&lt;p&gt;The last time Storybook turned up here it was a supporting act, one piece of a &lt;a href=&quot;/blog/screenshot-testing/&quot;&gt;screenshot testing setup&lt;/a&gt; alongside Jest and Puppeteer. It has grown a lot since then. What used to be a component explorer is now a second build of your UI that you can point tests, screenshots and accessibility checks at. That growth is mostly good and partly annoying, and I’ll get to the annoying part at the end.&lt;/p&gt;
&lt;p&gt;This post is about getting it running on a Next.js project and the parts of it I actually use. For the wider picture of how component testing fits together, see &lt;a href=&quot;/blog/testing-strategies-in-react/&quot;&gt;testing strategies in React&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-a-story-is&quot;&gt;What a story is&lt;/h3&gt;
&lt;p&gt;A story is one rendered example of a component with a specific set of props. That’s the whole idea. &lt;code&gt;Button&lt;/code&gt; gets a &lt;code&gt;Default&lt;/code&gt; story, a &lt;code&gt;Destructive&lt;/code&gt; story, a &lt;code&gt;Loading&lt;/code&gt; story, and each one shows up as its own entry in the sidebar.&lt;/p&gt;
&lt;p&gt;What you gain from that is mostly boring and mostly valuable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can build a component before the screen it belongs to exists, and before the API that feeds it exists.&lt;/li&gt;
&lt;li&gt;The states nobody looks at, empty, loading, error, “user has a 40-character name”, get a permanent home instead of being checked once during development and then forgotten.&lt;/li&gt;
&lt;li&gt;A published Storybook is something a designer can open in a browser without cloning the repository or running a dev server.&lt;/li&gt;
&lt;li&gt;Stories are importable, so the same file that documents a state can drive a test of that state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last one is the reason I keep coming back. Test fixtures and design documentation are the same data, and Storybook is the only tool I’ve used that treats them that way.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-running-with-nextjs&quot;&gt;Getting it running with Next.js&lt;/h3&gt;
&lt;p&gt;The installer detects your framework and writes the config for you:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; create storybook@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The older &lt;code&gt;npx storybook@latest init&lt;/code&gt; still works and does the same job. The &lt;code&gt;create&lt;/code&gt; form is what the docs point at now.)&lt;/p&gt;
&lt;p&gt;You’ll be asked to pick a builder: Webpack via &lt;code&gt;@storybook/nextjs&lt;/code&gt;, or Vite via &lt;code&gt;@storybook/nextjs-vite&lt;/code&gt;. Take Vite. The builds are faster and, more to the point, the Vitest integration that powers everything in the testing sections below only works on the Vite framework. Picking Webpack here means opting out of half the tool.&lt;/p&gt;
&lt;p&gt;The installer creates a &lt;code&gt;.storybook&lt;/code&gt; directory, drops a few example stories in and adds scripts to &lt;code&gt;package.json&lt;/code&gt;. Next.js specifics are handled for you: &lt;code&gt;next/image&lt;/code&gt;, &lt;code&gt;next/font&lt;/code&gt;, &lt;code&gt;next/navigation&lt;/code&gt;, CSS Modules and the path aliases from your &lt;code&gt;tsconfig.json&lt;/code&gt; all work without configuration. That sounds like a small thing until you’ve tried to render a Next.js component in a plain React Storybook and spent an afternoon stubbing out the router.&lt;/p&gt;
&lt;p&gt;For a Tailwind project, import your stylesheet in the preview file so every story gets the same base styles as the app:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// .storybook/preview.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../src/globals.css'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; preview &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  parameters&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    controls&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      matchers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;(background|color)$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;Date$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; preview&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those &lt;code&gt;matchers&lt;/code&gt; are worth setting up on day one. Any prop whose name ends in &lt;code&gt;color&lt;/code&gt; gets a colour picker, anything ending in &lt;code&gt;Date&lt;/code&gt; gets a date picker, across every story in the project. It saves a lot of repeated &lt;code&gt;argTypes&lt;/code&gt; boilerplate later.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/blog/modular-frontend-with-shadcn/&quot;&gt;shadcn/ui&lt;/a&gt; components need nothing special. They’re ordinary React components with Tailwind classes on them, so as long as the CSS variables from &lt;code&gt;globals.css&lt;/code&gt; are loaded they render exactly as they do in the app.&lt;/p&gt;
&lt;h3 id=&quot;writing-stories&quot;&gt;Writing stories&lt;/h3&gt;
&lt;p&gt;Stories use Component Story Format, which is a fancy name for “an ES module with a default export and some named exports”. The default export describes the component, each named export is a story.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Button.stories.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Meta&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; StoryObj &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@storybook/nextjs-vite'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./Button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; meta &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Button&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tags&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'autodocs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  argTypes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    variant&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'default'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'destructive'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'outline'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ghost'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      control&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'select'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; satisfies Meta&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; Button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; meta&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Story&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; StoryObj&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; meta&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Default&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Story &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Click me'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    variant&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'default'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Destructive&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Story &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Delete'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    variant&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'destructive'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Loading&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Story &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Saving...'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    disabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the import path. In Storybook 9 the types come from the framework package, &lt;code&gt;@storybook/nextjs-vite&lt;/code&gt;, not from &lt;code&gt;@storybook/react&lt;/code&gt;. Older tutorials and half the answers on Stack Overflow still show the renderer package, and it will look like it works until it doesn’t.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;args&lt;/code&gt; maps straight onto props, so anything you can pass to the component you can pass to a story. The &lt;code&gt;satisfies Meta&amp;lt;typeof Button&amp;gt;&lt;/code&gt; is doing real work here: pass an arg the component doesn’t accept and TypeScript complains at compile time rather than leaving you with a silently ignored prop.&lt;/p&gt;
&lt;h4 id=&quot;decorators&quot;&gt;Decorators&lt;/h4&gt;
&lt;p&gt;Some components only make sense inside something else. A dropdown needs room to open, a sidebar item needs a sidebar. Decorators wrap a story in whatever context it needs:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; InSidebar&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Story &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  decorators&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Story&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div style&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; width&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;250&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; padding&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Story &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Sidebar Action'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    variant&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ghost'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They stack in three layers: on a single story, on the meta so every story of that component gets them, or globally in &lt;code&gt;.storybook/preview.ts&lt;/code&gt;. Theme providers and i18n providers belong in the global layer. Layout wrappers usually belong on the story, because the whole point is that this particular story renders in a narrow column and the others don’t.&lt;/p&gt;
&lt;h3 id=&quot;controls&quot;&gt;Controls&lt;/h3&gt;
&lt;p&gt;Controls are the panel underneath the story where you can change props by hand. Storybook infers them from your TypeScript types, so a boolean becomes a toggle and a union of string literals becomes a dropdown, with no configuration at all.&lt;/p&gt;
&lt;p&gt;Where the inference isn’t good enough, &lt;code&gt;argTypes&lt;/code&gt; takes over:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; meta &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Card&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  argTypes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    backgroundColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; control&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'color'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    padding&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      control&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'range'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; min&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; max&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; step&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    size&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'sm'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'md'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'lg'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      control&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'radio'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    icon&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      control&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// no sensible widget for a ReactNode&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; satisfies Meta&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; Card&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turning a control off with &lt;code&gt;control: false&lt;/code&gt; is underrated. Storybook will happily generate a JSON editor for a prop that takes a React node, and nobody has ever typed valid JSX into it.&lt;/p&gt;
&lt;h3 id=&quot;play-functions&quot;&gt;Play functions&lt;/h3&gt;
&lt;p&gt;A play function runs after the story renders. It’s the same idea as a Testing Library test, except the setup is the story you already wrote:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; expect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fn&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; userEvent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; within &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'storybook/test'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; SubmitForm&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Story &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    onSubmit&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;play&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; args&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; step &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Fill in the form'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; userEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Email'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;'user@example.com'&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; userEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Password'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;'secretpassword'&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Submit'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; userEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByRole&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Sign In'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;onSubmit&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveBeenCalledOnce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;canvas&lt;/code&gt; gives you Testing Library queries scoped to the story, &lt;code&gt;userEvent&lt;/code&gt; does clicks, typing, hovering and keyboard navigation, and &lt;code&gt;fn()&lt;/code&gt; creates a spy you can assert against. Note the import: everything comes from &lt;code&gt;storybook/test&lt;/code&gt; now, a subpath of the core package. It used to be &lt;code&gt;@storybook/test&lt;/code&gt;, and before that &lt;code&gt;@storybook/jest&lt;/code&gt; plus &lt;code&gt;@storybook/testing-library&lt;/code&gt;. Three homes in three majors.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;step&lt;/code&gt; calls are optional and I’d still write them. They group the interactions into collapsible sections in the Interactions panel, which turns a red test into something you can step through, pause and rewind, rather than a stack trace you squint at. For a five-step checkout flow that difference is the whole debugging session.&lt;/p&gt;
&lt;p&gt;These same stories run headless in CI through Vitest. The addon writes a &lt;code&gt;storybook&lt;/code&gt; project into your Vitest config, so the script is:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test-storybook&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vitest --project=storybook&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;npm run test-storybook&lt;/code&gt; in your pipeline. Every story is a test even without a play function, because rendering without throwing is already an assertion worth having. Stories with play functions get their interactions checked on top.&lt;/p&gt;
&lt;h3 id=&quot;visual-testing&quot;&gt;Visual testing&lt;/h3&gt;
&lt;p&gt;Play functions catch behaviour. They will not notice that a container overflows at 375px or that someone bumped a margin and shunted the whole card grid down by four pixels. That needs pixels compared against pixels, which I’ve &lt;a href=&quot;/blog/screenshot-testing/&quot;&gt;written about before&lt;/a&gt; in a different context.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.chromatic.com/&quot;&gt;Chromatic&lt;/a&gt; is made by the Storybook team and plugs straight in:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx storybook &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; @chromatic-com/storybook&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On every push it screenshots each story across browsers, diffs against the accepted baseline and surfaces the changes on the pull request. You either accept the diff, which makes it the new baseline, or you don’t, which fails the build.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .github/workflows/chromatic.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Visual Tests
&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; push

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;chromatic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;fetch-depth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; chromaui/action@latest
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;projectToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.CHROMATIC_PROJECT_TOKEN &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;fetch-depth: 0&lt;/code&gt; matters. Chromatic works out what changed by walking the git history, and a shallow clone leaves it comparing against nothing useful.&lt;/p&gt;
&lt;p&gt;Be warned that visual tests are noisy until you tame them. Anything with a timestamp, a random avatar or an animation mid-flight will diff against itself forever. Freeze that data in the story rather than accepting diffs out of habit, because a team that clicks “accept all” every morning has a screenshot service, not a test suite.&lt;/p&gt;
&lt;h3 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h3&gt;
&lt;p&gt;The a11y addon runs axe-core against every story:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx storybook &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; @storybook/addon-a11y&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An Accessibility panel appears next to Interactions, listing violations, passes and the checks axe couldn’t decide about, each with a link to what to do about it. Storybook’s own docs put the ceiling at “up to 57% of WCAG issues”, which is the figure Deque quote for automated checking generally. It’s a floor for your process, not a certificate. Contrast, missing labels and bad ARIA get caught; whether your focus order makes sense to somebody using a screen reader does not.&lt;/p&gt;
&lt;p&gt;Rules can be adjusted per story or per component:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; WithCustomA11y&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Story &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  parameters&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    a11y&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      test&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'error'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      config&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        rules&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'color-contrast'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'landmark-one-main'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;test&lt;/code&gt; key decides what happens in CI: &lt;code&gt;&amp;#39;error&amp;#39;&lt;/code&gt; fails the run, &lt;code&gt;&amp;#39;todo&amp;#39;&lt;/code&gt; records the violation without failing, &lt;code&gt;&amp;#39;off&amp;#39;&lt;/code&gt; skips the story. Starting a legacy project on &lt;code&gt;&amp;#39;todo&amp;#39;&lt;/code&gt; and moving components to &lt;code&gt;&amp;#39;error&amp;#39;&lt;/code&gt; as you fix them is far more likely to succeed than switching the whole thing on and drowning in red.&lt;/p&gt;
&lt;h3 id=&quot;autodocs&quot;&gt;Autodocs&lt;/h3&gt;
&lt;p&gt;Add &lt;code&gt;tags: [&amp;#39;autodocs&amp;#39;]&lt;/code&gt; to the meta and Storybook generates a documentation page: every story rendered in sequence, plus a props table pulled from the TypeScript types.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; meta &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Button&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  tags&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'autodocs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  parameters&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    docs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Primary UI button with multiple variants and sizes.'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; satisfies Meta&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; Button&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For anything more involved, Storybook takes MDX files, so you can write prose around live examples and drop in doc blocks like &lt;code&gt;&amp;lt;Canvas&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Controls&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;Source&amp;gt;&lt;/code&gt; where you want them.&lt;/p&gt;
&lt;p&gt;The value here isn’t that the docs are pretty. It’s that they’re generated from the same source as the components, so renaming a prop updates the table by itself. Every hand-written component page I’ve maintained in a wiki was wrong within two months.&lt;/p&gt;
&lt;h3 id=&quot;addons-worth-keeping&quot;&gt;Addons worth keeping&lt;/h3&gt;
&lt;p&gt;The addon list is long and most of it you will never open. These are the ones I leave installed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Viewport&lt;/strong&gt; for checking layouts at phone and tablet widths without resizing the window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Themes&lt;/strong&gt; for flipping between light and dark, which is where about half of my contrast bugs turn up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backgrounds&lt;/strong&gt; for putting a component on a dark surface, because “looks fine on white” is not the same as fine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure &amp;amp; Outline&lt;/strong&gt; for when the spacing is off by an amount you can see but not name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything else I’ve installed at some point and removed again. Addons go in &lt;code&gt;.storybook/main.ts&lt;/code&gt; and most need no configuration beyond being listed.&lt;/p&gt;
&lt;h3 id=&quot;one-story-several-kinds-of-test&quot;&gt;One story, several kinds of test&lt;/h3&gt;
&lt;p&gt;Because stories are plain modules, other tools can import them. That gives you layers of checking over a single definition:&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: One story, several kinds of test&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test type&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Stories themselves&lt;/td&gt;
&lt;td&gt;Crashes, missing providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interaction&lt;/td&gt;
&lt;td&gt;Play functions via Vitest&lt;/td&gt;
&lt;td&gt;Broken flows, handlers that never fire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual&lt;/td&gt;
&lt;td&gt;Chromatic&lt;/td&gt;
&lt;td&gt;Layout shifts, contrast, overflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;a11y addon (axe-core)&lt;/td&gt;
&lt;td&gt;Missing labels, bad roles, contrast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;End-to-end tools can point at the story URL directly, which is handy for testing a single component under a real browser without booting the whole app:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; test&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@playwright/test'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'form submission works'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; page &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/storybook/iframe.html?id=forms-login--submit-form'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Welcome back'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeVisible&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h3&gt;
&lt;p&gt;Now the annoying part I promised. Storybook is a second build of your application, and second builds need feeding. The config drifts from the app config, a Next.js upgrade breaks the framework package before the framework package catches up, and the import paths move on every major version. The testing utilities alone have lived in three different packages across three releases, and each move meant a morning of codemods and confused imports.&lt;/p&gt;
&lt;p&gt;There’s also the rot problem. Stories only stay honest if somebody looks at them. A component library with 200 stories and no visual testing has 200 files that used to be true, and nobody notices until a designer opens the published build and asks why half of it looks nothing like production. Wiring up Chromatic or the Vitest run on day one is what keeps that from happening, not good intentions.&lt;/p&gt;
&lt;p&gt;So it isn’t free, and on a small project with two components it isn’t worth it. Where it pays is a shared component library, or any codebase where more than one person edits the UI and nobody can hold all the states in their head.&lt;/p&gt;
&lt;p&gt;Next thing on my list is getting the Vitest run fast enough to live in a pre-commit hook instead of only in CI. Not there yet&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Looking back at 2025]]></title><description><![CDATA[A short look back at 2025 and the twelve posts I published, nearly all of them circling the same Next.js stack, from auth and payments to analytics and docs.]]></description><link>https://angelo.dini.dev/blog/looking-back-at-2025/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/looking-back-at-2025/</guid><pubDate>Sat, 31 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-year-spent-in-the-same-stack&quot;&gt;A year spent in the same stack&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;2025 was a quiet year, and after the last few I'll happily take quiet. The monthly writing rhythm held, which I'm pleased about. Reading the list back, though, it's obvious I had one topic stuck in my head for twelve months straight.&lt;/p&gt;&lt;p&gt;Auth, payments, emails, server actions, styling, linting, type-safe APIs, analytics, documentation. Written out like that it looks deliberate, as if I’d sat down in January and drawn up a curriculum. I didn’t. Each month I wrote about whatever I happened to be wrestling with, and it turns out I spent the whole year wrestling with the same application.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-email/&quot;&gt;React Email&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/better-auth/&quot;&gt;BetterAuth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/stripe/&quot;&gt;Stripe Integration in Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/nextjs-server-actions/&quot;&gt;Next.js Server Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/tailwind-css-v4/&quot;&gt;Tailwind CSS v4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-compiler/&quot;&gt;React Compiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/biome/&quot;&gt;Biome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/trpc/&quot;&gt;End-to-End Typesafe APIs with tRPC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/nextjs-15/&quot;&gt;What’s New in Next.js 15&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/posthog/&quot;&gt;Product Analytics with PostHog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/fumadocs/&quot;&gt;Beautiful Documentation with Fumadocs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-id-change&quot;&gt;What I’d change&lt;/h3&gt;
&lt;p&gt;Two of those are the odd ones out. The agents piece and the frameworks piece are me speculating about where things are heading, and they’re the two I’m least happy with. Predictions age badly and I have no particular advantage at making them.&lt;/p&gt;
&lt;p&gt;So for 2026: fewer forecasts, more write-ups of things I’ve actually installed, run and broken.&lt;/p&gt;
&lt;p&gt;See you all in 2026 🎉&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Beautiful Documentation with Fumadocs]]></title><description><![CDATA[Fumadocs for Next.js documentation: MDX collections with typed frontmatter, the Source API, Orama search, theming on Tailwind v4, and when not to use it.]]></description><link>https://angelo.dini.dev/blog/fumadocs/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/fumadocs/</guid><pubDate>Sat, 20 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;docs-that-live-in-the-same-app&quot;&gt;Docs that live in the same app&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Documentation usually ends up in a second repository, with a second build system, a second deployment target and a second set of design decisions that slowly drift from the product it describes. It works, and it is nobody's favourite part of the week. Fumadocs takes the other route: the docs are routes in your existing Next.js app, sharing its bundler, its Tailwind config and its components.&lt;/p&gt;&lt;p&gt;I wrote about &lt;a href=&quot;/blog/docusaurus/&quot;&gt;Docusaurus&lt;/a&gt; a while back and ended by saying that MkDocs with Material is almost always enough. I still think that. Fumadocs is the exception to it, and only for one specific case: when the thing you are documenting is itself a Next.js or React project, and you want the documentation to be able to render the real component rather than a screenshot of it.&lt;/p&gt;
&lt;p&gt;Everything below is against the 16.x line of &lt;code&gt;fumadocs-core&lt;/code&gt; and &lt;code&gt;fumadocs-ui&lt;/code&gt;, with &lt;code&gt;fumadocs-mdx&lt;/code&gt; at 14. The API moved around quite a bit through 2025, so version numbers matter more than usual here.&lt;/p&gt;
&lt;h3 id=&quot;what-makes-it-different&quot;&gt;What makes it different&lt;/h3&gt;
&lt;p&gt;Docusaurus is its own application. It has a build system, a routing model and a theming approach that are all its own. It does not care that you already have a Next.js project; it sits next to one. Nextra grew up in the Pages Router era and has spent a while catching up with the App Router.&lt;/p&gt;
&lt;p&gt;Fumadocs was written for React Server Components from the start. Your pages render on the server and ship almost no client JavaScript; the only interactive parts are the search dialog and the sidebar toggles. If you already run Next.js with Tailwind, adding docs is adding a route group, not adopting a second framework.&lt;/p&gt;
&lt;p&gt;The adoption list is the part that made me look properly. Zod, Better Auth, shadcn/ui and several of Vercel’s own SDK docs run on it, and those are projects whose documentation people actually read.&lt;/p&gt;
&lt;p&gt;The other structural choice is that it splits into a headless core and a styled layer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fumadocs-core&lt;/code&gt; does the work with no opinions about appearance: the Source API, search adapters, Markdown utilities.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fumadocs-ui&lt;/code&gt; is the theme. Sidebar, table of contents, breadcrumbs, code blocks, callouts, built on Tailwind CSS v4.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fumadocs-mdx&lt;/code&gt; turns MDX files on disk into typed data, with validated frontmatter and a generated table of contents.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create-fumadocs-app&lt;/code&gt; scaffolds the whole thing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can take the core alone and build your own presentation. Almost nobody does at first, and the option matters mostly because it means the styled layer is not load-bearing. You can replace a piece of it without forking anything.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-running&quot;&gt;Getting it running&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; create fumadocs-app&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That scaffolds a working Next.js project. In an existing one:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; fumadocs-core fumadocs-ui fumadocs-mdx&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The MDX pipeline is a Next.js plugin, so &lt;code&gt;next.config.ts&lt;/code&gt; has to know about it:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createMDX &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-mdx/next'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; withMDX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createMDX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;withMDX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;declaring-the-content&quot;&gt;Declaring the content&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;source.config.ts&lt;/code&gt; at the project root describes what exists and what shape it has:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// source.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineDocs&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; frontmatterSchema &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-mdx/config'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; docs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineDocs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  dir&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'content/docs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  docs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    schema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; frontmatterSchema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;extend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      category&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'guides'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'api'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'tutorials'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      audience&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'user'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'developer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'developer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things to get right here. &lt;code&gt;schema&lt;/code&gt; takes an actual schema object, not a bag of field definitions. Plenty of tutorials show a plain object literal there, and it will not validate. And you extend &lt;code&gt;frontmatterSchema&lt;/code&gt; rather than replacing it, because that is what already defines &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; for the rest of the pipeline.&lt;/p&gt;
&lt;p&gt;The payoff is that a typo in a frontmatter key fails at build time with a message naming the file, instead of rendering an empty sidebar entry that nobody notices for two months. Zod is the obvious choice but any Standard Schema library works.&lt;/p&gt;
&lt;h4 id=&quot;the-loader&quot;&gt;The loader&lt;/h4&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/source.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; loader &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-core/source'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; docs &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/.source'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; source &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  baseUrl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/docs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  source&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; docs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFumadocsSource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.toFumadocsSource()&lt;/code&gt; is the bit people leave off. The generated &lt;code&gt;docs&lt;/code&gt; export is a collection of entries; the loader wants a Source, and that method converts one into the other. Skip it and TypeScript will tell you, at some length.&lt;/p&gt;
&lt;p&gt;What comes back is the Source API: &lt;code&gt;getPage(slugs)&lt;/code&gt;, &lt;code&gt;getPages()&lt;/code&gt;, &lt;code&gt;getPageTree()&lt;/code&gt;, &lt;code&gt;generateParams()&lt;/code&gt; for static generation. That object is the only interface between your content and your pages.&lt;/p&gt;
&lt;h4 id=&quot;layout-and-page&quot;&gt;Layout and page&lt;/h4&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/docs/layout.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DocsLayout &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-ui/layouts/docs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; source &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/source'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ReactNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Layout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReactNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DocsLayout&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;tree&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;source&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPageTree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;nav&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'My Docs'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;sidebar&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defaultOpenLevel&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DocsLayout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/docs/[[...slug]]/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; source &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/source'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DocsPage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; DocsBody &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-ui/layouts/docs/page'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; notFound &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/navigation'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  params&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  params&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; slug&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; slug &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; params&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; page &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; source&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;notFound&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MDX&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DocsPage&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;toc&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;toc&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DocsBody&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;MDX&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DocsBody&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DocsPage&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateStaticParams&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; source&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;generateParams&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole integration. Drop MDX files into &lt;code&gt;content/docs/&lt;/code&gt; and they appear, with navigation, breadcrumbs and a table of contents built from the headings.&lt;/p&gt;
&lt;h3 id=&quot;writing-in-it&quot;&gt;Writing in it&lt;/h3&gt;
&lt;p&gt;Code blocks go through Shiki at build time, so there is no highlighting library in the client bundle. Line ranges, titles and copy buttons come from the fence:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token code&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;token code-language&quot;&gt;typescript title=&quot;api/users.ts&quot; {3-5}&lt;/span&gt;
&lt;span class=&quot;token code-block language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getUsers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;```&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Callouts, tabs, steps and cards are components you can use directly in MDX:&lt;/p&gt;
&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Callout&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;warn&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  This deletes data and does not ask twice.
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Callout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&amp;lt;Tabs groupId=&quot;package-manager&quot; items={['npm', 'pnpm', 'yarn']}&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Tab&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;npm&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;npm install fumadocs-ui&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Tab&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Tab&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;pnpm&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;pnpm add fumadocs-ui&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Tab&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Tab&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;yarn&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;yarn add fumadocs-ui&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Tab&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Tabs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;groupId&lt;/code&gt; on tabs is the detail I did not expect to care about and now would not give up. Every tab group sharing that id switches together, and the choice persists as the reader moves between pages. A pnpm user picks pnpm once at the top of the install guide and never sees an &lt;code&gt;npm install&lt;/code&gt; again. It is a tiny thing that removes a constant low-grade annoyance from every documentation site that has ever existed.&lt;/p&gt;
&lt;p&gt;For library documentation there is &lt;code&gt;TypeTable&lt;/code&gt;, which renders a props or options table without you hand-maintaining a Markdown grid:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; TypeTable &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-ui/components/type-table'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TypeTable&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'string'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'The user display name'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      required&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;'admin' | 'user'&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'User role'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;'user'&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;search&quot;&gt;Search&lt;/h3&gt;
&lt;p&gt;Orama is the default and it is self-hosted, meaning it runs in your own route handler with no external service:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/search/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; source &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/source'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createFromSource &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-core/search/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createFromSource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;source&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four lines, and Cmd+K works. Fumadocs indexes your content structurally (headings, sections, paragraphs) rather than dumping page text into a blob, so results land you on the right section rather than the top of a long page.&lt;/p&gt;
&lt;p&gt;If you outgrow it, there is an Algolia adapter, though it works differently from what you might expect. It is not a drop-in replacement for the route handler: you write a sync script that runs after a build and pushes records up.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// scripts/sync-algolia.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; algoliasearch &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'algoliasearch'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; sync &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-core/search/algolia'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; source &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/source'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; client &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;algoliasearch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ALGOLIA_APP_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ALGOLIA_ADMIN_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;client&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  indexName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'docs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  documents&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; source&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    _id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    structured&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;structuredData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is an Orama Cloud adapter too if you want hosted search without Algolia’s pricing conversation. For anything under a few hundred pages, plain Orama is the right answer and adding a search vendor is work you have invented for yourself.&lt;/p&gt;
&lt;h3 id=&quot;api-reference-from-openapi&quot;&gt;API reference from OpenAPI&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fumadocs-openapi&lt;/code&gt; is a separate package (&lt;code&gt;npm i fumadocs-openapi shiki&lt;/code&gt;, plus one more stylesheet import), and worth mentioning because most documentation frameworks make you find a third-party plugin for this and then maintain it. You point &lt;code&gt;createOpenAPI&lt;/code&gt; from &lt;code&gt;fumadocs-openapi/server&lt;/code&gt; at an OpenAPI 3.0 or 3.1 document and it generates reference pages: endpoints, parameter tables, request and response schemas, example payloads, styled like the rest of the site rather than like an embedded Swagger UI.&lt;/p&gt;
&lt;p&gt;The value is not the rendering. It is that the reference is generated from the same spec your server validates against, so it cannot drift. Hand-written endpoint tables always drift.&lt;/p&gt;
&lt;h3 id=&quot;theming&quot;&gt;Theming&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fumadocs-ui&lt;/code&gt; is built on Tailwind CSS v4, which means the CSS-first configuration I wrote about in the &lt;a href=&quot;/blog/tailwind-css-v4/&quot;&gt;Tailwind v4&lt;/a&gt; post applies directly. Eight colour themes ship with it: neutral, black, vitepress, dusk, catppuccin, ocean, purple and solar. You pick one by importing its stylesheet.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'tailwindcss'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-ui/css/neutral.css'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-ui/css/preset.css'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@theme&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color-fd-primary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;oklch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0.55 0.2 250&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color-fd-background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;oklch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0.98 0 0&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every token is prefixed &lt;code&gt;--color-fd-*&lt;/code&gt;, which keeps them out of the way of your application’s own variables. That is a small piece of foresight, and it saves a genuinely annoying afternoon when the docs and the app share a stylesheet.&lt;/p&gt;
&lt;p&gt;There is also a &lt;code&gt;shadcn.css&lt;/code&gt; that maps the Fumadocs tokens onto shadcn/ui’s colour system. If your app already uses &lt;a href=&quot;/blog/modular-frontend-with-shadcn/&quot;&gt;shadcn/ui&lt;/a&gt;, that one import makes the documentation look like it belongs to the product rather than next to it.&lt;/p&gt;
&lt;p&gt;Beyond colour, the layout handles RTL, collapsible sidebar folders with configurable open depth, root-level tabs for splitting “Guides” from “API Reference”, banners for version notices, and a mobile navigation that you do not have to think about.&lt;/p&gt;
&lt;h3 id=&quot;multiple-languages&quot;&gt;Multiple languages&lt;/h3&gt;
&lt;p&gt;i18n is configured once and threaded through the loader:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/i18n.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineI18n &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fumadocs-core/i18n'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; i18n &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineI18n&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  languages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'en'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'de'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'fr'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  defaultLanguage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'en'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  hideLocale&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'default-locale'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/source.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; source &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  baseUrl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/docs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  source&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; docs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFumadocsSource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  i18n&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A page tree is built per language, missing translations fall back to the default, and the search index is built per locale so people search in the language they are reading. &lt;code&gt;hideLocale: &amp;#39;default-locale&amp;#39;&lt;/code&gt; keeps &lt;code&gt;/docs/getting-started&lt;/code&gt; rather than &lt;code&gt;/docs/en/getting-started&lt;/code&gt;, which matters if you have existing URLs and an SEO person.&lt;/p&gt;
&lt;p&gt;Living in Switzerland, I have opinions about translation workflows that are not really about tooling. Fumadocs does the mechanical part properly. Keeping four languages actually in sync is a staffing problem, not a framework problem.&lt;/p&gt;
&lt;h3 id=&quot;navigation-and-structure&quot;&gt;Navigation and structure&lt;/h3&gt;
&lt;p&gt;The sidebar comes from the directory structure. Nested folders become nested sections. Where you want to override the ordering or the label, drop a &lt;code&gt;meta.json&lt;/code&gt; into the folder:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Getting Started&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;pages&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;installation&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;quickstart&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;configuration&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is one of the few places I would have designed it differently. Ordering lives in &lt;code&gt;meta.json&lt;/code&gt;, page metadata lives in frontmatter, and when a page is in the wrong place you have to remember which file to open. It is a small tax and it never quite stops being one.&lt;/p&gt;
&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;
&lt;p&gt;MDX compiles to Server Components, so a documentation page ships the HTML and essentially nothing else. &lt;code&gt;generateStaticParams()&lt;/code&gt; means every page can be prerendered at build time, and Orama’s index can be prerendered too, which leaves you with a directory of static files and no server runtime at all.&lt;/p&gt;
&lt;p&gt;The Source API works from in-memory structures built during the build. There is no database, no external service, nothing to provision. For a documentation site that is exactly the right amount of infrastructure.&lt;/p&gt;
&lt;h3 id=&quot;where-it-doesnt-fit&quot;&gt;Where it doesn’t fit&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Where it doesn’t fit&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Fumadocs&lt;/th&gt;
&lt;th&gt;Docusaurus&lt;/th&gt;
&lt;th&gt;Nextra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runs inside&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your Next.js app&lt;/td&gt;
&lt;td&gt;Its own build&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rendering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RSC, minimal JS&lt;/td&gt;
&lt;td&gt;Client-side&lt;/td&gt;
&lt;td&gt;Pages or App Router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind v4&lt;/td&gt;
&lt;td&gt;CSS Modules&lt;/td&gt;
&lt;td&gt;Tailwind or CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MDX with a typed schema&lt;/td&gt;
&lt;td&gt;MDX&lt;/td&gt;
&lt;td&gt;MDX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Orama, Orama Cloud, Algolia&lt;/td&gt;
&lt;td&gt;Algolia&lt;/td&gt;
&lt;td&gt;Flexsearch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API reference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fumadocs-openapi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plugin&lt;/td&gt;
&lt;td&gt;Roll your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customising&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Headless core plus UI layer&lt;/td&gt;
&lt;td&gt;Swizzling&lt;/td&gt;
&lt;td&gt;Theme options&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Docusaurus has a much larger community, more plugins and more Stack Overflow answers, and that is worth real money when something breaks at an awkward hour. Fumadocs is younger and moves faster: the API churn through 2025 was not trivial, and reading a tutorial written six months ago will hand you imports that no longer exist.&lt;/p&gt;
&lt;p&gt;And the complaint I had about Docusaurus applies here twice over. It is React all the way down. A broken MDX import fails your production build, and now it fails your &lt;em&gt;product’s&lt;/em&gt; production build, because they are the same build. That is arguably correct and it is not the trade I would make for a Python service’s documentation, where Sphinx or MkDocs will serve you better and nobody has to learn anything.&lt;/p&gt;
&lt;h3 id=&quot;where-ive-landed&quot;&gt;Where I’ve landed&lt;/h3&gt;
&lt;p&gt;For a Next.js project whose docs need to render live components: Fumadocs, without much hesitation. The docs inherit the design system, the components are the real ones, and there is no second deployment to forget about.&lt;/p&gt;
&lt;p&gt;For everything else, my answer from the Docusaurus post stands. MkDocs with Material is almost always enough, and “almost always enough” remains an underrated property in a documentation tool.&lt;/p&gt;
&lt;p&gt;What I want to try next is pointing &lt;code&gt;fumadocs-openapi&lt;/code&gt; at a spec generated from a running server, so the reference pages fail the build when an endpoint changes shape. That feels like the version of this that would actually keep documentation honest&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Product Analytics with PostHog]]></title><description><![CDATA[Running PostHog in a Next.js App Router app: autocapture, custom events, feature flags, session replay, experiments, surveys and the proxy it all needs.]]></description><link>https://angelo.dini.dev/blog/posthog/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/posthog/</guid><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;one-vendor-instead-of-five&quot;&gt;One vendor instead of five&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;The analytics stack for a small product tends to accrete rather than get designed. Something for events, something else for session recordings, a third thing for feature flags, a fourth when somebody wants an A/B test, and by then nobody can answer &quot;did the users who saw variant B actually convert&quot; without exporting two CSVs and joining them in a spreadsheet.&lt;/p&gt;&lt;p&gt;PostHog’s pitch is that all of it belongs in one database. Analytics, replays, flags, experiments and surveys share the same event stream, which means the join is a filter rather than an export. It is open source, roughly 30,000 stars on GitHub, and the free tier is generous enough that a side project will never see an invoice.&lt;/p&gt;
&lt;p&gt;This is about wiring it into a Next.js App Router project, which has a few sharp edges that the marketing pages do not mention.&lt;/p&gt;
&lt;h3 id=&quot;why-this-one&quot;&gt;Why this one&lt;/h3&gt;
&lt;p&gt;Count the vendors in a typical setup: Google Analytics or Mixpanel for product analytics, Hotjar for recordings, LaunchDarkly for flags, Optimizely for experiments, Typeform for surveys. Five contracts, five scripts on the page, five dashboards, and five different ideas about what a “user” is.&lt;/p&gt;
&lt;p&gt;The open source part matters to me more than it probably should. The core repository is MIT licensed, with the &lt;code&gt;ee/&lt;/code&gt; directory under a separate commercial licence, so you can read exactly what the SDK collects rather than trusting a minified bundle. And there is an EU region if your client’s legal department asks where the data lives, which in Switzerland they generally do.&lt;/p&gt;
&lt;p&gt;The free tier at the time of writing covers a million analytics events, 5,000 session recordings, a million feature flag requests and 1,500 survey responses a month. That is enough to run a small product on indefinitely, which is a rare thing to be able to say about a free tier.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-in&quot;&gt;Getting it in&lt;/h3&gt;
&lt;p&gt;Two packages, and they do different jobs. &lt;code&gt;posthog-js&lt;/code&gt; runs in the browser, &lt;code&gt;posthog-node&lt;/code&gt; runs on the server, and (this is the bit worth knowing up front) they do not automatically know about each other.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; posthog-js posthog-node&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Environment variables:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env.local&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_POSTHOG_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;phc_your_project_api_key
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_POSTHOG_HOST&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://us.i.posthog.com&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Next.js 15.3 there is an &lt;code&gt;instrumentation-client.ts&lt;/code&gt; file that runs before your application code hydrates, which is exactly where SDK initialisation belongs:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// instrumentation-client.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; posthog &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

posthog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_POSTHOG_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  api_host&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/ingest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ui_host&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://us.posthog.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  defaults&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'2025-05-24'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;defaults&lt;/code&gt; value deserves an explanation, because it looks like a version string somebody forgot to remove. PostHog uses dated defaults so they can change behaviour without breaking existing installs: you opt into the behaviour as of a given date. &lt;code&gt;&amp;#39;2025-05-24&amp;#39;&lt;/code&gt; switches pageview capture to &lt;code&gt;&amp;#39;history_change&amp;#39;&lt;/code&gt;, which means client-side navigations in the App Router are captured automatically.&lt;/p&gt;
&lt;p&gt;That matters because most PostHog tutorials for Next.js still show a hand-written &lt;code&gt;PostHogPageView&lt;/code&gt; component that watches &lt;code&gt;usePathname()&lt;/code&gt; and &lt;code&gt;useSearchParams()&lt;/code&gt; and fires a &lt;code&gt;$pageview&lt;/code&gt; on every change. You needed that once. With dated defaults you do not, and the hand-rolled version has a subtle bug anyway. &lt;code&gt;useSearchParams()&lt;/code&gt; forces the component into client-side rendering unless you wrap it in a Suspense boundary, which is easy to forget.&lt;/p&gt;
&lt;p&gt;For the React hooks you still need a provider, in a client component:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/providers.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; posthog &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PostHogProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Providers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;PostHogProvider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;posthog&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;PostHogProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wrap &lt;code&gt;{children}&lt;/code&gt; in your root layout with that, and you are collecting.&lt;/p&gt;
&lt;p&gt;A note on imports, because this changed recently and will bite somebody: PostHog has published the React bindings as a standalone &lt;code&gt;@posthog/react&lt;/code&gt; package, and switched their documentation over to it in the last couple of weeks. &lt;code&gt;posthog-js/react&lt;/code&gt; still works. Check which one the docs page you are reading assumes before you spend twenty minutes on an import error.&lt;/p&gt;
&lt;h3 id=&quot;autocapture-and-its-limits&quot;&gt;Autocapture, and its limits&lt;/h3&gt;
&lt;p&gt;Without writing a line of tracking code, PostHog records clicks on interactive elements, form submissions, pageviews and page leaves, scroll depth, and the four Core Web Vitals: LCP, CLS, FCP and INP. (If you see an older guide mentioning FID, it predates INP replacing it in early 2024.)&lt;/p&gt;
&lt;p&gt;The value of autocapture is not that it replaces deliberate instrumentation. It is that when someone asks in three months “how many people ever clicked that button”, the answer exists retroactively, because you were capturing it before anyone thought to ask. That is genuinely useful and I have stopped arguing about it.&lt;/p&gt;
&lt;p&gt;The limit is that autocaptured events are described by DOM structure. Rename a CSS class, restructure a component, and your “clicks on &lt;code&gt;.btn-primary&lt;/code&gt; in the pricing page” series quietly becomes two series. Anything you plan to report on for more than a quarter should be a named custom event.&lt;/p&gt;
&lt;p&gt;You can narrow what gets captured:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;posthog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_POSTHOG_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  api_host&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/ingest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  defaults&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'2025-05-24'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  autocapture&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    dom_event_allowlist&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'click'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'submit'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    url_allowlist&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'https://yourdomain.com/app/*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    css_selector_allowlist&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'[data-track]'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And anything carrying a &lt;code&gt;ph-no-capture&lt;/code&gt; class is excluded outright. Put it on payment fields and anything showing another person’s data.&lt;/p&gt;
&lt;h3 id=&quot;custom-events&quot;&gt;Custom events&lt;/h3&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; posthog &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;PricingCard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; plan &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; plan&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleUpgrade&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    posthog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;capture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'upgrade_clicked'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      plan&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      source&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'pricing_page'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleUpgrade&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Upgrade to &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;plan&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Properties are arbitrary. Spend a few minutes deciding on naming before you have two hundred of them: &lt;code&gt;snake_case&lt;/code&gt;, verb in the past tense, and the same property key meaning the same thing everywhere. Nobody enforces this and everybody regrets not doing it.&lt;/p&gt;
&lt;h3 id=&quot;identifying-people&quot;&gt;Identifying people&lt;/h3&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; posthog &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IdentifyUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  user&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      posthog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;identify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;identify&lt;/code&gt; merges the anonymous session into the known person, so the pages they read before signing up stay attached to them. Call &lt;code&gt;posthog.reset()&lt;/code&gt; on logout, or the next person to use that browser inherits the identity, which on a shared machine is a data protection problem rather than just a messy chart.&lt;/p&gt;
&lt;h3 id=&quot;feature-flags&quot;&gt;Feature flags&lt;/h3&gt;
&lt;p&gt;Create a flag in the dashboard, read it in a component:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useFeatureFlagEnabled &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Dashboard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; showNewDashboard &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useFeatureFlagEnabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'new-dashboard'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;showNewDashboard&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NewDashboard&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;OldDashboard&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three kinds exist: boolean flags for on/off, multivariate flags for several named variants with rollout percentages, and remote config for values you want to change without a deploy. Targeting works on person properties, cohorts, percentage rollouts, geography, and dependencies on other flags.&lt;/p&gt;
&lt;p&gt;On the server you use &lt;code&gt;posthog-node&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/posthog-server.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PostHog &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-node'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posthogServer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PostHog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_POSTHOG_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; host&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://us.i.posthog.com'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; posthogServer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/posthog-server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; getSession &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSession&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; flags &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; posthogServer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAllFlags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;flags&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'new-header'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NewHeader&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;OldHeader&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hook returns &lt;code&gt;undefined&lt;/code&gt; on first render while flags load, which produces a visible flicker: the old header paints, then swaps. The fix is bootstrapping: evaluate the flags on the server and hand them to &lt;code&gt;posthog.init&lt;/code&gt; as &lt;code&gt;bootstrap: { featureFlags }&lt;/code&gt;, so the client has values before it renders anything. It is more wiring than it should be, and it is the difference between a flag you can use above the fold and one you cannot.&lt;/p&gt;
&lt;h3 id=&quot;session-replay&quot;&gt;Session replay&lt;/h3&gt;
&lt;p&gt;Replays are DOM snapshots, not video, so they are small and the text in them is selectable.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;posthog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_PUBLIC_POSTHOG_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  api_host&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/ingest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  defaults&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'2025-05-24'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  session_recording&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    maskAllInputs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    maskTextSelector&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'.sensitive-data'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Masking happens in the browser before anything is sent, which is the only design that survives a conversation with a privacy officer. You can also limit &lt;em&gt;when&lt;/em&gt; recording happens: only on certain URLs, only after an error event, only for users in a given flag, or a percentage sample to keep within quota.&lt;/p&gt;
&lt;p&gt;The reason replay is worth having in the same tool as everything else is the cross-referencing. Watch the recordings of the twelve people who dropped out of your signup funnel at step three. That is one filter, not an export. Twelve recordings will tell you more than a month of staring at the funnel chart.&lt;/p&gt;
&lt;h3 id=&quot;experiments&quot;&gt;Experiments&lt;/h3&gt;
&lt;p&gt;Experiments sit on top of multivariate flags:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useFeatureFlagVariantKey &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CheckoutButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; variant &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useFeatureFlagVariantKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'checkout-experiment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;variant &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'test'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;btn-green&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Complete Purchase&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;btn-blue&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Checkout&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You define the success metric in the dashboard and PostHog does the statistics. It defaults to a Bayesian analysis, so the result reads as “a 96% probability that variant B is better” rather than a p-value that half the people in the room will interpret incorrectly. A frequentist mode is available if your organisation prefers it.&lt;/p&gt;
&lt;p&gt;You can run one control against up to nine variants, test at group level rather than per person (useful when the unit of decision is a company account, not an individual), and set aside a holdout group to check that the accumulated wins are real.&lt;/p&gt;
&lt;h3 id=&quot;surveys&quot;&gt;Surveys&lt;/h3&gt;
&lt;p&gt;Most surveys need no code at all: build a popover in the dashboard, target it at a URL pattern or a person property, and it appears. When you want to trigger one yourself:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; posthog &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'posthog-js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FeedbackButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
        posthog&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;displaySurvey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'survey-id'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; displayType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'popover'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      Give Feedback
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lowercase &lt;code&gt;&amp;#39;popover&amp;#39;&lt;/code&gt;; &lt;code&gt;&amp;#39;inline&amp;#39;&lt;/code&gt; is the other option and needs a &lt;code&gt;selector&lt;/code&gt; telling it where to render. &lt;code&gt;renderSurvey&lt;/code&gt; still exists and still works, but it was deprecated a couple of months ago in favour of &lt;code&gt;displaySurvey&lt;/code&gt;, so start with the new one.&lt;/p&gt;
&lt;p&gt;Question types cover free text, ratings on emoji or numeric scales, and single or multiple choice. Targeting can key off a flag, a URL, a device type, person properties, or an event that happened earlier in the session, which is how you ask “what went wrong?” only of people who actually hit the error.&lt;/p&gt;
&lt;h3 id=&quot;the-proxy-is-not-optional&quot;&gt;The proxy is not optional&lt;/h3&gt;
&lt;p&gt;A meaningful share of visitors run an ad blocker, and every one of them blocks requests to &lt;code&gt;i.posthog.com&lt;/code&gt;. Route the traffic through your own domain instead:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rewrites&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/ingest/static/:path*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://us-assets.i.posthog.com/static/:path*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/ingest/array/:path*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://us-assets.i.posthog.com/array/:path*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/ingest/:path*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://us.i.posthog.com/:path*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three rules, and the order is load-bearing: the catch-all must come last or it swallows the other two. The &lt;code&gt;/array/&lt;/code&gt; one serves remote config and is the one people leave out, then wonder why their flags never arrive. Set &lt;code&gt;api_host: &amp;#39;/ingest&amp;#39;&lt;/code&gt; (as above) and &lt;code&gt;ui_host&lt;/code&gt; to the real PostHog URL so the toolbar links still work. Swap &lt;code&gt;us&lt;/code&gt; for &lt;code&gt;eu&lt;/code&gt; on the EU region.&lt;/p&gt;
&lt;h3 id=&quot;tracking-from-the-server&quot;&gt;Tracking from the server&lt;/h3&gt;
&lt;p&gt;Anything that happens without a browser (a webhook, a Stripe callback, a cron job) needs &lt;code&gt;posthog-node&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; posthogServer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/posthog-server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;purchaseAction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSession&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; plan &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'plan'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;processPurchase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;plan&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  posthogServer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;capture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    distinctId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    event&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'purchase_completed'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    properties&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; plan&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; source&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'upgrade_page'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the explicit &lt;code&gt;distinctId&lt;/code&gt;. Nothing links the browser’s anonymous ID to your server events for you: if you pass a different identifier on the server than the one you passed to &lt;code&gt;posthog.identify&lt;/code&gt; on the client, you get two person records for the same human, and every funnel that crosses the boundary breaks. Pick the identifier once, put it in a helper, and use it everywhere.&lt;/p&gt;
&lt;p&gt;The other server-side trap is flushing. &lt;code&gt;posthog-node&lt;/code&gt; batches and sends on an interval, which is fine on a long-lived process and useless in a serverless function that gets frozen the moment it returns. On Vercel, wrap the capture in the &lt;a href=&quot;/blog/nextjs-15/&quot;&gt;&lt;code&gt;after()&lt;/code&gt; API&lt;/a&gt; and call &lt;code&gt;flush()&lt;/code&gt; there, or &lt;code&gt;shutdown()&lt;/code&gt; if the process really is ending. Events you never flushed do not show up as an error anywhere. They just are not there.&lt;/p&gt;
&lt;h3 id=&quot;what-the-data-is-for&quot;&gt;What the data is for&lt;/h3&gt;
&lt;p&gt;Once events are flowing, the dashboard gives you trends over time, funnels through a multi-step process, retention cohorts, path analysis, lifecycle segmentation into new/returning/resurrecting/dormant, and stickiness. All of it filters on person properties, flag state, geography and anything you attached to an event. When the built-in charts run out, HogQL lets you write SQL directly against the event table.&lt;/p&gt;
&lt;p&gt;The part that took me longest to appreciate is that you should build fewer of these than you think. A dashboard with thirty charts is a dashboard nobody reads. Three numbers somebody checks on a Monday will change more decisions.&lt;/p&gt;
&lt;h3 id=&quot;self-hosting-honestly&quot;&gt;Self-hosting, honestly&lt;/h3&gt;
&lt;p&gt;You can run the whole thing yourself:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;/bin/bash &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/posthog/posthog/HEAD/bin/deploy-hobby&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the form: the script prompts you interactively, so piping it into &lt;code&gt;bash&lt;/code&gt; lets the pipe swallow its own input and it sits there looking hung.&lt;/p&gt;
&lt;p&gt;It wants something like 4 vCPUs, 16GB of RAM and more than 30GB of disk. And PostHog are unusually direct that self-hosting is unsupported: no help if it breaks, none of the paid features, and the data is your problem. Given that it is ClickHouse, Kafka, Redis and Postgres in a trench coat, I would take them at their word unless data residency leaves you no choice, and the EU cloud region probably already solves that.&lt;/p&gt;
&lt;h3 id=&quot;where-id-stop&quot;&gt;Where I’d stop&lt;/h3&gt;
&lt;p&gt;PostHog’s breadth is the reason to use it and the reason to be a little sceptical of it. LaunchDarkly’s flag targeting is more sophisticated. Hotjar’s heatmaps are more mature. Mixpanel’s cohort analysis goes deeper. If one of those is genuinely core to how your team works, PostHog will feel like the second-best tool for that job.&lt;/p&gt;
&lt;p&gt;For everyone else, having one event stream that flags, replays and experiments all agree on is worth more than any individual feature being best in class. That is the actual argument, and the vendor consolidation is a side effect.&lt;/p&gt;
&lt;p&gt;What I would still like is a decent story for identity across the client/server boundary that does not involve me threading a distinct ID through by hand. PostHog have been circling something for Next.js specifically; when it lands I will happily delete a helper file&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[What's New in Next.js 15]]></title><description><![CDATA[A practical tour of Next.js 15: async request APIs, the reversed caching defaults, stable Turbopack dev, after(), instrumentation and the upgrade path from 14.]]></description><link>https://angelo.dini.dev/blog/nextjs-15/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/nextjs-15/</guid><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-release-that-took-a-default-back&quot;&gt;The release that took a default back&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Frameworks rarely admit that a default was wrong. Next.js 15 does, twice. Fetches are no longer cached behind your back, GET route handlers are no longer static by accident, and the request APIs you have been calling synchronously for years now hand you a promise. It is the least glamorous kind of release and, I think, the most useful one Next.js has shipped.&lt;/p&gt;&lt;p&gt;Next.js 16 landed four days ago, which makes writing about 15 feel slightly odd. It is not, though: most of the codebases I touch are on some 15.x, and they will be for a good while yet. Everything below is about that line, from 15.0 through the point releases that quietly fixed half of it.&lt;/p&gt;
&lt;p&gt;The two changes worth reading carefully are the async request APIs and the caching reversal. Both require you to do something. The rest is upside.&lt;/p&gt;
&lt;h3 id=&quot;turbopack-now-by-choice&quot;&gt;Turbopack, now by choice&lt;/h3&gt;
&lt;p&gt;The Rust bundler went stable for development in 15. You opt in on the dev script:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;next dev &lt;span class=&quot;token parameter variable&quot;&gt;--turbopack&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vercel published numbers from their own application, which is a large real-world Next.js codebase rather than a benchmark repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;up to &lt;strong&gt;76.7% faster&lt;/strong&gt; local server startup&lt;/li&gt;
&lt;li&gt;up to &lt;strong&gt;96.3% faster&lt;/strong&gt; code updates with Fast Refresh&lt;/li&gt;
&lt;li&gt;up to &lt;strong&gt;45.8% faster&lt;/strong&gt; initial route compile, with no disk caching at that point&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;15.2 pushed compile times a further 57.6% down against 15.1, with roughly 30% less memory used during local development. My own experience is less dramatic than those headlines but consistently better, and the gap widens the bigger the project gets.&lt;/p&gt;
&lt;p&gt;Turbopack takes a different bet from Vite. Vite serves modules unbundled over native ESM in dev, which starts instantly and then pays a little on every request. Turbopack bundles, but lazily, compiling only what the route you are looking at actually needs. You get fast startup without the dev/production divergence that unbundled ESM occasionally produces.&lt;/p&gt;
&lt;p&gt;Under the hood it is SWC for JavaScript and TypeScript, Lightning CSS for stylesheets. Webpack &lt;em&gt;loaders&lt;/em&gt; work through the &lt;code&gt;turbopack.rules&lt;/code&gt; config (that key lived under &lt;code&gt;experimental.turbo&lt;/code&gt; until 15.3, so check which version you are reading docs for). Webpack &lt;em&gt;plugins&lt;/em&gt; do not work at all, and there is no compatibility layer coming. If your build depends on a plugin, that is your blocker, and it is worth finding out on day one rather than after you have rewritten your dev script.&lt;/p&gt;
&lt;h3 id=&quot;everything-request-shaped-is-now-a-promise&quot;&gt;Everything request-shaped is now a promise&lt;/h3&gt;
&lt;p&gt;This is the breaking change. These are all asynchronous:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cookies()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;headers()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draftMode()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;params&lt;/code&gt;, in layouts, pages, route handlers, &lt;code&gt;default.js&lt;/code&gt;, &lt;code&gt;generateMetadata&lt;/code&gt; and &lt;code&gt;generateViewport&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;searchParams&lt;/code&gt;, in pages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So this, from 14:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cookies &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/headers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Dashboard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cookieStore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cookieStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'session'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Dashboard for &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;token&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes this:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cookies &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/headers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Dashboard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cookieStore &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cookies&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cookieStore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'session'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Dashboard for &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;token&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Props follow the same rule:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  params&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  searchParams&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  params&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; slug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  searchParams&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; query&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; slug &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; params&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; query &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; searchParams&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Slug: &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;, Query: &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reasoning is worth understanding, because otherwise this looks like churn for its own sake. In traditional server rendering, nothing happens until a request shows up. But plenty of your component tree does not care about the request at all: a header, a nav, a marketing block. If the framework can tell which components need request data and which do not, it can render the second group ahead of time. Making the request APIs async is how it tells. That is the foundation for Partial Prerendering, further down.&lt;/p&gt;
&lt;p&gt;A codemod does most of the work:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @next/codemod@canary next-async-request-api &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It handles direct call sites well and gives up on anything indirect: a helper that takes &lt;code&gt;cookies()&lt;/code&gt; as an argument, a &lt;code&gt;params&lt;/code&gt; object destructured three levels deep, anything behind a conditional. Expect to finish by hand. The synchronous forms still work in 15 with a deprecation warning, so you can migrate over a few pull requests rather than in one heroic commit.&lt;/p&gt;
&lt;h3 id=&quot;nothing-is-cached-until-you-say-so&quot;&gt;Nothing is cached until you say so&lt;/h3&gt;
&lt;p&gt;Next.js 14 cached aggressively by default, and the resulting confusion was the single most common complaint I heard about the App Router. You would ship a fix, refresh, and see the old data. 15 reverses the defaults.&lt;/p&gt;
&lt;h4 id=&quot;fetches&quot;&gt;Fetches&lt;/h4&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Not cached (the 15 default)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'https://api.example.com/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Explicitly cached&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'https://api.example.com/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  cache&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'force-cache'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Cached, revalidated hourly&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'https://api.example.com/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  next&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3600&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;get-route-handlers&quot;&gt;GET route handlers&lt;/h4&gt;
&lt;p&gt;In 14, a GET handler that used no dynamic function was rendered once at build time. In 15 it is dynamic unless you say otherwise:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dynamic &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'force-static'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'cached'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Metadata files are the exception. &lt;code&gt;sitemap.ts&lt;/code&gt;, &lt;code&gt;opengraph-image.tsx&lt;/code&gt;, &lt;code&gt;icon.tsx&lt;/code&gt; and friends stay static by default unless they reach for something dynamic.&lt;/p&gt;
&lt;h4 id=&quot;the-client-router-cache&quot;&gt;The client router cache&lt;/h4&gt;
&lt;p&gt;Page segments now have a &lt;code&gt;staleTime&lt;/code&gt; of 0, so navigating to a route fetches its data again. Layouts still are not refetched, which keeps partial rendering intact, and back/forward navigation still restores from cache so the browser can put your scroll position back. &lt;code&gt;loading.js&lt;/code&gt; stays cached for five minutes.&lt;/p&gt;
&lt;p&gt;If you liked the old behaviour:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;experimental&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;staleTimes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;dynamic&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would not reach for that config, personally. Stale data is a horrible class of bug to debug because it never reproduces on your machine, and you spend the first hour convinced the problem is in your query. Choosing correctness first and adding caching deliberately where the profile says it matters is simply the better order to work in.&lt;/p&gt;
&lt;h3 id=&quot;react-19-underneath&quot;&gt;React 19 underneath&lt;/h3&gt;
&lt;p&gt;15.0 shipped the App Router on React 19 RC; 15.1 moved to React 19 stable, and the Pages Router kept working with React 18 for anyone not ready. In practice that means everything from the &lt;a href=&quot;/blog/react-19/&quot;&gt;React 19 release&lt;/a&gt; is available: Server Actions as real form handlers, &lt;code&gt;useActionState&lt;/code&gt;, &lt;code&gt;useFormStatus&lt;/code&gt;, &lt;code&gt;useOptimistic&lt;/code&gt;, and &lt;code&gt;use&lt;/code&gt; for reading a promise during render.&lt;/p&gt;
&lt;p&gt;React 19 also added sibling pre-warming for Suspense. When one boundary suspends, React begins work on its siblings instead of sitting idle, which shows up as less staggered loading on pages with several independent data sources.&lt;/p&gt;
&lt;h3 id=&quot;after&quot;&gt;&lt;code&gt;after()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Some work has no business blocking a response. Logging, analytics, syncing a row to some other system. On a serverless platform you cannot simply fire a promise and walk away, because the function is torn down the moment the response closes.&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; after &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;track&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'page_view'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; page&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/dashboard'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Dashboard&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It arrived as &lt;code&gt;unstable_after&lt;/code&gt; behind an experimental flag in 15.0 and went stable in 15.1, dropping the prefix. It works in Server Components, Server Actions, route handlers and middleware. The important difference from a bare &lt;code&gt;waitUntil&lt;/code&gt; is that the framework owns the lifecycle, including on self-hosted servers, so the callback still runs when the client disconnects mid-stream.&lt;/p&gt;
&lt;h3 id=&quot;the-form-component&quot;&gt;The &lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt; component&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;next/form&lt;/code&gt; is a small thing that removes a nuisance:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Form &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/form'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SearchForm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Form&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/search&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Search…&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Search&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Form&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It prefetches the target layout and loading UI once the form scrolls into view, navigates client-side on submit so shared layouts and state survive, and falls back to a plain full-page form post if JavaScript has not loaded yet. Getting all three by hand meant a &lt;code&gt;useRouter&lt;/code&gt;, an &lt;code&gt;onSubmit&lt;/code&gt; that reassembles &lt;code&gt;FormData&lt;/code&gt; into a query string, and a &lt;code&gt;router.prefetch&lt;/code&gt; in an effect. I have written that component more than once. I would rather not write it again.&lt;/p&gt;
&lt;p&gt;Worth being clear about the scope: this is for forms that &lt;em&gt;navigate&lt;/em&gt;, like search. Forms that mutate still want a Server Action.&lt;/p&gt;
&lt;h3 id=&quot;instrumentationts&quot;&gt;&lt;code&gt;instrumentation.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Stable in 15, and the &lt;code&gt;experimental.instrumentationHook&lt;/code&gt; flag can come out of your config. Put the file at the project root:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// instrumentation.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NEXT_RUNTIME&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'nodejs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NodeSDK &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'@opentelemetry/sdk-node'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NodeSDK&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onRequestError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  err&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Error&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  context&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; routerKind&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; routeType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; errorTracker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;captureException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;err&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    routeType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;routeType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;onRequestError&lt;/code&gt; was designed with Sentry, and the context it hands you is the part that matters: which router, and whether the error came from a Server Component, a Server Action, a route handler or middleware. Stack traces from a server render are otherwise not much help in telling you &lt;em&gt;where you were&lt;/em&gt; when it blew up.&lt;/p&gt;
&lt;h3 id=&quot;errors-you-can-actually-read&quot;&gt;Errors you can actually read&lt;/h3&gt;
&lt;p&gt;Spread across the point releases, and easy to overlook in a changelog:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;15.0&lt;/strong&gt;: hydration errors now show the offending source with a suggested fix, instead of the old diff-of-two-trees message that told you nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;15.1&lt;/strong&gt;: source maps use the &lt;code&gt;ignoreList&lt;/code&gt; property, so frames from your dependencies collapse out of the way in both the browser overlay and the terminal. Terminal output was reformatted to match the browser. Ignored frames also disappear from browser profiles, which makes profiling a Next.js app far less noisy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;15.2&lt;/strong&gt;: a redesigned overlay built on React’s &lt;code&gt;captureOwnerStack&lt;/code&gt;, which points at the subcomponent that actually created the bad element rather than the intermediate that passed it along. A new dev indicator shows rendering mode, Turbopack compile status and active errors in one place.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is a feature you would put on a slide. All of it is time you get back.&lt;/p&gt;
&lt;h3 id=&quot;metadata-stopped-blocking&quot;&gt;Metadata stopped blocking&lt;/h3&gt;
&lt;p&gt;In 14, an async &lt;code&gt;generateMetadata&lt;/code&gt; had to resolve before any HTML went out, because the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; belongs in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and the head comes first. One slow CMS call and your entire page waited on it.&lt;/p&gt;
&lt;p&gt;From 15.2 the initial UI streams immediately and metadata is streamed into the head when it resolves. Bots and crawlers still get the complete document up front, detected by user agent, and you can adjust which agents get that treatment with &lt;code&gt;htmlLimitedBots&lt;/code&gt; in &lt;code&gt;next.config&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;nextconfigts&quot;&gt;&lt;code&gt;next.config.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;TypeScript config, with a &lt;code&gt;NextConfig&lt;/code&gt; type:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  experimental&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    reactCompiler&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  turbopack&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    rules&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;'*.svg'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        loaders&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'@svgr/webpack'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'*.js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; nextConfig&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reactCompiler&lt;/code&gt; still lives under &lt;code&gt;experimental&lt;/code&gt; in the 15 line. It runs through Babel, so builds get slower when you turn it on; there is more on the trade-off in my post on the &lt;a href=&quot;/blog/react-compiler/&quot;&gt;React Compiler&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;server-actions-got-quieter&quot;&gt;Server Actions got quieter&lt;/h3&gt;
&lt;p&gt;Two changes, both automatic. Unused Server Actions are removed by dead code elimination during the build, so an exported action nobody calls no longer sits there as a live public endpoint. Actions that &lt;em&gt;are&lt;/em&gt; used get non-deterministic IDs, recalculated between builds, which means you cannot guess an action URL and you cannot replay one from a previous deployment.&lt;/p&gt;
&lt;p&gt;That is a genuine improvement, and it is not a security model. A Server Action is still a public HTTP endpoint that anyone can call with any payload. Validate the session and the input inside the action itself, every time: the same discipline I described in the post on &lt;a href=&quot;/blog/nextjs-server-actions/&quot;&gt;Server Actions&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;self-hosting&quot;&gt;Self-hosting&lt;/h3&gt;
&lt;p&gt;Vercel gets accused of neglecting the self-hosted path. 15 pushes back on that a little:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom &lt;code&gt;Cache-Control&lt;/code&gt; headers are no longer overwritten with framework defaults, so your CDN configuration finally survives contact with the app.&lt;/li&gt;
&lt;li&gt;The stale-while-revalidate window is configurable as &lt;code&gt;expireTime&lt;/code&gt; in &lt;code&gt;next.config&lt;/code&gt;, replacing the old &lt;code&gt;experimental.swrDelta&lt;/code&gt;. Its default moved to one year, which is what most CDNs need to apply SWR semantics properly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sharp&lt;/code&gt; no longer needs installing by hand. &lt;code&gt;next start&lt;/code&gt; and standalone output use it automatically, which removes a deployment gotcha that bit people for years.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;partial-prerendering-still-experimental&quot;&gt;Partial Prerendering, still experimental&lt;/h3&gt;
&lt;p&gt;PPR is what the async request APIs are for. A page gets split into a static shell and dynamic holes:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Suspense &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; experimental_ppr &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Dashboard&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Sidebar&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Skeleton&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DynamicContent&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The heading and sidebar are prerendered and served from the edge instantly. &lt;code&gt;DynamicContent&lt;/code&gt; renders per request and streams into its slot. You enable it per route with that export, having first switched it on in the config:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  experimental&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    ppr&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'incremental'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is still experimental, and I would not put it on a client project yet. But it explains the whole shape of the release: without knowing which components need the request, the framework cannot decide what to prerender.&lt;/p&gt;
&lt;h3 id=&quot;upgrading-from-14&quot;&gt;Upgrading from 14&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @next/codemod@canary upgrade latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That updates dependencies and offers the relevant codemods. Four things to check afterwards:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Async APIs.&lt;/strong&gt; The codemod gets the direct cases. Hunt for helpers that pass &lt;code&gt;cookies()&lt;/code&gt; or &lt;code&gt;params&lt;/code&gt; around.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching.&lt;/strong&gt; Anything that was fast because it was accidentally cached is now hitting your database on every request. Add &lt;code&gt;force-cache&lt;/code&gt; or &lt;code&gt;revalidate&lt;/code&gt; deliberately, and watch your query counts after deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config renames.&lt;/strong&gt; &lt;code&gt;serverComponentsExternalPackages&lt;/code&gt; is now &lt;code&gt;serverExternalPackages&lt;/code&gt;, &lt;code&gt;bundlePagesExternals&lt;/code&gt; is now &lt;code&gt;bundlePagesRouterDependencies&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js.&lt;/strong&gt; Minimum is 18.18.0.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The migration I would budget real time for is the second one. Type errors announce themselves; a fetch that used to be free and now is not will only show up as a slightly unhappy database.&lt;/p&gt;
&lt;h3 id=&quot;what-id-do&quot;&gt;What I’d do&lt;/h3&gt;
&lt;p&gt;If you are on 14 and staying put, do the async request API migration anyway. It is the one piece of work that carries forward, and 16 has already removed the synchronous fallbacks.&lt;/p&gt;
&lt;p&gt;The thing still keeping Turbopack out of older projects is the webpack plugin gap, and no amount of startup-time percentage fixes that. Check your plugin list before you promise anyone a faster dev server&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[End-to-End Typesafe APIs with tRPC]]></title><description><![CDATA[How tRPC v11 keeps API types in sync across a Next.js App Router project: routers, procedures, middleware, Zod validation and the TanStack Query integration.]]></description><link>https://angelo.dini.dev/blog/trpc/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/trpc/</guid><pubDate>Sat, 27 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-same-shape-written-twice&quot;&gt;The same shape, written twice&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every full-stack TypeScript project I have worked on ends up with the same quiet tax. The server knows what a user looks like. The client also knows what a user looks like. Those two pieces of knowledge live in different files, and sooner or later they disagree with each other. The disagreement never announces itself at compile time. It shows up in production, as &lt;code&gt;undefined is not a function&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;tRPC is a fairly blunt answer to that. Instead of describing your API in a schema and generating types from it, you write server functions and let TypeScript’s inference carry their signatures across to the client. No SDL, no codegen step, no &lt;code&gt;openapi.json&lt;/code&gt; sitting in the repo going stale. It pairs particularly well with a Next.js app that already talks to a typed database layer such as &lt;a href=&quot;/blog/prisma/&quot;&gt;Prisma&lt;/a&gt;, because at that point the types run all the way from a column definition to a button handler.&lt;/p&gt;
&lt;p&gt;I want to walk through tRPC v11 in a Next.js App Router project, because v11 changed the React integration enough that most of the tutorials you find are subtly wrong.&lt;/p&gt;
&lt;h3 id=&quot;why-bother&quot;&gt;Why bother&lt;/h3&gt;
&lt;p&gt;Take the ordinary REST case. You add a &lt;code&gt;GET /api/users/:id&lt;/code&gt; route. On the client you write an &lt;code&gt;interface User&lt;/code&gt; so that &lt;code&gt;fetch()&lt;/code&gt; returns something better than &lt;code&gt;any&lt;/code&gt;. Now there are two definitions of a user in the codebase and nothing at all connecting them. Rename a field on the server and the client compiles happily, right up until it renders &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;GraphQL fixes the &lt;em&gt;connection&lt;/em&gt; problem by making the schema the single source of truth. It charges you for it, though: a schema language, resolvers, a codegen step, and a build pipeline that has to run in the right order or your types are from last Tuesday. For a large team serving several clients that’s a fair trade. For one person shipping a Next.js app it is a lot of ceremony.&lt;/p&gt;
&lt;p&gt;tRPC removes the middle layer entirely. There is no schema because the router &lt;em&gt;is&lt;/em&gt; the schema, and the client reads its type. Change a procedure’s input and the call sites go red immediately, in the editor, before you have saved anything. I found that hard to give up again after a week of it.&lt;/p&gt;
&lt;p&gt;The obvious catch is that both ends have to be TypeScript. tRPC gives you nothing if a Swift app or a partner’s Python service needs to call your API. For those, REST with an OpenAPI document or GraphQL are still the right answers. But if the API only ever serves your own Next.js frontend, the polyglot flexibility you are paying for is flexibility you will never use.&lt;/p&gt;
&lt;h3 id=&quot;the-pieces&quot;&gt;The pieces&lt;/h3&gt;
&lt;p&gt;There are five nouns and you need all of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Procedures are the endpoints. A &lt;code&gt;query&lt;/code&gt; reads, a &lt;code&gt;mutation&lt;/code&gt; writes, a &lt;code&gt;subscription&lt;/code&gt; streams.&lt;/li&gt;
&lt;li&gt;Routers group procedures under a namespace, and nest as deep as you like.&lt;/li&gt;
&lt;li&gt;Context is per-request state (the session, a database handle) that every procedure receives.&lt;/li&gt;
&lt;li&gt;Middleware wraps procedures, and crucially can &lt;em&gt;narrow the context type&lt;/em&gt; for everything downstream.&lt;/li&gt;
&lt;li&gt;Validators check input and, optionally, output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That fourth point is the one people underestimate. I will come back to it.&lt;/p&gt;
&lt;h3 id=&quot;wiring-it-into-nextjs&quot;&gt;Wiring it into Next.js&lt;/h3&gt;
&lt;h4 id=&quot;1-install&quot;&gt;1. Install&lt;/h4&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @trpc/server @trpc/client @trpc/tanstack-react-query &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  @tanstack/react-query zod&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The React bindings live in &lt;code&gt;@trpc/tanstack-react-query&lt;/code&gt; now. If you find a tutorial importing &lt;code&gt;createTRPCReact&lt;/code&gt; from &lt;code&gt;@trpc/react-query&lt;/code&gt;, that is the v10 integration. It still works in v11, but it is not the one being developed.&lt;/p&gt;
&lt;h4 id=&quot;2-initialise-trpc&quot;&gt;2. Initialise tRPC&lt;/h4&gt;
&lt;p&gt;One instance, created once, exporting the builders everything else uses:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server/trpc.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; initTRPC&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TRPCError &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Context&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  db&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; initTRPC&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Context&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; router &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;router&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; publicProcedure &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;procedure&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; TRPCError &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-write-a-router&quot;&gt;3. Write a router&lt;/h4&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server/routers/users.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; router&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; publicProcedure&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TRPCError &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; usersRouter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  list&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; publicProcedure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  byId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; publicProcedure
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; user &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findUnique&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'NOT_FOUND'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'User not found'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  create&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; publicProcedure
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;.input()&lt;/code&gt; call does double duty. At runtime it validates and rejects bad payloads; at compile time it types &lt;code&gt;input&lt;/code&gt; inside the handler as &lt;code&gt;{ name: string; email: string }&lt;/code&gt;. You write the schema once and get both. Zod is the common choice but not a requirement, since v11 accepts anything implementing Standard Schema (Valibot, ArkType and friends all work).&lt;/p&gt;
&lt;h4 id=&quot;4-merge-into-an-app-router&quot;&gt;4. Merge into an app router&lt;/h4&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server/routers/index.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; router &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; usersRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./users'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; postsRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; appRouter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  users&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; usersRouter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  posts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; postsRouter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Export the TYPE, never the implementation&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AppRouter&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; appRouter&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last line matters more than it looks. &lt;code&gt;export type&lt;/code&gt; is erased at build time, so importing &lt;code&gt;AppRouter&lt;/code&gt; in a client component pulls in exactly zero bytes of server code. Export the value by accident and you have just shipped your database queries to the browser.&lt;/p&gt;
&lt;h4 id=&quot;5-serve-it&quot;&gt;5. Serve it&lt;/h4&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/trpc/[trpc]/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fetchRequestHandler &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/server/adapters/fetch'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; appRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/routers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/context'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handler&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;fetchRequestHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    endpoint&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/api/trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    router&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; appRouter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    createContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; handler &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handler &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POST&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One catch-all route, and every procedure in the tree is reachable.&lt;/p&gt;
&lt;h4 id=&quot;6-set-up-the-client&quot;&gt;6. Set up the client&lt;/h4&gt;
&lt;p&gt;This is the part that changed in v11. &lt;code&gt;createTRPCContext&lt;/code&gt; hands you a provider and a hook, and the client itself comes from &lt;code&gt;@trpc/client&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/trpc.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createTRPCContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/tanstack-react-query'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; AppRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/routers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; TRPCProvider&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useTRPC&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useTRPCClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createTRPCContext&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;AppRouter&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/providers.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; QueryClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; QueryClientProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@tanstack/react-query'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createTRPCClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; httpBatchLink &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; TRPCProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; AppRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/routers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Providers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;queryClient&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;QueryClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;trpcClient&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createTRPCClient&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;AppRouter&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      links&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;httpBatchLink&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/api/trpc'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;QueryClientProvider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;queryClient&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TRPCProvider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;trpcClient&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;trpcClient&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;queryClient&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;queryClient&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TRPCProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;QueryClientProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note &lt;code&gt;trpcClient&lt;/code&gt;, not &lt;code&gt;client&lt;/code&gt;. I lost a while to that one.&lt;/p&gt;
&lt;h3 id=&quot;calling-it-from-a-component&quot;&gt;Calling it from a component&lt;/h3&gt;
&lt;p&gt;v11 dropped the old &lt;code&gt;trpc.users.list.useQuery()&lt;/code&gt; style in favour of handing you plain TanStack Query option objects. It reads as slightly more verbose and is much less magical:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useQuery&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useMutation&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useQueryClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@tanstack/react-query'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useTRPC &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;UserList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; trpc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTRPC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; queryClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useQueryClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; users&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isPending &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trpc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;list&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;queryOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; createUser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useMutation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    trpc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;create&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mutationOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function-variable function&quot;&gt;onSuccess&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        queryClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;invalidateQueries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; queryKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; trpc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;list&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;queryKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Loading…&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;users&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
          createUser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mutate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'New User'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'new@example.com'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        Add User
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The win over the v10 hooks is that &lt;code&gt;useQuery&lt;/code&gt; here is the real TanStack hook. Anything you know about &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;enabled&lt;/code&gt;, &lt;code&gt;placeholderData&lt;/code&gt; or suspense applies directly, and you are not waiting for tRPC to wrap each new Query option. &lt;code&gt;queryKey()&lt;/code&gt; giving you the exact key tRPC used is the small detail that makes invalidation stop being guesswork.&lt;/p&gt;
&lt;h3 id=&quot;context-and-the-middleware-trick&quot;&gt;Context, and the middleware trick&lt;/h3&gt;
&lt;p&gt;Context is built once per request:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server/context.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;createContext&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;opts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; req&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; session&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;user &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    db&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;user&lt;/code&gt; is nullable, because most requests are anonymous. Middleware is where that gets fixed:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server/trpc.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; authedProcedure &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; publicProcedure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'UNAUTHORIZED'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    ctx&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; adminProcedure &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; authedProcedure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;role &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'admin'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'FORBIDDEN'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now build the router out of those instead of &lt;code&gt;publicProcedure&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; usersRouter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  list&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; publicProcedure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  create&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; authedProcedure
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; adminProcedure
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
      ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the bit I actually like. Inside &lt;code&gt;authedProcedure&lt;/code&gt;, &lt;code&gt;ctx.user&lt;/code&gt; is typed non-null, because &lt;code&gt;opts.next({ ctx })&lt;/code&gt; narrows the context for everything downstream. Inside &lt;code&gt;adminProcedure&lt;/code&gt; you can reach &lt;code&gt;ctx.user.role&lt;/code&gt; without a single optional chain. The authorisation check and the type narrowing are the same line of code, which means you cannot skip the check and still compile. That is a much stronger guarantee than a linting rule about calling &lt;code&gt;requireAuth()&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;Middleware also does the boring cross-cutting work:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; loggedProcedure &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; publicProcedure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; start &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;opts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;path&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; completed in &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; start&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;when-things-go-wrong&quot;&gt;When things go wrong&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TRPCError&lt;/code&gt; takes a code from a fixed list, each mapped to an HTTP status:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; TRPCError &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'NOT_FOUND'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Post not found'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 404&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'BAD_REQUEST'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Invalid input'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 400&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'FORBIDDEN'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Access denied'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 403&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TRPCError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'CONFLICT'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Slug already taken'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 409&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An input that fails Zod validation becomes a &lt;code&gt;BAD_REQUEST&lt;/code&gt; on its own; you do not need to catch and rethrow. On the client the error arrives through React Query’s normal channel:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trpc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;byId&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;queryOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// error.data?.code is 'NOT_FOUND', 'UNAUTHORIZED', …&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Error: &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for everything you would rather see in your logs than in a user’s browser, the handler takes an &lt;code&gt;onError&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;fetchRequestHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  endpoint&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/api/trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  router&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; appRouter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;tRPC error on &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;path&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing worth knowing: in production tRPC strips the stack trace out of &lt;code&gt;INTERNAL_SERVER_ERROR&lt;/code&gt; responses and replaces the message. That is the right default, and it does mean &lt;code&gt;onError&lt;/code&gt; is the only place you will ever see the real cause.&lt;/p&gt;
&lt;h3 id=&quot;prefetching-from-a-server-component&quot;&gt;Prefetching from a Server Component&lt;/h3&gt;
&lt;p&gt;Client components fetching on mount produce the waterfall everyone complains about: HTML arrives, React hydrates, &lt;em&gt;then&lt;/em&gt; the request goes out. You can hoist the fetch into the Server Component that renders them.&lt;/p&gt;
&lt;p&gt;The server-side proxy needs to be told how to build a context, which router to call, and where to put the results:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/trpc-server.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'server-only'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cache &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createTRPCOptionsProxy &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/tanstack-react-query'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; appRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/routers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/context'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; makeQueryClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./query-client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getQueryClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;makeQueryClient&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; trpc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createTRPCOptionsProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ctx&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; createContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  router&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; appRouter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  queryClient&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; getQueryClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cache()&lt;/code&gt; is not optional decoration. Without it you get a fresh &lt;code&gt;QueryClient&lt;/code&gt; per call and nothing you prefetch ends up in the one that gets dehydrated.&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/users/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; dehydrate&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; HydrationBoundary &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@tanstack/react-query'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; trpc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; getQueryClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/trpc-server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; UserList &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./user-list'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;UsersPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; queryClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getQueryClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; queryClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prefetchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trpc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;list&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;queryOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HydrationBoundary&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dehydrate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queryClient&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;UserList&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HydrationBoundary&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the proxy calls the router directly rather than over HTTP, there is no network hop at all on the server. The data is serialised into the HTML, and &lt;code&gt;UserList&lt;/code&gt; hydrates with &lt;code&gt;isPending&lt;/code&gt; already false. The &lt;code&gt;void&lt;/code&gt; is deliberate: not awaiting means the page can stream while the query resolves.&lt;/p&gt;
&lt;h3 id=&quot;batching-streaming-and-subscriptions&quot;&gt;Batching, streaming and subscriptions&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;httpBatchLink&lt;/code&gt; collects every tRPC call that happens in the same tick and sends them as one request. Render six components that each want their own query and you get one round trip instead of six. It is on by default in most setups and it quietly removes a whole category of performance problem.&lt;/p&gt;
&lt;p&gt;Its downside is head-of-line blocking: the batch resolves when the slowest procedure in it resolves. &lt;code&gt;httpBatchStreamLink&lt;/code&gt; fixes that by streaming each result as it lands.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createTRPCClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; httpBatchStreamLink &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createTRPCClient&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;AppRouter&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  links&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;httpBatchStreamLink&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'/api/trpc'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;v11 also added subscriptions over Server-Sent Events, which is a big deal for anyone who was avoiding real-time features because they did not want a WebSocket server to operate. SSE is a plain HTTP response that stays open. Your existing route handler, your existing deployment, no second process.&lt;/p&gt;
&lt;p&gt;And if your router tree has grown large enough to hurt cold starts, &lt;code&gt;lazy&lt;/code&gt; defers loading a subtree until something calls into it:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lazy &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@trpc/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; router &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./trpc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; appRouter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  users&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'./routers/users'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  reports&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'./routers/reports'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reportsRouter&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The types stay exactly as they were. Only the loading is deferred.&lt;/p&gt;
&lt;h3 id=&quot;validating-what-goes-out&quot;&gt;Validating what goes out&lt;/h3&gt;
&lt;p&gt;Input validation gets all the attention. Output validation is the one that saved me from a bad afternoon.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; userOutput &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; usersRouter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  byId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; publicProcedure
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userOutput&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findUnique&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add a &lt;code&gt;passwordHash&lt;/code&gt; column to the users table, forget that this procedure does a bare &lt;code&gt;findUnique&lt;/code&gt;, and without &lt;code&gt;.output()&lt;/code&gt; you have just published password hashes to every client. With it, the response fails validation and tRPC returns an &lt;code&gt;INTERNAL_SERVER_ERROR&lt;/code&gt; instead: loudly, in your logs, rather than silently in the payload. It costs a little runtime and it is worth it on anything touching user records.&lt;/p&gt;
&lt;h3 id=&quot;testing-without-a-server&quot;&gt;Testing without a server&lt;/h3&gt;
&lt;p&gt;Procedures are functions that take a context and an input, so you can call them directly. No listening port, no supertest, no fixture app.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; appRouter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/server/routers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'users router'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'creates a user'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; caller &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; appRouter&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createCaller&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'1'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Test'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'admin'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      db&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; mockDb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; caller&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'New User'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'test@example.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'New User'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'refuses an anonymous delete'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; caller &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; appRouter&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createCaller&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; db&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; mockDb &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;caller&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'1'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rejects&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toThrow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;'UNAUTHORIZED'&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Middleware runs, so that second test genuinely exercises the auth path rather than mocking it away. There is also a &lt;code&gt;createCallerFactory&lt;/code&gt; helper if you would rather build the caller once and reuse it across a suite.&lt;/p&gt;
&lt;h3 id=&quot;where-it-doesnt-fit&quot;&gt;Where it doesn’t fit&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Where it doesn’t fit&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;tRPC&lt;/th&gt;
&lt;th&gt;REST&lt;/th&gt;
&lt;th&gt;GraphQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inferred, no config&lt;/td&gt;
&lt;td&gt;Manual, or OpenAPI codegen&lt;/td&gt;
&lt;td&gt;Schema plus codegen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Schema artefact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;OpenAPI (optional)&lt;/td&gt;
&lt;td&gt;SDL (required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Usually required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Non-TS clients&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full-stack TS apps&lt;/td&gt;
&lt;td&gt;Public and partner APIs&lt;/td&gt;
&lt;td&gt;Large data graphs, many clients&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The honest summary is that tRPC’s greatest strength and its only real weakness are the same property. Skipping the schema is what buys you the instant feedback, and it is also why nobody outside your TypeScript codebase can talk to the thing. If there is any chance of a public API later, either put REST in front of the same service layer, or accept that you will be writing an adapter.&lt;/p&gt;
&lt;h3 id=&quot;what-id-watch-for&quot;&gt;What I’d watch for&lt;/h3&gt;
&lt;p&gt;Two things bother me, and neither is a reason not to use it.&lt;/p&gt;
&lt;p&gt;The first is compile speed. Inference this deep is not free, and a router with a few hundred procedures will make &lt;code&gt;tsc&lt;/code&gt; and your editor think about it. Splitting into sub-routers and keeping return types simple helps.&lt;/p&gt;
&lt;p&gt;The second is that it encourages you to treat the API as an implementation detail, because for a while it is. Then someone wants a mobile app, and the “API” turns out to be four hundred procedures with no versioning, no documented contract, and Zod schemas as the only description of what anything accepts. Keep the business logic in plain functions and let procedures be thin wrappers over them. Future you will need those functions.&lt;/p&gt;
&lt;p&gt;Next on my list is pointing tRPC at Server Actions rather than a route handler, since a lot of what I use mutations for could be a form post. I suspect the answer is “pick one”, but I would like to find out properly first.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Biome]]></title><description><![CDATA[Biome replaces ESLint and Prettier with a single Rust binary. What v2 added, how the automated migration works, and the file types it still refuses to touch.]]></description><link>https://angelo.dini.dev/blog/biome/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/biome/</guid><pubDate>Sat, 23 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;one-binary-instead-of-six-packages&quot;&gt;One binary instead of six packages&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Open the devDependencies of any frontend project older than a year and count how many of them exist purely to make the linter and the formatter stop arguing. On this site the answer is six: eslint, prettier, eslint-config-prettier, eslint-plugin-prettier, stylelint and stylelint-prettier, plus a handful of plugins underneath. None of that is doing work I asked for. It is doing the work of reconciling two tools that both have opinions about where a line break goes.&lt;/p&gt;&lt;p&gt;Biome is the attempt to remove that entirely. One binary, written in Rust, that formats, lints and sorts imports, configured by one file. Version 2 landed in June 2025 and is the release where I stopped filing it under “interesting, ask again next year”.&lt;/p&gt;
&lt;h3 id=&quot;where-it-came-from&quot;&gt;Where it came from&lt;/h3&gt;
&lt;p&gt;Biome is what is left of Rome, the toolchain Sebastian McKenzie started after Babel and Yarn. Rome was going to replace everything in the JavaScript build pipeline at once. It was written in TypeScript, then rewritten in Rust for speed, and backed by a company.&lt;/p&gt;
&lt;p&gt;The company failed. When the layoffs came, the core contributors found they had been locked out of the infrastructure they had been maintaining: no npm registry access, no Discord, no website. In August 2023 they forked it and shipped under a new name, made by fusing “bis” onto “Rome”. Second Rome.&lt;/p&gt;
&lt;p&gt;I have a mild fondness for that origin story. The project people remember is the venture-funded one that stopped; the one that is still shipping releases is the fork the maintainers made when they had no infrastructure and no salary.&lt;/p&gt;
&lt;h3 id=&quot;the-tax-it-is-removing&quot;&gt;The tax it is removing&lt;/h3&gt;
&lt;p&gt;The ESLint and Prettier pairing works, and I have run it for years. The friction is not in any one piece:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Two configuration files, and a third package whose only job is switching off the ESLint rules that fight Prettier.&lt;/li&gt;
&lt;li&gt;Two tools with overlapping jurisdiction, which is why that third package needs to exist.&lt;/li&gt;
&lt;li&gt;JavaScript performance, on a workload that is entirely parsing and walking trees.&lt;/li&gt;
&lt;li&gt;Plugin arithmetic. A typical React setup pulls in &lt;code&gt;@typescript-eslint/parser&lt;/code&gt;, &lt;code&gt;@typescript-eslint/eslint-plugin&lt;/code&gt;, &lt;code&gt;eslint-plugin-react&lt;/code&gt;, &lt;code&gt;eslint-plugin-import&lt;/code&gt; and &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt;, each with its own peer dependency range, all of which have to agree before anything runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one is the one that actually costs time. Nobody minds a slow linter as much as they mind a major ESLint release forcing a coordinated bump across five plugins that update on five different schedules. It is the same complaint I ended up with when writing about &lt;a href=&quot;/blog/conventional-commits/&quot;&gt;Conventional Commits&lt;/a&gt;: the tooling around the convention kept demanding more attention than the convention itself.&lt;/p&gt;
&lt;h3 id=&quot;speed&quot;&gt;Speed&lt;/h3&gt;
&lt;p&gt;Biome’s own benchmark is formatting 171,127 lines across 2,104 files on an Intel Core i7 1270P, where it comes out roughly 35 times faster than Prettier. Take the multiplier with the usual pinch of salt, since it is the vendor’s own number on the vendor’s own hardware, but the order of magnitude is not in dispute.&lt;/p&gt;
&lt;p&gt;Where you feel it is not the full run. It is the pre-commit hook and the editor. A formatter that takes 800ms to start up before it does anything is a formatter you eventually configure to run less often, and then you stop trusting the repo to be formatted. Biome keeps a daemon alive for editor requests, so there is no per-invocation startup at all.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-in&quot;&gt;Getting it in&lt;/h3&gt;
&lt;p&gt;Install it pinned, because formatter output does drift across versions and an unpinned formatter turns one person’s save into a hundred-line diff:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev --save-exact @biomejs/biome&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then generate a config:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @biomejs/biome init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three commands cover daily use:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Format files&lt;/span&gt;
npx biome &lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Lint files&lt;/span&gt;
npx biome lint &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Format, lint, and apply assist actions in one pass&lt;/span&gt;
npx biome check &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;check&lt;/code&gt; is the one that ends up in your scripts. It runs the formatter, applies the safe lint fixes, and runs the assist actions, which in v2 is where import sorting now lives.&lt;/p&gt;
&lt;h3 id=&quot;the-config-file&quot;&gt;The config file&lt;/h3&gt;
&lt;p&gt;Everything goes in &lt;code&gt;biome.json&lt;/code&gt;, or &lt;code&gt;biome.jsonc&lt;/code&gt; if you want comments. A version tuned for a React and TypeScript project:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://biomejs.dev/schemas/2.0.0/schema.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;formatter&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;indentStyle&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;space&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;indentWidth&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lineWidth&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;linter&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;rules&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;recommended&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;complexity&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;noUselessFragments&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;warn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;noForEach&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;off&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;style&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;noNonNullAssertion&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;warn&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;suspicious&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;noExplicitAny&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;warn&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;assist&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;actions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;organizeImports&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;on&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;javascript&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;formatter&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;quoteStyle&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;single&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;semicolons&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;always&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;trailingCommas&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;all&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;vcs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;clientKind&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;git&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;useIgnoreFile&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;defaultBranch&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;main&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;files&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;includes&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tests/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No parser configuration anywhere. Biome works out that a &lt;code&gt;.tsx&lt;/code&gt; file is TypeScript with JSX without being told, which after years of &lt;code&gt;parserOptions&lt;/code&gt; blocks feels like something is missing.&lt;/p&gt;
&lt;p&gt;Two things moved in v2 and will bite you if you are upgrading from v1 rather than starting fresh. &lt;code&gt;files.include&lt;/code&gt; and &lt;code&gt;files.ignore&lt;/code&gt; were replaced by a single &lt;code&gt;files.includes&lt;/code&gt; list that takes negated globs with &lt;code&gt;!&lt;/code&gt;. And &lt;code&gt;organizeImports&lt;/code&gt; is no longer a top-level key; it is an assist action, as above. Running &lt;code&gt;npx @biomejs/biome migrate --write&lt;/code&gt; after a version bump rewrites the config for you, which is a nice habit to have even when nothing has changed.&lt;/p&gt;
&lt;h3 id=&quot;the-formatter&quot;&gt;The formatter&lt;/h3&gt;
&lt;p&gt;Biome formats JavaScript, TypeScript, JSX, TSX, JSON, JSONC, CSS and GraphQL. It passes 97% of Prettier’s test suite, which is how it won the Prettier Challenge back in 2023.&lt;/p&gt;
&lt;p&gt;The 3% is worth knowing before you run it on a repository full of other people’s code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default indent style is tabs, where Prettier defaults to spaces. Set &lt;code&gt;&amp;quot;indentStyle&amp;quot;: &amp;quot;space&amp;quot;&lt;/code&gt; if you would rather not explain the diff.&lt;/li&gt;
&lt;li&gt;Object property unquoting follows ES2015 identifier rules rather than Prettier’s ES5 ones, so a few more keys lose their quotes.&lt;/li&gt;
&lt;li&gt;The parser is stricter. Things Prettier’s Babel parser waves through, like duplicate modifiers, are errors here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Expect one enormous reformatting commit and then nothing. Put its hash in &lt;code&gt;.git-blame-ignore-revs&lt;/code&gt; and move on.&lt;/p&gt;
&lt;p&gt;Opting out of formatting for a block uses a comment:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// biome-ignore format: complex template literal alignment&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; query &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  SELECT * FROM users
  WHERE  id = &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;userId&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  AND    active = true
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-linter&quot;&gt;The linter&lt;/h3&gt;
&lt;p&gt;Rules are grouped into eight domains: accessibility, complexity, correctness, nursery, performance, security, style and suspicious. Nursery is opt-in and holds whatever is still settling.&lt;/p&gt;
&lt;p&gt;Most rules are ports of existing ones from ESLint core, &lt;code&gt;typescript-eslint&lt;/code&gt;, &lt;code&gt;eslint-plugin-react&lt;/code&gt;, &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; and &lt;code&gt;eslint-plugin-unicorn&lt;/code&gt;. The naming convention changed, though: Biome uses camelCase where ESLint uses kebab-case, and often renames on the way, so &lt;code&gt;no-unused-vars&lt;/code&gt; arrives as &lt;code&gt;noUnusedVariables&lt;/code&gt;. Any suppression comment you have memorised is wrong here, and so is any rule name in your team’s coding guidelines.&lt;/p&gt;
&lt;h4 id=&quot;type-aware-rules-without-the-typescript-compiler&quot;&gt;Type-aware rules without the TypeScript compiler&lt;/h4&gt;
&lt;p&gt;This is the headline of v2 and the reason it is worth a second look. &lt;code&gt;typescript-eslint&lt;/code&gt;’s type-aware rules are the slowest part of most lint runs, because they need a full TypeScript program in memory. Biome v2 does its own type inference across files instead, so rules like &lt;code&gt;noFloatingPromises&lt;/code&gt; work with no &lt;code&gt;tsconfig.json&lt;/code&gt; wiring at all:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Biome catches this without the TypeScript compiler&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/data'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Error: noFloatingPromises - This promise must be awaited or handled&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;fetchData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The coverage is partial. Biome puts it at roughly three quarters of what &lt;code&gt;typescript-eslint&lt;/code&gt; catches, which is the honest way to describe it and also the thing to check before you delete anything. If your team relies on a specific type-aware rule, look it up rather than assuming.&lt;/p&gt;
&lt;h4 id=&quot;safe-and-unsafe-fixes&quot;&gt;Safe and unsafe fixes&lt;/h4&gt;
&lt;p&gt;Every fix is classified:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Apply safe fixes only (semantics-preserving)&lt;/span&gt;
npx biome lint &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Apply safe AND unsafe fixes (may change behavior)&lt;/span&gt;
npx biome lint &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--unsafe&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A safe fix cannot change what your program does: dropping an unused import, collapsing a redundant expression. An unsafe fix might, and rewriting a &lt;code&gt;for&lt;/code&gt; loop into &lt;code&gt;Array.map&lt;/code&gt; is the example that makes the distinction obvious. Having the two separated by a flag rather than by rule documentation is the difference between running the fixer in CI and not.&lt;/p&gt;
&lt;h3 id=&quot;imports&quot;&gt;Imports&lt;/h3&gt;
&lt;p&gt;Import organising was rebuilt in v2. It merges duplicate imports from the same module, sorts the specifiers inside each one, understands import attributes, and handles exports as well:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After (biome check --write)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also respects blank lines you put in deliberately, treating them as group boundaries rather than noise to be tidied away. That single behaviour removes the reason most people reach for &lt;code&gt;eslint-plugin-import&lt;/code&gt; or a Prettier sorting plugin in the first place.&lt;/p&gt;
&lt;h3 id=&quot;migrating&quot;&gt;Migrating&lt;/h3&gt;
&lt;p&gt;Biome reads your existing configuration and converts it:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Migrate ESLint config&lt;/span&gt;
npx @biomejs/biome migrate eslint &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Migrate Prettier config&lt;/span&gt;
npx @biomejs/biome migrate prettier &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ESLint side handles both the legacy &lt;code&gt;.eslintrc&lt;/code&gt; format and flat config, resolves shared configs and plugins, and maps rules onto their Biome equivalents where they exist. Rules with no equivalent are dropped, so read the output rather than trusting it.&lt;/p&gt;
&lt;p&gt;If the result is a wall of violations, you do not have to fix them before adopting the tool. Write suppression comments for everything currently failing and work through them later:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx biome lint &lt;span class=&quot;token parameter variable&quot;&gt;--write&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--suppress&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--reason&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;suppressed during Biome migration&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the uninstall, which is the satisfying part:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; uninstall eslint prettier eslint-config-prettier eslint-plugin-react &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  @typescript-eslint/parser @typescript-eslint/eslint-plugin &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  eslint-plugin-import eslint-plugin-jsx-a11y&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eight packages, and every one of them was a version constraint you had to keep satisfied.&lt;/p&gt;
&lt;h3 id=&quot;editors-and-ci&quot;&gt;Editors and CI&lt;/h3&gt;
&lt;p&gt;First-party extensions exist for VS Code, IntelliJ and Zed, with community ones for Vim, Neovim and Sublime Text. In VS Code the settings are:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;editor.defaultFormatter&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;biomejs.biome&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;editor.formatOnSave&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;editor.codeActionsOnSave&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;quickfix.biome&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;explicit&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;source.organizeImports.biome&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;explicit&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For CI there is a purpose-built command that checks everything and fixes nothing:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx biome ci &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It reports formatting, lint and assist violations and exits non-zero. On a pull request you rarely want the whole repository, and Biome knows about your VCS:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Only check files changed since the default branch&lt;/span&gt;
npx biome check &lt;span class=&quot;token parameter variable&quot;&gt;--changed&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Only check staged files, for a pre-commit hook&lt;/span&gt;
npx biome check &lt;span class=&quot;token parameter variable&quot;&gt;--staged&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which makes for a short workflow file:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Code Quality
&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pull_request&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;biome&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;node@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm ci
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npx biome ci .&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-it-will-not-touch&quot;&gt;What it will not touch&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: What it will not touch&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Lint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSX / TSX&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON / JSONC&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphQL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Nursery rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;Experimental&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The absences matter more than the table suggests. SCSS, Sass and Less are not supported, only plain CSS. Vue, Svelte and Astro single-file components are on the roadmap and not here yet. If your project contains either of those, Biome is an addition to your toolchain rather than a replacement for it, and the whole argument for adopting it gets weaker.&lt;/p&gt;
&lt;h3 id=&quot;what-v2-brought&quot;&gt;What v2 brought&lt;/h3&gt;
&lt;p&gt;Beyond type-aware linting, the June release added a plugin system built on GritQL for writing your own pattern-matching rules, though the distribution story for sharing them is still being worked out. Assist actions arrived as a category separate from lint diagnostics, covering things like &lt;code&gt;useSortedKeys&lt;/code&gt; and &lt;code&gt;useSortedAttributes&lt;/code&gt; that are preferences rather than problems. Suppressions gained a range form with &lt;code&gt;// biome-ignore-start&lt;/code&gt; and &lt;code&gt;// biome-ignore-end&lt;/code&gt;, which spares you a comment per line when you need to exempt a block. Monorepos got nested configuration files that inherit from the root. And the HTML formatter appeared, experimental and off by default.&lt;/p&gt;
&lt;h3 id=&quot;would-i-switch-this-site&quot;&gt;Would I switch this site?&lt;/h3&gt;
&lt;p&gt;No, and the reason is the third row from the bottom of that table.&lt;/p&gt;
&lt;p&gt;This blog is Gatsby with Bootstrap and a stack of hand-written SCSS, linted by Stylelint with a handful of plugins on top of it. Biome does not do SCSS. So switching would mean replacing ESLint and Prettier with Biome, keeping Stylelint anyway, and ending up with two tools and two config files, which is precisely the thing I would be switching to avoid. The dependency count barely moves.&lt;/p&gt;
&lt;p&gt;For a project that is TypeScript and plain CSS, I would use it without hesitating, and I would do it on day one rather than migrating later. That is not really a caveat about Biome, it is a caveat about my own repositories, most of which predate the assumption that CSS is something you write directly.&lt;/p&gt;
&lt;p&gt;The thing I will actually do is smaller. The next time an ESLint major forces me to bump five plugins in lockstep, I am going to open that language support table again and check whether the SCSS row has moved&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[React Compiler]]></title><description><![CDATA[How the React Compiler memoises your components at build time, what the Rules of React have to do with it, and how to adopt it one directory at a time.]]></description><link>https://angelo.dini.dev/blog/react-compiler/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-compiler/</guid><pubDate>Sat, 26 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;memoisation-you-no-longer-write-by-hand&quot;&gt;Memoisation you no longer write by hand&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Two years ago I wrote a post about how useCallback does nothing in most of the places people put it. The React Compiler is the React team's answer to the same problem, arrived at from the other direction: instead of teaching everyone when memoisation pays off, take the decision away from us and let a build step make it, every time, everywhere.&lt;/p&gt;&lt;p&gt;It has been called React Forget, then an experimental release, then a beta, and since April it has been a release candidate. It is not finished, but it is well past the stage where trying it is an adventure. Meta runs it in production on Facebook, Instagram, Threads and the Quest Store, and the version you can install today is close to what will ship as 1.0.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-it-is-solving&quot;&gt;The problem it is solving&lt;/h3&gt;
&lt;p&gt;React’s model is that a state change re-renders a component and everything below it. That simplicity is why React is pleasant to work with and also why it does redundant work: a child whose props are identical still re-renders, and rebuilds its own children while it is at it.&lt;/p&gt;
&lt;p&gt;The manual fix has been three APIs. &lt;code&gt;useMemo&lt;/code&gt; caches a computed value, &lt;code&gt;useCallback&lt;/code&gt; keeps a function reference stable between renders, and &lt;code&gt;React.memo&lt;/code&gt; skips a re-render when props compare equal. They work. They are also a tax that gets paid on every component you write, in attention rather than milliseconds. You have to decide what to wrap, keep the dependency arrays honest, and remember that memoising the callback accomplishes nothing unless the component receiving it is also memoised. I went through those trade-offs in detail in &lt;a href=&quot;/blog/react-use-callback/&quot;&gt;Optimising React Components&lt;/a&gt; and the conclusion was uncomfortable: most &lt;code&gt;useCallback&lt;/code&gt; calls in most codebases are decoration.&lt;/p&gt;
&lt;p&gt;Meta measured this before building the compiler. Only about 8% of their React pull requests used manual memoisation at all, and those pull requests took 31 to 46% longer to author. That is a real cost for an optimisation most people get partly wrong. A missing dependency in &lt;code&gt;useMemo&lt;/code&gt; produces stale data, which surfaces as a bug that reproduces once a week. Too much memoisation burns memory and cache-comparison time to avoid work that was cheap anyway.&lt;/p&gt;
&lt;h3 id=&quot;what-it-actually-does&quot;&gt;What it actually does&lt;/h3&gt;
&lt;p&gt;The compiler is a Babel plugin. It reads your source before anything else has touched it, converts each component into an internal representation built around a control flow graph, works out which values depend on which, and emits the memoisation you would have written if you had infinite patience.&lt;/p&gt;
&lt;p&gt;Take a component with nothing interesting in it:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Your source code&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Greeting&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Hello, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is roughly what comes out the other side:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Compiled output (simplified)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; c &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; _c &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react/compiler-runtime&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Greeting&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; $ &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;_c&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; t0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;$&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Hello, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    t0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    $&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    $&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; t0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    t0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; t0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$&lt;/code&gt; is a cache slot array sized to the number of values worth caching. First render fills it, subsequent renders compare and reuse. It is the same idea as &lt;code&gt;useMemo&lt;/code&gt;, applied to every intermediate value and every JSX element rather than the two or three you happened to worry about.&lt;/p&gt;
&lt;p&gt;The interesting part is what it can do that you cannot. Hooks must be called unconditionally at the top level, so anything after an early return is off-limits to &lt;code&gt;useMemo&lt;/code&gt; by construction. The compiler has no such restriction, because the cache is an array it indexes rather than a hook call it has to keep in order. It also tracks mutation across a function body properly, which is the bit people get wrong when they hand-write dependency arrays.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-running&quot;&gt;Getting it running&lt;/h3&gt;
&lt;p&gt;Install the plugin, pinned:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev --save-exact babel-plugin-react-compiler@rc&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--save-exact&lt;/code&gt; is not paranoia. The memoisation the compiler emits changes between versions, and a floating range means two developers on the same branch can produce different output from identical source.&lt;/p&gt;
&lt;p&gt;For Next.js the framework does the wiring:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// next.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nextConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;experimental&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;reactCompiler&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nextConfig&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You still need &lt;code&gt;babel-plugin-react-compiler&lt;/code&gt; installed; the flag only tells Next.js to use it. On recent versions Next.js runs an SWC pass first to work out which files contain components at all, so Babel only touches the files that need it. That matters more than it sounds, because adding a Babel step to an otherwise SWC-based build is the single biggest cost of adoption.&lt;/p&gt;
&lt;p&gt;For Vite, hang it off the React plugin’s Babel config:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vite.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; react &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@vitejs/plugin-react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;babel&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'babel-plugin-react-compiler'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for anything else with a Babel config, the plugin goes first in the list. Not somewhere in the list. First:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// babel.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'babel-plugin-react-compiler'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// must be first&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ... other plugins&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason is that the compiler needs to see your code as you wrote it. Once another transform has rewritten your JSX or shuffled your arrow functions, the analysis it relies on stops being trustworthy, and rather than guess it will bail out and leave the component alone.&lt;/p&gt;
&lt;h3 id=&quot;if-you-are-not-on-react-19&quot;&gt;If you are not on React 19&lt;/h3&gt;
&lt;p&gt;This is the part I care about most, because plenty of code out there is not on 19 and will not be soon. This blog, for instance, still runs Gatsby 4 on React 17, and Gatsby 4 is not moving.&lt;/p&gt;
&lt;p&gt;The compiler targets React 19 by default, but it will compile for 17 and 18 if you give it the runtime polyfill and tell it what to aim at:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; react-compiler-runtime@rc&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// babel.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'babel-plugin-react-compiler'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'18'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The polyfill supplies the cache primitive that React 19 has built in. It is not a compatibility hack bolted on late; backwards compatibility was one of the things the release candidate specifically added, which suggests the team understands that “upgrade to 19 first” is not advice most teams can act on this quarter.&lt;/p&gt;
&lt;h3 id=&quot;the-rules-it-depends-on&quot;&gt;The rules it depends on&lt;/h3&gt;
&lt;p&gt;The compiler is only correct if your components follow the Rules of React. These are not new rules and the compiler did not invent them. What is new is that breaking them used to produce a subtle bug and now produces a subtle bug that is harder to find, because the compiler made a caching decision based on a promise your code did not keep.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Components must be pure.&lt;/strong&gt; Same input, same output, no side effects on the render path.&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Bad: side effect during render&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Counter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; count &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Count: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;count&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// side effect in render!&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Good: side effect in useEffect&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Counter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; count &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Count: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;count&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Props and state are immutable.&lt;/strong&gt; The compiler decides a cached value is still good by comparing references. Mutate the thing behind the reference and the comparison lies to it:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Bad: mutating state directly&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;TodoList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; todos &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; todos&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Todo&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sorted &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; todos&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;localeCompare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// mutates!&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;sorted&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Good: create a new array&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;TodoList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; todos &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; todos&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Todo&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sorted &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;todos&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;localeCompare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;sorted&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Array.prototype.sort&lt;/code&gt; sorting in place is the classic one. &lt;code&gt;reverse&lt;/code&gt; and &lt;code&gt;splice&lt;/code&gt; are the same trap wearing different hats.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hooks stay at the top level.&lt;/strong&gt; Unchanged, but now enforced by something that will act on the assumption.&lt;/p&gt;
&lt;h3 id=&quot;the-lint-rule-is-the-actual-advice&quot;&gt;The lint rule is the actual advice&lt;/h3&gt;
&lt;p&gt;This is the bit I would do first, and it does not require installing the compiler at all. The compiler’s static analysis has been packaged as a lint rule inside &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev eslint-plugin-react-hooks@rc&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// eslint.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; reactHooks &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'eslint-plugin-react-hooks'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  reactHooks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;configs&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'recommended-latest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;'react-hooks/react-compiler'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'error'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule is not on by default in the release candidate, so turn it on explicitly. What it reports is the set of places where the compiler would have to give up: state set during render, refs read where they should not be, mutation of something that came in as a prop. Every one of those is a latent bug today, compiler or no compiler. Fixing them is worth doing on its own terms, and it happens to leave your codebase ready.&lt;/p&gt;
&lt;h3 id=&quot;adopting-it-in-pieces&quot;&gt;Adopting it in pieces&lt;/h3&gt;
&lt;p&gt;Nobody sensible turns this on across a large repository in one commit. There are two supported ways to go slowly.&lt;/p&gt;
&lt;p&gt;Scope it to directories with Babel’s &lt;code&gt;overrides&lt;/code&gt;, then widen:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// babel.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// other plugins...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;overrides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'./src/features/dashboard/**'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'babel-plugin-react-compiler'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or flip it around and compile nothing except what you have marked:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// babel.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'babel-plugin-react-compiler'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;compilationMode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'annotation'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;OptimizedComponent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use memo&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// This component WILL be compiled&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ExpensiveTree&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;RegularComponent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// This component will NOT be compiled&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SimpleTree&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Annotation mode is the one I would start with on an older codebase, because it inverts the risk. Directory mode compiles whatever happens to live in that folder, including the two components someone wrote in a hurry in 2022.&lt;/p&gt;
&lt;p&gt;Going the other way, &lt;code&gt;&amp;quot;use no memo&amp;quot;&lt;/code&gt; excludes a single component:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ProblematicComponent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;use no memo&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// This component will be skipped by the compiler&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Treat that directive as a comment with a deadline attached, not a fix. It marks a component that is breaking a rule, and the rule was already being broken before the compiler noticed.&lt;/p&gt;
&lt;h3 id=&quot;what-meta-got-out-of-it&quot;&gt;What Meta got out of it&lt;/h3&gt;
&lt;p&gt;The published figures come from the Quest Store: up to 12% faster initial loads and cross-page navigations, some interactions more than 2.5x faster, and memory usage flat. Flat memory is the number I find most reassuring, since caching everything is exactly the sort of change that trades one resource for another without telling you.&lt;/p&gt;
&lt;p&gt;The number that says more about adoption risk is a different one: Meta’s monorepo holds over 100,000 React components, and onboarding them needed few code changes. That is the strongest available evidence that ordinary, non-heroic React code is already compatible.&lt;/p&gt;
&lt;h3 id=&quot;when-it-goes-wrong&quot;&gt;When it goes wrong&lt;/h3&gt;
&lt;p&gt;React DevTools puts a sparkle badge next to compiled components in the tree, so you can at least see what you are dealing with. From there the loop is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;&amp;quot;use no memo&amp;quot;&lt;/code&gt; to the component you suspect.&lt;/li&gt;
&lt;li&gt;See whether the symptom disappears.&lt;/li&gt;
&lt;li&gt;If it does, the component is breaking a rule somewhere.&lt;/li&gt;
&lt;li&gt;Run the lint rule and read what it says about that file.&lt;/li&gt;
&lt;li&gt;Fix the cause, then delete the directive.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Nearly every real report I have seen traces back to code that depended on referential &lt;em&gt;inequality&lt;/em&gt; for correctness. An effect that re-runs because a callback is a new object every render, for instance, works entirely by accident. Give it a stable reference and the effect stops firing, and the bug looks like the compiler broke your component when it only revealed what was holding it together.&lt;/p&gt;
&lt;p&gt;You do not have to strip out existing &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; calls first. They keep working. The team’s advice is to leave them where they are, because removing them changes what the compiler chooses to cache, and doing that at the same time as adopting the compiler leaves you with two variables and one bug.&lt;/p&gt;
&lt;h3 id=&quot;where-this-leaves-things&quot;&gt;Where this leaves things&lt;/h3&gt;
&lt;p&gt;Some caveats, none of them dealbreakers. The plugin has to run on original source, which means adding Babel to a build that may have been happily Babel-free. Library code has to be compiled by the library’s own author, so nothing you install benefits until they ship it. The SWC path is still experimental. And a release candidate is a release candidate, however many Meta apps are running it.&lt;/p&gt;
&lt;p&gt;Still, my honest reaction is relief rather than excitement. This is not a new capability, it is the removal of a chore, and specifically a chore that produced more bad advice than good code. Whole blog posts, mine included, exist to explain when &lt;code&gt;useCallback&lt;/code&gt; is worth it. I would rather they stopped needing to exist.&lt;/p&gt;
&lt;p&gt;I will not be putting this near the site you are reading, because Gatsby 4 and React 17 have their own opinions. The lint rule, though, goes on everything I touch from here 🧹&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Tailwind CSS v4]]></title><description><![CDATA[What changed in Tailwind CSS v4: CSS-first configuration, the new engine, container queries, the breaking changes that bite, and whether to migrate yet.]]></description><link>https://angelo.dini.dev/blog/tailwind-css-v4/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/tailwind-css-v4/</guid><pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;configuration-that-lives-in-the-stylesheet&quot;&gt;Configuration that lives in the stylesheet&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Tailwind v4 landed in January 2025 and it is a rewrite, not a refresh. The JavaScript config file is gone from the happy path, the engine underneath is new, and a pile of utilities have been renamed. If you have a v3 project, none of that is optional reading: the upgrade is mechanical in places and genuinely disruptive in others, and the disruptive parts are the ones nobody mentions in the release notes summary.&lt;/p&gt;&lt;p&gt;I have a soft spot for the direction here. Moving the design tokens into CSS custom properties is the same argument I made in &lt;a href=&quot;/blog/css-custom-properties/&quot;&gt;CSS custom properties&lt;/a&gt; back in 2022, only now the framework agrees with me. What I am less thrilled about is what v4 asks you to give up to get there. More on that at the end.&lt;/p&gt;
&lt;h3 id=&quot;the-engine&quot;&gt;The engine&lt;/h3&gt;
&lt;p&gt;The headline is the new engine, written from scratch. The numbers from the Tailwind team’s own benchmarks:&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: The engine&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build type&lt;/th&gt;
&lt;th&gt;v3.4&lt;/th&gt;
&lt;th&gt;v4.0&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Full build&lt;/td&gt;
&lt;td&gt;378ms&lt;/td&gt;
&lt;td&gt;100ms&lt;/td&gt;
&lt;td&gt;3.78x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental rebuild, new CSS&lt;/td&gt;
&lt;td&gt;44ms&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;8.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental rebuild, no new CSS&lt;/td&gt;
&lt;td&gt;35ms&lt;/td&gt;
&lt;td&gt;192µs&lt;/td&gt;
&lt;td&gt;182x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The bottom row is the one that changes how the tool feels. 192 microseconds is below the threshold where you notice anything at all, so an incremental rebuild that produces no new classes is effectively free. Most keystrokes in most sessions fall into that row, because you spend far more time nudging existing classes around than inventing new ones.&lt;/p&gt;
&lt;p&gt;I would treat the first row with the usual scepticism you apply to any benchmark shipped by the people who wrote the thing being benchmarked. It is a real improvement, and your project is not their project.&lt;/p&gt;
&lt;h3 id=&quot;configuration-moved-into-css&quot;&gt;Configuration moved into CSS&lt;/h3&gt;
&lt;p&gt;Here is v3, where the theme lives in a JavaScript module that the build tool has to load:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// tailwind.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'./src/**/*.{html,js,tsx}'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;extend&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;colors&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#ff5733'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;accent&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#3498db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;fontFamily&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Satoshi'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'sans-serif'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;spacing&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'4.5rem'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is v4, where it lives in the stylesheet:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@theme&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--font-display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Satoshi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sans-serif&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color-brand&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #ff5733&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color-accent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #3498db&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing-18&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4.5rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--breakpoint-3xl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1920px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--ease-fluid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cubic-bezier&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0.3&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The naming is not arbitrary. The prefix of each variable tells Tailwind which family of utilities to generate: &lt;code&gt;--color-*&lt;/code&gt; produces &lt;code&gt;bg-brand&lt;/code&gt;, &lt;code&gt;text-brand&lt;/code&gt;, &lt;code&gt;border-brand&lt;/code&gt; and the rest; &lt;code&gt;--breakpoint-*&lt;/code&gt; produces &lt;code&gt;3xl:&lt;/code&gt; variants; &lt;code&gt;--ease-*&lt;/code&gt; produces &lt;code&gt;ease-fluid&lt;/code&gt;. Learn the prefixes and the config stops needing documentation.&lt;/p&gt;
&lt;p&gt;Everything in &lt;code&gt;@theme&lt;/code&gt; is also emitted as a real custom property on &lt;code&gt;:root&lt;/code&gt;. That means a value you defined for Tailwind is available to hand-written CSS, to inline styles, to a charting library that wants a hex string, and to &lt;code&gt;getComputedStyle&lt;/code&gt; in a test. In v3, getting a theme value into JavaScript meant importing the config and running it through &lt;code&gt;resolveConfig&lt;/code&gt;, which pulled the whole config into your bundle if you were careless. Now it is &lt;code&gt;var(--color-brand)&lt;/code&gt; and there is no build step involved.&lt;/p&gt;
&lt;p&gt;If you still need JavaScript for something, the config file is supported. It is no longer discovered automatically, so you point at it:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@config&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../tailwind.config.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plugins load the same way, with &lt;code&gt;@plugin &amp;quot;@tailwindcss/typography&amp;quot;;&lt;/code&gt; in the stylesheet.&lt;/p&gt;
&lt;h3 id=&quot;setting-it-up&quot;&gt;Setting it up&lt;/h3&gt;
&lt;p&gt;Two dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; tailwindcss @tailwindcss/postcss&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One PostCSS plugin:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// postcss.config.mjs&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tailwindcss/postcss&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One import:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* main.css */&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole setup. The three &lt;code&gt;@tailwind base/components/utilities&lt;/code&gt; directives are gone, and so are &lt;code&gt;postcss-import&lt;/code&gt; and &lt;code&gt;autoprefixer&lt;/code&gt; as separate installs, since the Tailwind plugin now handles imports and prefixing itself.&lt;/p&gt;
&lt;p&gt;Content detection is automatic. Tailwind walks your project, skips anything in &lt;code&gt;.gitignore&lt;/code&gt;, and ignores binary files, so the &lt;code&gt;content&lt;/code&gt; array has no successor. When the heuristic guesses wrong, which it does for template files that live outside the project root or inside a linked package, you tell it explicitly:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@source&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../packages/ui/src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Vite, skip PostCSS entirely and use the first-party plugin, which is measurably quicker because it hooks into Vite’s own module graph instead of running as a post-processing step:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; tailwindcss @tailwindcss/vite&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vite.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; tailwindcss &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@tailwindcss/vite'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;tailwindcss&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-v4-takes-from-modern-css&quot;&gt;What v4 takes from modern CSS&lt;/h3&gt;
&lt;p&gt;Tailwind v3 was written for a browser landscape that no longer exists. v4 assumes Safari 16.4, Chrome 111 and Firefox 128 as its floor and spends that budget freely.&lt;/p&gt;
&lt;h4 id=&quot;cascade-layers&quot;&gt;Cascade layers&lt;/h4&gt;
&lt;p&gt;Tailwind’s own output now sits inside real &lt;code&gt;@layer&lt;/code&gt; rules. In v3 the ordering was emulated, and the emulation leaked whenever a third-party stylesheet had opinions about specificity. Now the browser does the ordering, which mostly means you stop losing arguments with a component library’s reset.&lt;/p&gt;
&lt;h4 id=&quot;container-queries-no-plugin&quot;&gt;Container queries, no plugin&lt;/h4&gt;
&lt;p&gt;The container query plugin is now part of the core:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;@container&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;grid grid-cols-1 @sm:grid-cols-3 @lg:grid-cols-4&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Responsive based on container width --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are &lt;code&gt;@max-*&lt;/code&gt; variants too, and you can stack them into a range with &lt;code&gt;@min-md:@max-xl:hidden&lt;/code&gt;. I wrote about why this matters for component-driven layouts in &lt;a href=&quot;/blog/css-container-queries/&quot;&gt;CSS container queries&lt;/a&gt;; the short version is that a card should respond to the column it is in, not to the window.&lt;/p&gt;
&lt;h4 id=&quot;oklch-and-color-mix&quot;&gt;OKLCH and color-mix&lt;/h4&gt;
&lt;p&gt;The default palette moved from sRGB to OKLCH, which gives more saturated colours on wide-gamut displays and, more usefully, makes lightness steps behave consistently across hues. Opacity modifiers are implemented with &lt;code&gt;color-mix()&lt;/code&gt;, so they now work on any colour value at all, including custom properties and &lt;code&gt;currentColor&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-brand/50&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 50% opacity of your custom brand color --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-(--my-custom-color)/75&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Works with any CSS variable too --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;registered-custom-properties&quot;&gt;Registered custom properties&lt;/h4&gt;
&lt;p&gt;Tailwind registers its animatable properties with &lt;code&gt;@property&lt;/code&gt;, which tells the browser their type. A browser that knows &lt;code&gt;--tw-gradient-from&lt;/code&gt; is a colour can interpolate it; a browser that thinks it is an arbitrary string cannot. This is what makes transitions on gradients and individual transform components work.&lt;/p&gt;
&lt;h3 id=&quot;new-utilities-worth-knowing&quot;&gt;New utilities worth knowing&lt;/h3&gt;
&lt;p&gt;3D transforms are built in now:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;perspective-500&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;rotate-x-12 rotate-y-6 transform-3d&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- 3D transformed element --&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The set includes &lt;code&gt;rotate-x-*&lt;/code&gt;, &lt;code&gt;rotate-y-*&lt;/code&gt;, &lt;code&gt;scale-z-*&lt;/code&gt;, &lt;code&gt;translate-z-*&lt;/code&gt;, &lt;code&gt;perspective-*&lt;/code&gt; and &lt;code&gt;perspective-origin-*&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Gradients gained angles, radial and conic variants, and interpolation control:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Arbitrary angle --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-linear-45 from-blue-500 to-purple-500&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Radial gradient (new) --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-radial from-yellow-300 to-orange-500&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Conic gradient (new) --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-conic from-red-500 via-green-500 to-blue-500&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- More vivid gradients via OKLCH interpolation --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-linear-to-r/oklch from-blue-500 to-green-500&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bg-gradient-*&lt;/code&gt; is now &lt;code&gt;bg-linear-*&lt;/code&gt;, which the upgrade tool renames for you.&lt;/p&gt;
&lt;p&gt;Numeric utilities take any value without arbitrary-value brackets, which quietly removes a category of ugly class names:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- v3: needed arbitrary values --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;grid-cols-[15] mt-[4.25rem]&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- v4: just works --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;grid-cols-15 mt-17 w-29&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;not-*&lt;/code&gt; variant negates a pseudo-class, a media query or a &lt;code&gt;@supports&lt;/code&gt; condition:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;opacity-100 not-hover:opacity-75&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  Subtle opacity when not hovered
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;not-supports-[display:grid]:flex&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  Fallback for browsers without grid
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;starting:&lt;/code&gt; exposes &lt;code&gt;@starting-style&lt;/code&gt;, which is how you do enter transitions without a JavaScript library holding a class on for one frame:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;popover&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;update-panel&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;transition-discrete starting:open:opacity-0&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Fades in when opened, no JavaScript needed --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A handful of smaller additions that I keep forgetting exist and then rediscovering: &lt;code&gt;inset-shadow-*&lt;/code&gt; and &lt;code&gt;inset-ring-*&lt;/code&gt; stack up to four shadow layers on one element, &lt;code&gt;field-sizing&lt;/code&gt; makes a textarea grow with its content without a resize observer, &lt;code&gt;color-scheme&lt;/code&gt; fixes the white scrollbar on a dark page, &lt;code&gt;font-stretch&lt;/code&gt; drives variable-font width axes, and the &lt;code&gt;nth-*&lt;/code&gt;, &lt;code&gt;inert&lt;/code&gt; and &lt;code&gt;in-*&lt;/code&gt; variants cover cases that used to need a custom selector.&lt;/p&gt;
&lt;h3 id=&quot;the-breaking-changes-that-will-actually-bite&quot;&gt;The breaking changes that will actually bite&lt;/h3&gt;
&lt;p&gt;The migration tool handles the renames. These are the ones that survive it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The scale shift.&lt;/strong&gt; Shadows, radii and blurs all moved down a step, so a bare &lt;code&gt;shadow&lt;/code&gt; is now &lt;code&gt;shadow-sm&lt;/code&gt; and the old &lt;code&gt;shadow-sm&lt;/code&gt; is &lt;code&gt;shadow-xs&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: The breaking changes that will actually bite&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;v3&lt;/th&gt;
&lt;th&gt;v4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shadow-sm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shadow-xs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shadow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shadow-sm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rounded-sm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rounded-xs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rounded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rounded-sm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blur-sm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blur-xs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blur-sm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The same shift applies to &lt;code&gt;drop-shadow-*&lt;/code&gt; and &lt;code&gt;backdrop-blur-*&lt;/code&gt;. Codemods catch these in class attributes. They do not catch class names assembled at runtime from a variable, which is exactly where they will hide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Defaults that changed.&lt;/strong&gt; Default border colour is &lt;code&gt;currentColor&lt;/code&gt; instead of &lt;code&gt;gray-200&lt;/code&gt;, so every &lt;code&gt;border&lt;/code&gt; with no colour beside it now inherits the text colour, which on a dark heading looks like someone drew on your card with a marker. Default ring width dropped from 3px to 1px and its colour from &lt;code&gt;blue-500&lt;/code&gt; to &lt;code&gt;currentColor&lt;/code&gt;. &lt;code&gt;outline-none&lt;/code&gt; was renamed to &lt;code&gt;outline-hidden&lt;/code&gt; and the name &lt;code&gt;outline-none&lt;/code&gt; was reused for &lt;code&gt;outline-style: none&lt;/code&gt;, so the old class still compiles and no longer does what it did.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hover.&lt;/strong&gt; The &lt;code&gt;hover:&lt;/code&gt; variant only applies where the primary input actually supports hovering. Sticky hover states on touch devices are gone, which is a fix, but if your design relied on a hover style being visible after a tap you will notice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Variant stacking reversed.&lt;/strong&gt; Stacked variants read left to right now, so &lt;code&gt;first:*:pt-0&lt;/code&gt; becomes &lt;code&gt;*:first:pt-0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opacity utilities removed.&lt;/strong&gt; &lt;code&gt;bg-opacity-*&lt;/code&gt;, &lt;code&gt;text-opacity-*&lt;/code&gt; and friends are gone in favour of the slash modifier:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- v3 --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-black bg-opacity-50&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- v4 --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-black/50&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Custom utilities.&lt;/strong&gt; The &lt;code&gt;@layer utilities&lt;/code&gt; pattern is replaced by &lt;code&gt;@utility&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* v3 */&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@layer&lt;/span&gt; utilities&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.custom-scrollbar&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;scrollbar-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; thin&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* v4 */&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@utility&lt;/span&gt; custom-scrollbar&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;scrollbar-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; thin&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Variables in arbitrary values.&lt;/strong&gt; &lt;code&gt;bg-[--brand-color]&lt;/code&gt; became &lt;code&gt;bg-(--brand-color)&lt;/code&gt;, because square brackets now unambiguously mean “a literal value” and parentheses mean “a variable”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;And the big one: no preprocessors.&lt;/strong&gt; Tailwind v4 does not run alongside Sass, Less or Stylus. Tailwind is the preprocessor now, handling imports, nesting and prefixing itself. If your project has a &lt;code&gt;.scss&lt;/code&gt; file with mixins in it, you rewrite those styles before you upgrade, not after.&lt;/p&gt;
&lt;h3 id=&quot;migrating&quot;&gt;Migrating&lt;/h3&gt;
&lt;p&gt;Do it on a branch, because the diff is large and you will want to bisect it:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; tailwind-v4-migration
npx @tailwindcss/upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tool needs Node 20 or later. It converts &lt;code&gt;tailwind.config.js&lt;/code&gt; into &lt;code&gt;@theme&lt;/code&gt;, renames deprecated utilities in your templates, and rewrites your PostCSS or Vite config. Then read the diff properly, and pay attention to four things.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Anything that builds class names at runtime.&lt;/strong&gt; Template literals, lookup objects mapping a prop to a class, &lt;code&gt;clsx&lt;/code&gt; calls with conditional fragments. The codemod sees strings, not intent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dark mode.&lt;/strong&gt; The &lt;code&gt;darkMode: &amp;#39;class&amp;#39;&lt;/code&gt; option has no direct equivalent; you declare it as a variant: &lt;code&gt;@custom-variant dark (&amp;amp;:where(.dark, .dark *));&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;container&lt;/code&gt; utility.&lt;/strong&gt; Its &lt;code&gt;center&lt;/code&gt; and &lt;code&gt;padding&lt;/code&gt; options are gone. Redefine it with &lt;code&gt;@utility container&lt;/code&gt; if you relied on them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spacing between siblings.&lt;/strong&gt; &lt;code&gt;space-y-*&lt;/code&gt; changed its selector from &lt;code&gt;&amp;gt; :not([hidden]) ~ :not([hidden])&lt;/code&gt; to &lt;code&gt;&amp;gt; :not(:last-child)&lt;/code&gt;, and the margin moved from the top of the following element to the bottom of the preceding one. Layouts with a hidden first child will shift.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What the tool cannot do at all: rewrite your Sass, port a plugin that reaches into internals, or update snapshot tests that assert on generated CSS.&lt;/p&gt;
&lt;h3 id=&quot;who-should-wait&quot;&gt;Who should wait&lt;/h3&gt;
&lt;p&gt;New project, no legacy: use v4, there is nothing to weigh up.&lt;/p&gt;
&lt;p&gt;An existing project splits along one line, and it is not size. It is whether you have Sass. A large v4 migration without Sass is a long afternoon of renames and a careful look at the diff. A small project built on Sass mixins is a rewrite of your styling layer wearing a migration’s clothes, and the upgrade tool will not help with any of it.&lt;/p&gt;
&lt;p&gt;The browser floor matters if you have users you cannot choose. Safari 16.4 is March 2023, so this is not aggressive, but “not aggressive” and “acceptable to your client’s compliance team” are different standards.&lt;/p&gt;
&lt;p&gt;Component libraries are mostly fine. shadcn/ui, Headless UI and Radix all support v4. Smaller plugins are the risk, though it is worth correcting a claim I have seen repeated in a few places: the JavaScript plugin API was not removed. &lt;code&gt;@plugin&lt;/code&gt; still loads a v3-style plugin. Plugins that dig into Tailwind’s internal config resolution are the ones that break, and that is a much smaller set.&lt;/p&gt;
&lt;h3 id=&quot;what-im-doing-about-this-site&quot;&gt;What I’m doing about this site&lt;/h3&gt;
&lt;p&gt;Nothing, for now, and for a boring reason. This blog runs on Bootstrap 5 with a pile of hand-written SCSS, compiled by &lt;code&gt;gatsby-plugin-sass&lt;/code&gt;. Tailwind v4 will not sit next to that, and I am not rewriting a working stylesheet to change which utility framework I am not using.&lt;/p&gt;
&lt;p&gt;That is the part of this release I find genuinely awkward. The CSS-first configuration is good. The engine is fast. But “we are your preprocessor now” is a much bigger claim than “we renamed some shadows”, and it is the one that decides whether you can adopt v4 at all. Projects that started on Tailwind will not notice. Projects that grew a Sass layer over five years get to choose between two rewrites, and I suspect a lot of them will simply stay on v3 until something forces the issue 🎨&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Next.js Server Actions]]></title><description><![CDATA[How Server Actions replace API routes for mutations in Next.js, the React form hooks around them, cache revalidation, and the security you still must write.]]></description><link>https://angelo.dini.dev/blog/nextjs-server-actions/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/nextjs-server-actions/</guid><pubDate>Sat, 24 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-endpoint-you-never-write&quot;&gt;The endpoint you never write&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Most React codebases eventually grow a folder of tiny HTTP endpoints whose only job in life is to let one form write one row to a database. Each of them costs a route file, a fetch call, a JSON round trip, and the same data shape declared twice in TypeScript. Server Actions delete that folder. You write the function, you hand it to a form, and Next.js does the plumbing.&lt;/p&gt;&lt;p&gt;They landed as an alpha in Next.js 13.4 and were marked stable in 14, so at this point they are the default answer for mutations in the App Router rather than a thing you try out on a side project. If you read my notes on &lt;a href=&quot;/blog/react-19/&quot;&gt;React 19&lt;/a&gt;, the hooks further down will look familiar. The form hooks and Server Actions were designed together and they only really make sense as a pair.&lt;/p&gt;
&lt;h3 id=&quot;what-a-server-action-is&quot;&gt;What a Server Action is&lt;/h3&gt;
&lt;p&gt;A Server Action is an async function marked with the &lt;code&gt;&amp;#39;use server&amp;#39;&lt;/code&gt; directive. It runs on the server, only on the server, and it can be called from a Server Component or a Client Component without you writing any transport code:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'content'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing in that file ever reaches the browser. What the client gets instead is a reference: an opaque identifier for the function, plus a bit of generated code that POSTs to it. Next.js serialises the arguments, sends them, runs the function, and serialises whatever comes back.&lt;/p&gt;
&lt;p&gt;The second thing worth knowing is that Server Actions are wired into React’s transition machinery. Called from a form’s &lt;code&gt;action&lt;/code&gt; prop, or wrapped in &lt;code&gt;startTransition&lt;/code&gt;, they participate in concurrent rendering. That is what makes pending states and optimistic updates work without any bookkeeping on your side.&lt;/p&gt;
&lt;h3 id=&quot;compared-with-an-api-route&quot;&gt;Compared with an API route&lt;/h3&gt;
&lt;p&gt;The usual way to write a create-post form before all this looked like the following. First the endpoint:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/posts/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the form that talks to it, which has to become a Client Component because it needs an &lt;code&gt;onSubmit&lt;/code&gt; handler:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/posts/new/page.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NewPostPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;FormEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; formData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FormData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; HTMLFormElement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'POST'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'application/json'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'content'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleSubmit&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Create Post&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same feature with a Server Action is two files that barely say anything:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/posts/actions.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidatePath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/cache'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'content'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/posts/new/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createPost &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NewPostPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;createPost&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Create Post&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No fetch, no headers, no JSON, no &lt;code&gt;&amp;#39;use client&amp;#39;&lt;/code&gt;. The page stays a Server Component and the form keeps working with JavaScript disabled, because a &lt;code&gt;&amp;lt;form action={fn}&amp;gt;&lt;/code&gt; degrades to an ordinary browser form post. That last property is the one I keep coming back to. Progressive enhancement used to be something you argued for in a planning meeting and then quietly dropped; here it is the default and you have to work to lose it.&lt;/p&gt;
&lt;h3 id=&quot;where-the-directive-goes&quot;&gt;Where the directive goes&lt;/h3&gt;
&lt;p&gt;There are two placements, and they are not interchangeable in practice.&lt;/p&gt;
&lt;p&gt;The module-level version puts &lt;code&gt;&amp;#39;use server&amp;#39;&lt;/code&gt; at the top of a file and turns every export into an action:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/actions.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/db'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidatePath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/cache'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'content'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;deletePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The inline version declares the action inside a Server Component:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/posts/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;PostsPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleDelete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'id'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleDelete&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;123&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Delete&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I default to the file. It is easier to test, easier to grep for, and it keeps components readable. The inline form has one genuine advantage, though, which is that it closes over the render scope: if you need a snapshot of a value as it was when the page rendered, an inline action captures it for free. That convenience has a cost attached, and I come back to it in the security section.&lt;/p&gt;
&lt;h3 id=&quot;pending-states-with-useformstatus&quot;&gt;Pending states with useFormStatus&lt;/h3&gt;
&lt;p&gt;A submit button that does nothing visible for two seconds is a bug report waiting to happen. &lt;code&gt;useFormStatus&lt;/code&gt; from &lt;code&gt;react-dom&lt;/code&gt; reads the state of the enclosing form:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// components/SubmitButton.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useFormStatus &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SubmitButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; label &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Save'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; label&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pending &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useFormStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pending &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Processing...'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createPost &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; SubmitButton &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/components/SubmitButton'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NewPostPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;createPost&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SubmitButton&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Create Post&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hook has one rule that trips everybody up exactly once: it has to live in a component rendered &lt;em&gt;inside&lt;/em&gt; the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, not in the component that renders the form. It reads from the nearest form context above it, so calling it alongside the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element gives you &lt;code&gt;pending: false&lt;/code&gt; forever, with no warning and no error. That is why the button gets extracted into its own file even when it does nothing else.&lt;/p&gt;
&lt;h3 id=&quot;tracking-the-result-with-useactionstate&quot;&gt;Tracking the result with useActionState&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useFormStatus&lt;/code&gt; tells you that something is happening. It does not tell you how it went. For that there is &lt;code&gt;useActionState&lt;/code&gt;, which lives in &lt;code&gt;react&lt;/code&gt; rather than &lt;code&gt;react-dom&lt;/code&gt; and replaced the older &lt;code&gt;useFormState&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/contact/page.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useActionState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; sendMessage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; initialState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ContactPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; formAction&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isPending&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useActionState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    sendMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    initialState
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;formAction&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;isPending &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Sending...'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Send Message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'text-green'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'text-red'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The action on the other side gains an extra first argument, the previous state:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/contact/actions.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevState&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; email &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'email'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; to&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; message &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Message sent successfully!'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Failed to send message.'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get back the current state, a wrapped action to hand to the form, and an &lt;code&gt;isPending&lt;/code&gt; flag. Two small notes. The &lt;code&gt;prevState: any&lt;/code&gt; in the signature above is what most examples show and it is worth replacing with a real type, because the return type of the action and the initial state have to agree and TypeScript will happily let them drift. And whatever you return crosses the network, so it has to be serialisable. A &lt;code&gt;Date&lt;/code&gt; survives. A Prisma model instance with methods on it does not.&lt;/p&gt;
&lt;h3 id=&quot;optimistic-updates&quot;&gt;Optimistic updates&lt;/h3&gt;
&lt;p&gt;For interactions where waiting for the server feels absurd, &lt;code&gt;useOptimistic&lt;/code&gt; lets you render the outcome you expect and roll it back if the server disagrees:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useOptimistic&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useTransition &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toggleLike &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  liked&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;PostItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; post &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; post&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Post &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startTransition&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;optimisticPost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setOptimisticPost&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useOptimistic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    post&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentPost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newLiked&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;currentPost&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      liked&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; newLiked&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleToggle&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setOptimisticPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;optimisticPost&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;liked&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toggleLike&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;optimisticPost&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleToggle&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;optimisticPost&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;liked &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Unlike'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Like'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;startTransition&lt;/code&gt; wrapper is not decoration. Outside a form’s &lt;code&gt;action&lt;/code&gt; prop, it is the thing that ties the optimistic value to the lifetime of the server round trip. Drop it and the optimistic state reverts the moment the render finishes, which produces a button that flickers back to its old label for a few hundred milliseconds and looks broken. If the action throws, React discards the optimistic value and you are back at &lt;code&gt;post.liked&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;revalidation-and-redirects&quot;&gt;Revalidation and redirects&lt;/h3&gt;
&lt;p&gt;A mutation that does not invalidate its cache is a mutation that appears not to have happened. Next.js gives you two levers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;revalidatePath&lt;/code&gt; throws away everything cached for a URL. &lt;code&gt;revalidateTag&lt;/code&gt; throws away every &lt;code&gt;fetch&lt;/code&gt; that was tagged with a given string:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidatePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; revalidateTag &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/cache'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updatePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'id'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Blow away everything rendered for this path&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Or, more surgically, everything tagged 'posts'&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;revalidateTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tags are the better instrument once an app has more than a handful of pages, since the same data usually appears in several places. To use them you tag the read side as well, with &lt;code&gt;next: { tags: [&amp;#39;posts&amp;#39;] }&lt;/code&gt; on the &lt;code&gt;fetch&lt;/code&gt; call. Paths are the blunt instrument you reach for when you are not yet sure which pages are affected, which in my experience is most of the time on a new project.&lt;/p&gt;
&lt;p&gt;Redirecting after a mutation uses &lt;code&gt;redirect&lt;/code&gt; from &lt;code&gt;next/navigation&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; redirect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/navigation'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/posts/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;redirect&lt;/code&gt; works by throwing an internal &lt;code&gt;NEXT_REDIRECT&lt;/code&gt; error, which means a &lt;code&gt;try/catch&lt;/code&gt; around it will swallow the navigation and leave the user staring at the form they just submitted. Call it after the &lt;code&gt;try/catch&lt;/code&gt;, never inside. If you need both, store the target in a variable in the &lt;code&gt;try&lt;/code&gt; block and redirect once you are out.&lt;/p&gt;
&lt;h3 id=&quot;validating-what-comes-in&quot;&gt;Validating what comes in&lt;/h3&gt;
&lt;p&gt;Client-side validation is a courtesy to the user. It is not a check. Anyone can POST to the action directly, so the action validates or nothing does. &lt;a href=&quot;https://zod.dev/&quot;&gt;Zod&lt;/a&gt; is the usual pairing:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/actions.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zod'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidatePath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/cache'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; PostSchema &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Title is required'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Content must be at least 10 characters'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  category&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'tutorial'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'guide'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'opinion'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevState&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rawData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'content'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    category&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'category'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validated &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; PostSchema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;safeParse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rawData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;validated&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      errors&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; validated&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fieldErrors&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; validated&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; errors&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flatten().fieldErrors&lt;/code&gt; gives you an object keyed by field name with an array of messages under each, which maps straight onto the inputs. Combined with &lt;code&gt;useActionState&lt;/code&gt;, that is a complete server-validated form with per-field errors and no client validation library at all.&lt;/p&gt;
&lt;p&gt;One wrinkle: &lt;code&gt;formData.get()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; for a missing field, and &lt;code&gt;null&lt;/code&gt; is not a string. If a checkbox is unchecked or a select never gets touched, your schema sees &lt;code&gt;null&lt;/code&gt; and produces an error message about the wrong thing. Either give the schema explicit handling for it or normalise before parsing.&lt;/p&gt;
&lt;h3 id=&quot;errors&quot;&gt;Errors&lt;/h3&gt;
&lt;p&gt;Returning errors as data beats throwing them, because a thrown error in an action becomes a generic message in production. Next.js deliberately hides the details, which is the correct behaviour and also mildly infuriating the first time you meet it:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; revalidatePath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/cache'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;processPayment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FormData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; amount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'amount'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; paymentService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/billing'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transactionId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error &lt;span class=&quot;token keyword&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PaymentError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; success&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'An unexpected error occurred.'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anything you do throw lands in the nearest &lt;code&gt;error.tsx&lt;/code&gt; boundary. Be deliberate about what you put in a returned error string: database driver messages and upstream API responses leak schema names, table names and occasionally credentials. Log the real thing on the server, return the sentence the user can act on. I wrote about a related version of this problem in the &lt;a href=&quot;/blog/stripe/&quot;&gt;Stripe&lt;/a&gt; post, where the payment provider’s error codes and the message you want to show a customer are almost never the same text.&lt;/p&gt;
&lt;h3 id=&quot;calling-actions-outside-a-form&quot;&gt;Calling actions outside a form&lt;/h3&gt;
&lt;p&gt;Actions are ordinary functions from the caller’s point of view, so an event handler or an effect can invoke them:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; incrementViewCount &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useTransition &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;PostView&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; postId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; postId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startTransition&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;incrementViewCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;postId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// actions.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;incrementViewCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; postId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; views&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; increment&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worth remembering that every call is a POST. Actions dispatched from the same page are handled one after another rather than in parallel, so a component that fires one per item in a list will queue them up behind each other. Batch on the server instead of looping on the client.&lt;/p&gt;
&lt;h3 id=&quot;what-nextjs-protects-you-from-and-what-it-doesnt&quot;&gt;What Next.js protects you from, and what it doesn’t&lt;/h3&gt;
&lt;p&gt;This is the part I would put first if the post were shorter, because it is the part that gets skipped. An exported Server Action is a public POST endpoint. It does not become one when you attach it to a form; it already is one.&lt;/p&gt;
&lt;p&gt;Next.js does put real protection in the way:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: What Next.js protects you from, and what it doesn’t&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Encrypted action IDs&lt;/td&gt;
&lt;td&gt;The client references actions by a non-deterministic ID, recalculated between builds, not by function name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead code elimination&lt;/td&gt;
&lt;td&gt;An exported action that nothing in the app calls is stripped from the build and never gets a public ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Origin/Host comparison&lt;/td&gt;
&lt;td&gt;Requests whose &lt;code&gt;Origin&lt;/code&gt; header does not match &lt;code&gt;Host&lt;/code&gt; (or &lt;code&gt;X-Forwarded-Host&lt;/code&gt;) are rejected, which handles most CSRF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closure encryption&lt;/td&gt;
&lt;td&gt;Variables an inline action closes over travel to the client and back, so Next.js encrypts them with a per-build key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;That last row is the one to read twice. When you declare an action inside a component and it captures a variable, that variable is serialised into the payload the browser holds. It is encrypted, and the React team’s own advice is not to treat the encryption as a licence to close over secrets. Keep API keys and tokens out of the render scope of any component that declares an inline action.&lt;/p&gt;
&lt;p&gt;If you self-host across several instances, each one generates its own encryption key at build time and actions created on one box fail on another. &lt;code&gt;NEXT_SERVER_ACTIONS_ENCRYPTION_KEY&lt;/code&gt; pins it, base64-encoded, 32 bytes from &lt;code&gt;openssl rand -base64 32&lt;/code&gt;. Behind a reverse proxy where the public domain differs from the internal host, the Origin check will also start rejecting legitimate traffic until you list the proxy in &lt;code&gt;serverActions.allowedOrigins&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;None of that is authorisation. The framework has no idea who is allowed to delete which post, so every action does its own check:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;deletePost&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;session&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Unauthorized'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findUnique&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;authorId &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Forbidden'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;revalidatePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/posts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two checks, not one. Authentication asks whether there is a user; authorisation asks whether &lt;em&gt;this&lt;/em&gt; user owns &lt;em&gt;this&lt;/em&gt; row. A page-level guard covers neither, because the guard decides what renders and the action is a separate door into the same building. If you are picking a session library to hang off &lt;code&gt;auth()&lt;/code&gt;, I went through the options in &lt;a href=&quot;/blog/better-auth/&quot;&gt;Better Auth&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;habits-worth-forming&quot;&gt;Habits worth forming&lt;/h3&gt;
&lt;p&gt;Some of these I arrived at by reading the docs properly, others by getting them wrong first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One action, one job. Actions that create &lt;em&gt;and&lt;/em&gt; email &lt;em&gt;and&lt;/em&gt; audit are impossible to reuse and unpleasant to test.&lt;/li&gt;
&lt;li&gt;Keep the action thin and push the database work into a module marked &lt;code&gt;import &amp;#39;server-only&amp;#39;&lt;/code&gt;. The action becomes a validation and authorisation shell.&lt;/li&gt;
&lt;li&gt;Return only what the UI renders. A returned Prisma record ships every column to the browser, including the ones you forgot were there.&lt;/li&gt;
&lt;li&gt;Revalidate the narrowest thing that changed.&lt;/li&gt;
&lt;li&gt;Rate limit anything that sends mail or costs money.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;where-ive-landed&quot;&gt;Where I’ve landed&lt;/h3&gt;
&lt;p&gt;I like Server Actions more than I expected to. The thing that won me over was not the reduced boilerplate, it was that a form works before the JavaScript arrives and keeps working after it does, and I did not have to argue with anyone to get that.&lt;/p&gt;
&lt;p&gt;What I am still uneasy about is how invisible the endpoint is. An API route is a file you can see, curl and reason about. An action is a function that quietly acquires a public URL, and the only thing standing between it and the internet is whatever check you remembered to write inside it. The framework has done a lot to make that safe by default. It cannot do the authorisation for you, and the ease of writing an action makes it very easy to forget that you owe it one&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Stripe Integration in Next.js]]></title><description><![CDATA[Wiring Stripe Checkout into a Next.js App Router project: sessions, webhooks, local testing with the CLI, and what the new Basil API version quietly changes.]]></description><link>https://angelo.dini.dev/blog/stripe/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/stripe/</guid><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-part-you-only-get-to-test-in-production-once&quot;&gt;The part you only get to test in production once&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Payments are the one feature where a bug does not produce a bad user experience, it produces a bank statement. Everything else in a web app can be fixed on Tuesday. A double charge cannot, and nor can an order that was paid for and never shipped because a request timed out at the wrong moment.&lt;/p&gt;&lt;p&gt;Stripe is the obvious choice and I’m not going to pretend otherwise. What I want to write about is the shape of a Next.js integration that survives contact with reality, which mostly means: the checkout session is the easy half, and the webhook is the actual integration.&lt;/p&gt;
&lt;p&gt;Stripe shipped a new API version on 31 March, &lt;code&gt;2025-03-31.basil&lt;/code&gt;, and the Node SDK went to 18.0.0 the day after to match. There are two changes in there that will quietly break a subscription integration you copied from a tutorial written last year, so I’ll come back to those. For the database side I’m assuming something like the setup from &lt;a href=&quot;/blog/prisma/&quot;&gt;Using Prisma with Next.js&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;two-ways-in-and-only-one-of-them-is-worth-starting-with&quot;&gt;Two ways in, and only one of them is worth starting with&lt;/h3&gt;
&lt;p&gt;Stripe gives you Checkout, which is a page they host and maintain, and Elements plus the Payment Intents API, which is a set of components you assemble into your own form.&lt;/p&gt;
&lt;p&gt;Start with Checkout. It handles Strong Customer Authentication, Apple Pay and Google Pay, local payment methods, address collection, tax and receipts, and every one of those is a thing you would otherwise have to build and then keep working. The usual objection is branding, and it’s a weak one: you can set a logo, colours and a domain, and the customers who abandon carts are not abandoning them over the shade of your button.&lt;/p&gt;
&lt;p&gt;Move to Elements when you have a concrete reason, like a checkout flow with steps that have to happen between card entry and confirmation. Not before.&lt;/p&gt;
&lt;h3 id=&quot;keys-versions-and-the-pin-nobody-sets&quot;&gt;Keys, versions and the pin nobody sets&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; stripe @stripe/stripe-js&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stripe&lt;/code&gt; is the server SDK. &lt;code&gt;@stripe/stripe-js&lt;/code&gt; is the browser loader, and for a plain hosted-Checkout redirect you do not actually need it at all, which surprised me. It earns its place once you use Elements or embedded Checkout.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env.local&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;sk_test_&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token assign-left variable&quot;&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;whsec_&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token assign-left variable&quot;&gt;NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;pk_test_&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; one reaches the browser, which is exactly right: the publishable key is meant to be public, the other two are not. Next.js will happily inline anything prefixed that way into the client bundle, so mis-prefixing the secret key ships it to every visitor. Worth a second look before you commit.&lt;/p&gt;
&lt;p&gt;Pin the API version explicitly:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/stripe.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Stripe &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'stripe'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stripe &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Stripe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  apiVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'2025-03-31.basil'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the pin you get whatever version your account defaults to, which means a Stripe dashboard setting can change your application’s behaviour without a deploy. Pin it, and upgrade deliberately when you have time to read the changelog.&lt;/p&gt;
&lt;h3 id=&quot;creating-the-session&quot;&gt;Creating the session&lt;/h3&gt;
&lt;p&gt;One route handler, one job:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/checkout/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; headers &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/headers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; stripe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/stripe'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSession&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;session&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Unauthorized'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;401&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; priceId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; quantity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; origin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'origin'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; checkout &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; stripe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;checkout&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sessions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      mode&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'payment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      line_items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; priceId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; quantity &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      client_reference_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      customer_email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      success_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;origin&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/order/success?session_id={CHECKOUT_SESSION_ID}&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cancel_url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;origin&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/cart&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      idempotencyKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;checkout:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;priceId&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;quantity&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; checkout&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The session lookup is &lt;a href=&quot;/blog/better-auth/&quot;&gt;Better Auth&lt;/a&gt;, which I wrote about last month; swap in whatever you use. A few other things in there that the short examples leave out.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;price&lt;/code&gt; with a Price ID created in the dashboard rather than inline &lt;code&gt;price_data&lt;/code&gt;. Inline prices mean your catalogue lives in your source code, and the first time someone asks you to run a discount you’ll wish it didn’t. &lt;code&gt;price_data&lt;/code&gt; is fine for genuinely dynamic amounts, and nothing else.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;client_reference_id&lt;/code&gt; is the field that ties the Stripe session back to your user. It comes back on the webhook event. Without it you’re matching on email addresses, which works right up until someone checks out with a different one.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{CHECKOUT_SESSION_ID}&lt;/code&gt; is a literal template token that Stripe substitutes, not a JavaScript placeholder. It looks like a typo in every code sample and it isn’t.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;idempotencyKey&lt;/code&gt; means a double-clicked button creates one session rather than two. Stripe replays the original response for 24 hours. It costs nothing and removes a whole category of support ticket.&lt;/p&gt;
&lt;h3 id=&quot;redirecting&quot;&gt;Redirecting&lt;/h3&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// components/checkout-button.tsx&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CheckoutButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; priceId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; priceId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pending&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setPending&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleClick&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setPending&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/checkout'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'POST'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'application/json'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; priceId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setPending&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; url &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;href &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pending &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'One moment…'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Checkout'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. No Stripe.js, no &lt;code&gt;loadStripe&lt;/code&gt;, no promise held in module scope.&lt;/p&gt;
&lt;p&gt;If you’ve seen &lt;code&gt;stripe.redirectToCheckout({ sessionId })&lt;/code&gt; in older posts, that is the legacy path. It still exists in &lt;code&gt;@stripe/stripe-js&lt;/code&gt;, but Stripe’s own quickstarts have used &lt;code&gt;session.url&lt;/code&gt; for years now, and the &lt;code&gt;url&lt;/code&gt; approach is one fewer network round trip and one fewer script on your page.&lt;/p&gt;
&lt;h3 id=&quot;the-webhook-is-the-integration&quot;&gt;The webhook is the integration&lt;/h3&gt;
&lt;p&gt;Here is the thing that separates a demo from something you can charge money with: &lt;strong&gt;do not fulfil the order on the success page&lt;/strong&gt;. The customer’s browser can close, crash, lose signal or simply never follow the redirect, and the payment still went through. Stripe knows about the payment. Your database does not.&lt;/p&gt;
&lt;p&gt;The success page is a receipt. The webhook is the truth.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/stripe/webhook/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Stripe&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'stripe'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; stripe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/stripe'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fulfillOrder &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/orders'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; runtime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'nodejs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; signature &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'stripe-signature'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;signature&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Missing signature'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; event&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Stripe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Event&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    event &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; stripe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webhooks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;constructEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      signature&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Invalid signature: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'checkout.session.completed'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fulfillOrder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'checkout.session.async_payment_failed'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'payment_intent.payment_failed'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// notify, release stock, whatever your domain needs&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four details worth spelling out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the raw body.&lt;/strong&gt; &lt;code&gt;await request.text()&lt;/code&gt;, not &lt;code&gt;request.json()&lt;/code&gt;. The signature is computed over the exact bytes Stripe sent, so parsing and re-serialising invalidates it. In the App Router this is easy; in the old Pages Router you had to disable the body parser with &lt;code&gt;export const config = { api: { bodyParser: false } }&lt;/code&gt; and buffer the stream yourself. Not having to do that is one of the better reasons to be on the App Router.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pin the runtime to Node.&lt;/strong&gt; &lt;code&gt;constructEvent&lt;/code&gt; uses Node’s crypto. On an edge runtime you want &lt;code&gt;constructEventAsync&lt;/code&gt; instead, which goes through the Web Crypto API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verify before you trust.&lt;/strong&gt; The endpoint is public. Anyone can POST to it. Without signature verification, “please mark order 42 as paid” is a curl command.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Handle duplicates.&lt;/strong&gt; Stripe retries on any non-2xx response, and it will occasionally deliver the same event twice for reasons entirely on its side. &lt;code&gt;event.id&lt;/code&gt; is stable, so store it and make fulfilment a no-op the second time:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fulfillOrder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;eventId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; session&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Stripe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Checkout&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Session&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; created &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;processedEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; eventId &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    skipDuplicates&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;created&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// already handled&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;order&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;client_reference_id&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'paid'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; paymentIntentId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payment_intent &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And return quickly. Stripe’s delivery has a timeout, and a slow handler starts a retry storm. If fulfilment involves sending mail or talking to a warehouse, acknowledge first and queue the work.&lt;/p&gt;
&lt;h3 id=&quot;testing-it-locally&quot;&gt;Testing it locally&lt;/h3&gt;
&lt;p&gt;The webhook is unreachable from the internet on &lt;code&gt;localhost&lt;/code&gt;, so the CLI tunnels for you:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stripe listen&lt;/code&gt; prints a &lt;code&gt;whsec_...&lt;/code&gt; secret on startup. That is the one to put in &lt;code&gt;.env.local&lt;/code&gt; while developing; it is not the same as the endpoint secret from the dashboard, which is the one you need in production. I have lost time to that twice.&lt;/p&gt;
&lt;p&gt;To fire an event without going through the flow:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe trigger checkout.session.completed&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The card numbers, once you’re in test mode:&lt;/p&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Testing it locally&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;Expiry, CVC&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;4242 4242 4242 4242&lt;/td&gt;
&lt;td&gt;any future, any&lt;/td&gt;
&lt;td&gt;Succeeds immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4000 0027 6000 3184&lt;/td&gt;
&lt;td&gt;any future, any&lt;/td&gt;
&lt;td&gt;Forces a 3D Secure challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4000 0000 0000 0002&lt;/td&gt;
&lt;td&gt;any future, any&lt;/td&gt;
&lt;td&gt;Declined, &lt;code&gt;generic_decline&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4000 0000 0000 9995&lt;/td&gt;
&lt;td&gt;any future, any&lt;/td&gt;
&lt;td&gt;Declined, &lt;code&gt;insufficient_funds&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Test the 3D Secure one. European cards increasingly require it, and a flow that dead-ends on the authentication modal looks fine on your machine and loses you every customer in the EU.&lt;/p&gt;
&lt;h3 id=&quot;what-basil-changed&quot;&gt;What Basil changed&lt;/h3&gt;
&lt;p&gt;Two breaking changes in &lt;code&gt;2025-03-31.basil&lt;/code&gt; are worth knowing before they find you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subscriptions are created after payment now.&lt;/strong&gt; Previously, creating a subscription-mode Checkout Session created the subscription up front. Now Stripe waits until the customer has actually paid. This is a genuine improvement (it fixes the case where a failed first payment left a customer unable to update their billing details) but it means &lt;code&gt;session.subscription&lt;/code&gt; and &lt;code&gt;session.invoice&lt;/code&gt; are &lt;code&gt;null&lt;/code&gt; until the session completes. Any code that reads those straight after &lt;code&gt;sessions.create&lt;/code&gt; now reads nothing. Move it to the &lt;code&gt;checkout.session.completed&lt;/code&gt; handler.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;current_period_end&lt;/code&gt; moved.&lt;/strong&gt; It used to live on the &lt;code&gt;Subscription&lt;/code&gt;. In Basil it lives on each subscription item, because items in a single subscription can be on different billing cycles:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; subscription &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; stripe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;subscriptions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;retrieve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;subscriptionId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// used to work, doesn't any more:&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// const renewsAt = subscription.current_period_end;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; renewsAt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; subscription&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;items&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current_period_end&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every “gate the feature until the period ends” implementation written before April 2025 reads &lt;code&gt;undefined&lt;/code&gt; here, and &lt;code&gt;undefined&lt;/code&gt; compared against a timestamp is &lt;code&gt;false&lt;/code&gt;, so the failure mode is “everyone’s subscription looks expired”. Delightful.&lt;/p&gt;
&lt;p&gt;Also gone: expanding &lt;code&gt;total_count&lt;/code&gt; on list responses. If you were paginating with it, you now count differently.&lt;/p&gt;
&lt;h3 id=&quot;the-bit-that-still-bothers-me&quot;&gt;The bit that still bothers me&lt;/h3&gt;
&lt;p&gt;Amounts are integers in the currency’s smallest unit. &lt;code&gt;unit_amount: 2000&lt;/code&gt; is twenty francs, not two thousand. Everyone knows this and everyone has still, at some point, written &lt;code&gt;unit_amount: price&lt;/code&gt; where &lt;code&gt;price&lt;/code&gt; was already in francs and charged someone one hundredth of what they owed. The API cannot catch it because both values are valid integers.&lt;/p&gt;
&lt;p&gt;The only thing that has reliably worked for me is refusing to let a bare number cross the boundary. Wrap it, name it &lt;code&gt;amountInCents&lt;/code&gt;, make the type system carry the unit, and validate the total against a server-side price lookup before creating the session. Never send an amount from the client. It is a form field, and form fields lie.&lt;/p&gt;
&lt;h3 id=&quot;what-id-add-next&quot;&gt;What I’d add next&lt;/h3&gt;
&lt;p&gt;Reconciliation. Everything above is correct as long as webhooks arrive, and mostly they do, but “mostly” is not a word I enjoy near an order table. A nightly job that lists Stripe’s completed sessions for the last day and compares them against local orders would close the gap, and I have not written it yet.&lt;/p&gt;
&lt;p&gt;Also worth a look is embedded Checkout via &lt;code&gt;ui_mode: &amp;#39;embedded&amp;#39;&lt;/code&gt;, which keeps the customer on your domain and returns a &lt;code&gt;client_secret&lt;/code&gt; instead of a &lt;code&gt;url&lt;/code&gt;. Slightly more work, noticeably less jarring.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Better Auth]]></title><description><![CDATA[Setting up Better Auth in a TypeScript app: database adapters, the generated schema, mounting handlers, plugins for 2FA and organisations, and testing.]]></description><link>https://angelo.dini.dev/blog/better-auth/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/better-auth/</guid><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;authentication-you-can-read&quot;&gt;Authentication you can read&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every project reaches the same fork. You either hand authentication to a service and accept that your users live in someone else's database, or you build it yourself and spend the next three weeks discovering what you forgot about session rotation. Neither option has ever felt right to me, which is why Better Auth caught my attention.&lt;/p&gt;&lt;p&gt;It is a TypeScript library, not a service. It writes its tables into your database, runs inside your app, and hands you a &lt;code&gt;Response&lt;/code&gt; when a request comes in. There is no dashboard, no per-monthly-active-user pricing, and no third party holding your session table hostage.&lt;/p&gt;
&lt;p&gt;I was already using Prisma on the project I tried it on, which I wrote about in &lt;a href=&quot;/blog/prisma/&quot;&gt;Using Prisma with Next.js&lt;/a&gt;, so the adapter story mattered to me more than the feature list. Version 1.2.5 is current as I write this, and 1.2 landed a few weeks ago with a pile of new plugins, so this is a reasonable moment to look at it.&lt;/p&gt;
&lt;h3 id=&quot;where-it-actually-sits&quot;&gt;Where it actually sits&lt;/h3&gt;
&lt;p&gt;The comparison people reach for is Auth.js, and it’s the wrong one to start with. Auth.js is built around OAuth providers, with credentials support bolted on and deliberately discouraged. Better Auth treats email and password as a first-class citizen and hangs the social providers off the side.&lt;/p&gt;
&lt;p&gt;The comparison that matters is Clerk or Auth0. Those give you hosted UI, a support contract and someone else’s uptime. Better Auth gives you none of that and, in exchange, your &lt;code&gt;user&lt;/code&gt; table is a table in your database that you can join against. For a small product where the user record needs to be foreign-keyed to half the schema, that trade is worth making. For a team without anyone who wants to own auth, it is not, and I’d stop pretending otherwise.&lt;/p&gt;
&lt;h3 id=&quot;setting-it-up&quot;&gt;Setting it up&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; better-auth&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two environment variables. The secret is used for encryption and hashing, so generate a real one:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .env&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;BETTER_AUTH_SECRET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# openssl rand -base64 32&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;BETTER_AUTH_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;http://localhost:3000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then an &lt;code&gt;auth.ts&lt;/code&gt;, exported as &lt;code&gt;auth&lt;/code&gt; so the CLI can find it later:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/auth.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; betterAuth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; prismaAdapter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/adapters/prisma'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PrismaClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@prisma/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prisma &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PrismaClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;betterAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  appName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'My App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  database&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;prismaAdapter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prisma&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    provider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'postgresql'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  emailAndPassword&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  socialProviders&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    github&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      clientId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;GITHUB_CLIENT_ID&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      clientSecret&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;GITHUB_CLIENT_SECRET&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  trustedOrigins&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;as string&lt;/code&gt; casts are ugly and I resented typing them, but &lt;code&gt;process.env.X&lt;/code&gt; is &lt;code&gt;string | undefined&lt;/code&gt; and the option is not. Validate your environment with Zod at boot and the casts become honest.&lt;/p&gt;
&lt;p&gt;If you’re not on an ORM, you can hand &lt;code&gt;database&lt;/code&gt; a &lt;code&gt;better-sqlite3&lt;/code&gt; instance, a &lt;code&gt;pg&lt;/code&gt; &lt;code&gt;Pool&lt;/code&gt;, a &lt;code&gt;mysql2&lt;/code&gt; pool, or any Kysely dialect, and Better Auth runs the queries itself. The built-in adapters are Prisma, Drizzle and MongoDB. There is a memory adapter too, which is mostly useful for tests. That’s the whole list, and it’s worth knowing before you start, because a fair amount of what you’ll read online invents adapters that don’t exist.&lt;/p&gt;
&lt;h3 id=&quot;the-schema-is-yours-which-is-the-point&quot;&gt;The schema is yours, which is the point&lt;/h3&gt;
&lt;p&gt;Better Auth needs four core tables, and plugins add more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt; holds the profile plus an &lt;code&gt;emailVerified&lt;/code&gt; flag&lt;/li&gt;
&lt;li&gt;&lt;code&gt;session&lt;/code&gt; holds live sessions, with IP address and user agent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;account&lt;/code&gt; holds linked providers, OAuth tokens, and the password hash&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verification&lt;/code&gt; holds short-lived tokens for email confirmation and password resets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The password living on &lt;code&gt;account&lt;/code&gt; rather than on &lt;code&gt;user&lt;/code&gt; threw me for a minute, and then made sense: email-and-password is just another provider, so it gets a row like GitHub does. Which also means a user can have several accounts and one of them happens to be credentials.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @better-auth/cli generate   &lt;span class=&quot;token comment&quot;&gt;# writes a Prisma/Drizzle schema or SQL file&lt;/span&gt;
npx @better-auth/cli migrate    &lt;span class=&quot;token comment&quot;&gt;# applies directly (built-in Kysely adapter only)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With Prisma you run &lt;code&gt;generate&lt;/code&gt; and then your usual &lt;code&gt;prisma migrate dev&lt;/code&gt;. The important habit is re-running &lt;code&gt;generate&lt;/code&gt; every time you add a plugin or an additional field, because plugins ship schema. Forgetting that produces runtime errors about missing columns that read like library bugs and aren’t.&lt;/p&gt;
&lt;p&gt;Additional fields go in the config rather than straight into the schema file, so the types flow through to &lt;code&gt;useSession&lt;/code&gt; and &lt;code&gt;signUp.email&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;betterAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
  user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    additionalFields&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'string'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        defaultValue&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'user'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// users don't get to set their own role&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;input: false&lt;/code&gt; is doing real work. Without it, the sign-up endpoint accepts a &lt;code&gt;role&lt;/code&gt; from the request body and you have handed anyone with curl an admin account.&lt;/p&gt;
&lt;h3 id=&quot;mounting-the-handler&quot;&gt;Mounting the handler&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;auth.handler&lt;/code&gt; takes a web &lt;code&gt;Request&lt;/code&gt; and returns a &lt;code&gt;Response&lt;/code&gt;. Every framework integration is a thin wrapper over that.&lt;/p&gt;
&lt;p&gt;Next.js App Router:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/auth/[...all]/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toNextJsHandler &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/next-js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POST&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toNextJsHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Express, where there is a trap:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; express &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'express'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toNodeHandler &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/node'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; app &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;express&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/auth/*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toNodeHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// after, never before&lt;/span&gt;
app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;express&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3005&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mount &lt;code&gt;express.json()&lt;/code&gt; before the auth handler and the body gets consumed by the JSON parser, the handler waits for a stream that will never arrive, and every client call hangs on pending forever. There’s no error, just silence. On Express 5 the route pattern changes to &lt;code&gt;/api/auth/*splat&lt;/code&gt;, and CommonJS is not supported at all, so &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can check the mount with a &lt;code&gt;GET&lt;/code&gt; to &lt;code&gt;/api/auth/ok&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-client&quot;&gt;The client&lt;/h3&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/auth-client.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createAuthClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; authClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createAuthClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  baseURL&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are equivalents for Vue, Svelte, Solid and vanilla. The React one gives you &lt;code&gt;useSession&lt;/code&gt; as a reactive hook, and everything else is a plain async call:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; authClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;signUp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; password&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;onSuccess&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; router&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/dashboard'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; authClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;signIn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;social&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; provider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'github'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second argument is a set of lifecycle callbacks (&lt;code&gt;onRequest&lt;/code&gt;, &lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onError&lt;/code&gt;), which I ended up preferring to unwrapping &lt;code&gt;{ data, error }&lt;/code&gt; at every call site. Users are signed in automatically after sign-up unless you set &lt;code&gt;autoSignIn: false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On the server, the same endpoints are available under &lt;code&gt;auth.api&lt;/code&gt;, taking &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt; and &lt;code&gt;query&lt;/code&gt; instead of a merged object:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; headers &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/headers'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; auth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; session &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSession&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Failures throw &lt;code&gt;APIError&lt;/code&gt; from &lt;code&gt;better-auth/api&lt;/code&gt;, which carries a &lt;code&gt;status&lt;/code&gt;, so mapping it to an HTTP response is straightforward.&lt;/p&gt;
&lt;h3 id=&quot;sending-the-mail-is-your-job&quot;&gt;Sending the mail is your job&lt;/h3&gt;
&lt;p&gt;Better Auth never sends an email. It calls a function you supply. I like that more than I expected to, because it means the verification mail lives with the rest of the templates instead of in someone’s dashboard. I’d been taking &lt;a href=&quot;/blog/react-email/&quot;&gt;React Email&lt;/a&gt; apart the month before, so it dropped straight in:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/auth.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; sendPasswordResetMail&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sendVerificationMail &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/mailer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;betterAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
  emailAndPassword&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    requireEmailVerification&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;sendResetPassword&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendPasswordResetMail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  emailVerification&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    sendOnSignUp&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    autoSignInAfterVerification&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;sendVerificationEmail&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendVerificationMail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the rendering behind that &lt;code&gt;mailer&lt;/code&gt; module rather than inlining it here, because the templates are JSX and &lt;code&gt;auth.ts&lt;/code&gt; would have to become &lt;code&gt;auth.tsx&lt;/code&gt; to hold them. Small thing, annoying to discover halfway through.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;requireEmailVerification: true&lt;/code&gt;, an unverified user attempting to sign in triggers &lt;code&gt;sendVerificationEmail&lt;/code&gt; again rather than getting a session, which is the behaviour you want and not the behaviour I assumed.&lt;/p&gt;
&lt;p&gt;Sessions are worth a deliberate look too. They last seven days by default and get extended whenever they’re used and &lt;code&gt;updateAge&lt;/code&gt; has passed:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;session&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  expiresIn&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  updateAge&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  cookieCache&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    maxAge&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cookieCache&lt;/code&gt; keeps a signed copy of the session in the cookie so &lt;code&gt;getSession&lt;/code&gt; can answer without a database round trip. With a five minute window, a revoked session stays usable for up to five minutes, so it’s a straight trade between latency and immediacy. There’s also &lt;code&gt;freshAge&lt;/code&gt;, one day by default, controlling how recently a session must have been created before the sensitive endpoints will accept it. You can set it to &lt;code&gt;0&lt;/code&gt; to disable the check. I would think about that one for a while first.&lt;/p&gt;
&lt;h3 id=&quot;plugins-are-where-it-gets-interesting&quot;&gt;Plugins are where it gets interesting&lt;/h3&gt;
&lt;p&gt;The core is deliberately small. Everything else is a plugin, and most plugins come in pairs: a server half and a client half that adds the matching methods to &lt;code&gt;authClient&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two-factor authentication, as an example:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/auth.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; twoFactor &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/plugins'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;betterAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  appName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'My App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// used as the TOTP issuer&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
  plugins&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;twoFactor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/auth-client.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; twoFactorClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/client/plugins'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; authClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createAuthClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  plugins&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;twoFactorClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, after re-running the CLI:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; authClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;twoFactor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;enable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; password &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// data.totpURI -&gt; render as a QR code&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// data.backupCodes -&gt; show once, never again&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; authClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;twoFactor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;verifyTotp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; code &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worth knowing: &lt;code&gt;twoFactorEnabled&lt;/code&gt; stays &lt;code&gt;false&lt;/code&gt; until the user verifies a TOTP code. The flag reflects a working authenticator, not an intention, which is exactly right and caught me off guard the first time. After that, &lt;code&gt;signIn.email&lt;/code&gt; for a 2FA user returns &lt;code&gt;twoFactorRedirect: true&lt;/code&gt; instead of a session, and you handle the second step yourself.&lt;/p&gt;
&lt;p&gt;The organisation plugin covers multi-tenancy: organisations, members, invitations, and since 1.2, teams inside an organisation. Role-based access control lives in &lt;code&gt;better-auth/plugins/access&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createAccessControl &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/plugins/access'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defaultStatements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; adminAc &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/plugins/organization/access'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; statement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;defaultStatements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  invoice&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'read'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'issue'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'void'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ac &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createAccessControl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;statement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; admin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ac&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newRole&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;adminAc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;statements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  invoice&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'read'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'issue'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'void'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spreading &lt;code&gt;defaultStatements&lt;/code&gt; and &lt;code&gt;adminAc.statements&lt;/code&gt; is not optional. Define a custom role without them and you silently overwrite the built-in permissions for that role, which is a fun afternoon.&lt;/p&gt;
&lt;p&gt;The rest of 1.2 brought an API key plugin, a captcha plugin, a Stripe plugin for subscriptions, JWT encryption and a few more social providers. The passkey, magic link, email OTP, username, admin, multi-session and OIDC provider plugins were already there. It’s a lot of surface for a library that reached 1.0 four months ago, and I’d treat the newer ones with the caution that implies.&lt;/p&gt;
&lt;h3 id=&quot;testing&quot;&gt;Testing&lt;/h3&gt;
&lt;p&gt;The nice consequence of auth being a library is that it is testable without a network. Point it at the memory adapter and call the server API directly:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// auth.test.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; expect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; test &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vitest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; betterAuth &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; memoryAdapter &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/adapters/memory'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; APIError &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'better-auth/api'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;makeAuth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;betterAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    baseURL&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    secret&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'test-secret-not-for-production'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    database&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;memoryAdapter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      session&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      account&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      verification&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    emailAndPassword&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; enabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'signs a user up and returns a session'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;makeAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;signUpEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'user@example.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      password&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'correct-horse-battery'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Test User'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'user@example.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'rejects a short password'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; auth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;makeAuth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    auth&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;signUpEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'a@example.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; password&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'short'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'A'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rejects&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeInstanceOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;APIError&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A fresh instance per test keeps the in-memory store from leaking between them. The minimum password length is eight characters by default, which is what the second test is leaning on.&lt;/p&gt;
&lt;p&gt;For anything involving cookies you’ll want a real adapter and &lt;code&gt;asResponse: true&lt;/code&gt;, which gives you the full &lt;code&gt;Response&lt;/code&gt; including &lt;code&gt;Set-Cookie&lt;/code&gt; rather than just the data.&lt;/p&gt;
&lt;h3 id=&quot;what-id-watch-out-for&quot;&gt;What I’d watch out for&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;trustedOrigins&lt;/code&gt; is not decoration. Get it wrong across subdomains and you’ll spend an evening staring at requests that succeed and sessions that never appear, because the cookie was set on the wrong host.&lt;/p&gt;
&lt;p&gt;Plugin order in the array matters more than the docs suggest, particularly when several plugins hook the same endpoint.&lt;/p&gt;
&lt;p&gt;And the schema is generated, not owned. Custom columns you add by hand outside &lt;code&gt;additionalFields&lt;/code&gt; will not survive the next &lt;code&gt;generate&lt;/code&gt;. Put them in the config or in a separate table.&lt;/p&gt;
&lt;h3 id=&quot;would-i-use-it-again&quot;&gt;Would I use it again&lt;/h3&gt;
&lt;p&gt;Yes, on a project where the user record is genuinely part of the domain model and I want it in the same transaction as everything else. I would not reach for it on something where auth is a checkbox and nobody on the team wants to think about session rotation. That’s what the hosted services are for, and they’re good at it.&lt;/p&gt;
&lt;p&gt;I’m now curious about the Stripe plugin from 1.2, mostly because subscription state and user state ending up in two different databases is a problem I’ve watched several projects lose to. That might be the next thing I take apart.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[React Email]]></title><description><![CDATA[A practical look at React Email: the component set, the async render change, Tailwind support, testing with Vitest, and sending templates through AWS SES.]]></description><link>https://angelo.dini.dev/blog/react-email/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-email/</guid><pubDate>Sun, 16 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;still-tables-all-the-way-down&quot;&gt;Still tables, all the way down&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Email markup never moved on. The HTML you write for an inbox in 2025 is roughly the HTML you would have written in 2003: nested tables, inline styles, and a quiet hope that Outlook does something reasonable. Every convenience I take for granted in the browser (components, types, a dev server that reloads) stops at the mailbox door.&lt;/p&gt;&lt;p&gt;React Email is an attempt to put those conveniences back. You write templates as React components, and it renders them down to the table soup that mail clients actually understand. The generated HTML is still horrible. The difference is that you no longer have to look at it.&lt;/p&gt;
&lt;p&gt;I write React most days, so the pitch landed easily. The components now declare React 19 as a supported peer alongside 18, so if you’ve already moved to &lt;a href=&quot;/blog/react-19/&quot;&gt;React 19&lt;/a&gt; there’s no dependency wrestling involved. Below is what I found once I got past the readme.&lt;/p&gt;
&lt;h3 id=&quot;whats-actually-in-the-package&quot;&gt;What’s actually in the package&lt;/h3&gt;
&lt;p&gt;Two packages do the work, and it’s worth knowing which is which.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@react-email/components&lt;/code&gt; (0.0.33 at the time of writing) is the component library. It re-exports about twenty small packages: &lt;code&gt;Html&lt;/code&gt;, &lt;code&gt;Head&lt;/code&gt;, &lt;code&gt;Body&lt;/code&gt;, &lt;code&gt;Container&lt;/code&gt;, &lt;code&gt;Section&lt;/code&gt;, &lt;code&gt;Row&lt;/code&gt;, &lt;code&gt;Column&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Heading&lt;/code&gt;, &lt;code&gt;Button&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt;, &lt;code&gt;Img&lt;/code&gt;, &lt;code&gt;Hr&lt;/code&gt;, &lt;code&gt;Font&lt;/code&gt;, &lt;code&gt;Preview&lt;/code&gt;, &lt;code&gt;Markdown&lt;/code&gt;, &lt;code&gt;CodeBlock&lt;/code&gt;, &lt;code&gt;CodeInline&lt;/code&gt; and the &lt;code&gt;Tailwind&lt;/code&gt; wrapper. It also re-exports &lt;code&gt;render&lt;/code&gt;, so in practice one dependency covers you.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;react-email&lt;/code&gt; (3.0.7) is the CLI and the preview app. It is a dev dependency, not something you ship.&lt;/p&gt;
&lt;p&gt;The components are thin on purpose, and that’s the part I like. Unwrap the layout ones and there’s almost nothing there:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: What’s actually in the package&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it renders&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Section&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; with a single &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Row&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; with a &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Column&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Container&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; capped at &lt;code&gt;maxWidth: 37.5em&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hidden &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with &lt;code&gt;maxHeight: 0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;37.5em&lt;/code&gt; is the 600px everyone has silently agreed on. &lt;code&gt;Preview&lt;/code&gt; holds the snippet text that shows up in the inbox list, padded with invisible characters so the client doesn’t drag your footer into the preview line.&lt;/p&gt;
&lt;p&gt;None of that is clever. It is just the table boilerplate you would have written by hand, given a name and a prop signature. Which is the correct amount of abstraction for a problem this dumb.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-running&quot;&gt;Getting it running&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @react-email/components
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev react-email&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Templates go in an &lt;code&gt;emails/&lt;/code&gt; directory by default. Add a script:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;email dev --dir ./emails --port 3000&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;email dev&lt;/code&gt; starts a preview server that watches the directory and rebuilds on save. The other commands are &lt;code&gt;email build&lt;/code&gt; and &lt;code&gt;email start&lt;/code&gt; (build the preview app and serve it, useful if you want designers poking at templates on a staging URL) and &lt;code&gt;email export&lt;/code&gt;, which writes the compiled templates to &lt;code&gt;out/&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx email &lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--outDir&lt;/span&gt; out &lt;span class=&quot;token parameter variable&quot;&gt;--pretty&lt;/span&gt;
npx email &lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--outDir&lt;/span&gt; out &lt;span class=&quot;token parameter variable&quot;&gt;--plainText&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing that caught me out: &lt;code&gt;--dir&lt;/code&gt; is resolved relative to the project root, not to wherever you run the command from. If the preview server shows an empty list, that’s usually why.&lt;/p&gt;
&lt;h3 id=&quot;a-template-with-some-meat-on-it&quot;&gt;A template with some meat on it&lt;/h3&gt;
&lt;p&gt;The tutorial examples are all “Hello world in a &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;”, which tells you nothing. Here’s something closer to a real transactional mail, with typed props and a preview fixture:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// emails/welcome.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Body&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Button&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Container&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Head&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Hr&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Html&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Preview&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Section&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@react-email/components'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;WelcomeEmailProps&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  confirmUrl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;WelcomeEmail&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; confirmUrl &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; WelcomeEmailProps&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Html&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;en&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Head&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Preview&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Confirm your address and you are done&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Preview&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Body&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;container&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;heading&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Hello &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          One more step. Confirm your address so we know the mailbox is real.
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Section&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; textAlign&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'center'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;confirmUrl&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;button&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
            Confirm my address
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Section&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Hr&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;rule&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;footnote&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          If you did not sign up, ignore this mail. Nothing happens.
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Body&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Html&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

WelcomeEmail&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PreviewProps &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Angelo'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  confirmUrl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://example.com/confirm/abc123'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; satisfies WelcomeEmailProps&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; WelcomeEmail&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  backgroundColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#f4f4f5'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  fontFamily&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Helvetica, Arial, sans-serif'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; backgroundColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#ffffff'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; padding&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'24px'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; heading &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fontSize&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'20px'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fontWeight&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'bold'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; button &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  backgroundColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#111827'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#ffffff'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  padding&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'12px 20px'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  borderRadius&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'4px'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  textDecoration&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'none'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rule &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; borderColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#e4e4e7'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; footnote &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fontSize&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'12px'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#71717a'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PreviewProps&lt;/code&gt; is the bit worth stealing. The preview server reads that static property and renders the template with those values, so you get a realistic preview without a fake wrapper component. &lt;code&gt;satisfies&lt;/code&gt; keeps it honest when you rename a prop.&lt;/p&gt;
&lt;p&gt;Note that &lt;code&gt;Button&lt;/code&gt; is just an &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; under the hood, so &lt;code&gt;textDecoration: &amp;#39;none&amp;#39;&lt;/code&gt; is on you. Older versions had &lt;code&gt;pX&lt;/code&gt;/&lt;code&gt;pY&lt;/code&gt; props for padding; those are gone, and normal &lt;code&gt;padding&lt;/code&gt; in the style object is the way now.&lt;/p&gt;
&lt;h3 id=&quot;tailwind-and-where-it-stops&quot;&gt;Tailwind, and where it stops&lt;/h3&gt;
&lt;p&gt;There’s a &lt;code&gt;Tailwind&lt;/code&gt; component that wraps your tree and rewrites utility classes into inline styles at render time:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Tailwind &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@react-email/components'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Tailwind
  config&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    theme&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      extend&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        colors&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; brand&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'#111827'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Body&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-zinc-100 font-sans&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-white p-6&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text-xl font-bold&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Hello&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Body&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Tailwind&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The config prop takes a subset of a Tailwind v3 config object (theme, plugins, darkMode and a few others), so a shared theme file from your app mostly transplants.&lt;/p&gt;
&lt;p&gt;Here is the caveat nobody puts in the headline. Anything that cannot be expressed as an inline style, so media queries, &lt;code&gt;hover:&lt;/code&gt;, &lt;code&gt;dark:&lt;/code&gt;, gets hoisted into a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; instead. That works in clients which keep head styles, and it silently does nothing in the ones that strip them. Gmail’s web client is fine with it; several others are not. So responsive breakpoints in an email are a progressive enhancement, not a layout strategy. Build the thing to look correct at 600px first.&lt;/p&gt;
&lt;h3 id=&quot;render-returns-a-promise-now&quot;&gt;render() returns a promise now&lt;/h3&gt;
&lt;p&gt;This is the change most old tutorials get wrong. Since &lt;code&gt;@react-email/render&lt;/code&gt; 1.0, &lt;code&gt;render()&lt;/code&gt; is asynchronous:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  element&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactElement&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Options
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;renderAsync&lt;/code&gt; still exists but is deprecated, and now just points at &lt;code&gt;render&lt;/code&gt;. The reason is that React is moving away from &lt;code&gt;renderToStaticMarkup&lt;/code&gt;, and an async renderer is what keeps Suspense and server components working.&lt;/p&gt;
&lt;p&gt;The practical consequence: if you forget the &lt;code&gt;await&lt;/code&gt;, you pass a &lt;code&gt;Promise&lt;/code&gt; where a string is expected and send an email whose body reads &lt;code&gt;[object Promise]&lt;/code&gt;. It fails quietly, which is the worst kind of failing.&lt;/p&gt;
&lt;p&gt;The options are small:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; html &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;WelcomeEmail &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pretty&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;WelcomeEmail &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; plainText&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pretty&lt;/code&gt; beautifies the output (nice for snapshots, pointless in production, and it costs you bytes you may not have). &lt;code&gt;plainText&lt;/code&gt; runs the HTML through &lt;code&gt;html-to-text&lt;/code&gt;, and you can tune that with &lt;code&gt;htmlToTextOptions&lt;/code&gt;. There’s also a &lt;code&gt;plainTextSelectors&lt;/code&gt; export with sensible defaults if you want to build your own conversion.&lt;/p&gt;
&lt;h3 id=&quot;testing-the-templates&quot;&gt;Testing the templates&lt;/h3&gt;
&lt;p&gt;Templates are components, so they test like components. I covered the general approach in &lt;a href=&quot;/blog/testing-strategies-in-react/&quot;&gt;Testing Strategies in React&lt;/a&gt;; nothing here is special except the &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev vitest @vitejs/plugin-react&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You do need the React plugin, otherwise Vitest will not transform the JSX in your &lt;code&gt;.tsx&lt;/code&gt; templates and you’ll get a parse error that looks like a Vitest bug and isn’t.&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vitest.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vitest/config'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; react &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@vitejs/plugin-react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  plugins&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  test&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; environment&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'node'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// emails/welcome.test.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; expect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; test &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vitest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; render &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@react-email/components'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; WelcomeEmail &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./welcome'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; props &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Angelo'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; confirmUrl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://example.com/c/abc'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'renders the confirmation link'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; html &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;WelcomeEmail&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toContain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'https://example.com/c/abc'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toContain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Hello Angelo'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'has a usable plain text alternative'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;WelcomeEmail&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; plainText&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toContain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Confirm my address'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;not&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toContain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'&amp;lt;td'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would not snapshot the full HTML. It changes every time a component package bumps a patch version, and you end up approving diffs you haven’t read. Assert on the things that would actually cause a support ticket: the link is correct, the name is interpolated, the plain text alternative is not empty.&lt;/p&gt;
&lt;h3 id=&quot;sending-through-aws-ses&quot;&gt;Sending through AWS SES&lt;/h3&gt;
&lt;p&gt;React Email renders. It does not send. That part is yours, and SES is the cheap option if you already have an AWS account.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @aws-sdk/client-ses&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/send-welcome.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; SESClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; SendEmailCommand &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@aws-sdk/client-ses'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; render &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@react-email/components'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; WelcomeEmail &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../emails/welcome'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ses &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SESClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; region&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'eu-central-1'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendWelcome&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;to&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; confirmUrl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; element &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;WelcomeEmail&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;confirmUrl&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;confirmUrl&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;element&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;element&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; plainText&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; ses&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SendEmailCommand&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      Source&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'hello@example.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      Destination&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ToAddresses&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;to&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      Message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        Subject&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Confirm your address'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Charset&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'UTF-8'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          Html&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; html&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Charset&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'UTF-8'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          Text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Charset&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'UTF-8'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details that matter and are easy to miss.&lt;/p&gt;
&lt;p&gt;The file has to be &lt;code&gt;.tsx&lt;/code&gt;, not &lt;code&gt;.ts&lt;/code&gt;. It contains JSX. I have seen that exact mistake copied around in half the blog posts on this subject.&lt;/p&gt;
&lt;p&gt;Send both parts. A message with only an HTML body scores worse with spam filters and renders as nothing at all in a text-only client. Since &lt;code&gt;render&lt;/code&gt; already gives you the plain text version, there is no excuse.&lt;/p&gt;
&lt;p&gt;On the AWS side, a new account starts in the SES sandbox: 200 messages per 24 hours, one message per second, and every recipient address has to be verified first. You request production access through a support ticket, which takes a day or two. Before you do, set up DKIM signing on the sending domain and add SPF and DMARC records, because the request asks how you handle bounces and complaints and the honest answer needs to be “with a configuration set publishing to SNS”, not “I don’t”.&lt;/p&gt;
&lt;p&gt;If you’d rather use the newer API, &lt;code&gt;@aws-sdk/client-sesv2&lt;/code&gt; has its own &lt;code&gt;SendEmailCommand&lt;/code&gt; with a different parameter shape (&lt;code&gt;FromEmailAddress&lt;/code&gt;, &lt;code&gt;Content.Simple.…&lt;/code&gt;). Same idea, different nouns.&lt;/p&gt;
&lt;h3 id=&quot;the-parts-that-still-hurt&quot;&gt;The parts that still hurt&lt;/h3&gt;
&lt;p&gt;React Email fixes the authoring experience. It does not fix email.&lt;/p&gt;
&lt;p&gt;Classic Outlook on Windows still renders through the Word engine, so no flexbox, no grid, and background images need VML if you really want them. The new Outlook is on a web engine, which helps, but you cannot assume your recipients have it.&lt;/p&gt;
&lt;p&gt;Gmail clips a message once the HTML passes about 102 KB and hides the rest behind a “view entire message” link. That is HTML only, images don’t count, but a Tailwind-heavy template with lots of repeated inline styles gets there faster than you’d think. Rendering without &lt;code&gt;pretty&lt;/code&gt; helps.&lt;/p&gt;
&lt;p&gt;Dark mode is a mess. Some clients invert your colours for you, some do it badly, some ignore &lt;code&gt;prefers-color-scheme&lt;/code&gt; entirely. I have stopped trying to control it and just avoid pure white backgrounds with near-white text.&lt;/p&gt;
&lt;p&gt;And the preview server is not a rendering test. It’s Chrome. It tells you your template compiles and roughly how it looks; it tells you nothing about Outlook. For that you still need Litmus, Email on Acid, or a folder of test accounts and some patience.&lt;/p&gt;
&lt;h3 id=&quot;where-ive-landed&quot;&gt;Where I’ve landed&lt;/h3&gt;
&lt;p&gt;I’d use it again. Not because emails became easy, but because the templates now live next to the rest of the code, get type-checked in CI, and can be reviewed in a pull request by someone who does not know what VML is.&lt;/p&gt;
&lt;p&gt;Next thing on my list is wiring &lt;code&gt;email export&lt;/code&gt; into the build so the compiled templates are artefacts rather than something rendered at request time. I have not decided yet whether that’s a real improvement or just me enjoying a build step&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Looking back at 2024]]></title><description><![CDATA[A short recap of my 2024: travelling, a few things that changed, and the eleven articles I published here about React, CSS, monorepos and headless CMSes.]]></description><link>https://angelo.dini.dev/blog/looking-back-at-2024/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/looking-back-at-2024/</guid><pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;eleven-posts-later&quot;&gt;Eleven posts later&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;2024 moved a lot. There was travelling, there were relationships that shifted around, and there was the usual amount of quietly rearranging my own head. On balance a good year.&lt;/p&gt;&lt;p&gt;I’m not going to write much about the personal side of that here. I’d rather keep going, pick up new things and see where they lead. What I can point at is the writing, which is the habit I managed to hold onto through all of it.&lt;/p&gt;
&lt;p&gt;Eleven articles, plus last January’s recap. One a month, every month, same as the year before.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/ai-and-ml/&quot;&gt;Integrating AI and Machine Learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-useeffectevent/&quot;&gt;Exploring useEffectEvent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/docusaurus/&quot;&gt;Introduction to Docusaurus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/testing-strategies-in-react/&quot;&gt;Testing Strategies in React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/css-techniques/&quot;&gt;Modern CSS Techniques&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/css-container-queries/&quot;&gt;CSS Container Queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/modular-frontend-with-shadcn/&quot;&gt;Modular Frontend Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/headless-cms/&quot;&gt;Headless CMS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/monorepos/&quot;&gt;Monorepos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/prisma/&quot;&gt;Using Prisma with Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-19/&quot;&gt;React 19&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reading the list back, it leans further into React than I intended when I started the year. Not a single &lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;homelab&lt;/a&gt; post either, which is a bit of a shame. The rack did not stop existing just because I stopped writing about it 🙃&lt;/p&gt;
&lt;h3 id=&quot;what-id-like-to-change&quot;&gt;What I’d like to change&lt;/h3&gt;
&lt;p&gt;Fewer “here is a new feature” posts and more “here is what broke”. The ones I enjoy writing are the ones where something went wrong first.&lt;/p&gt;
&lt;p&gt;See you all in 2025 🎉&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[React 19]]></title><description><![CDATA[React 19 is stable. A tour of Actions, useActionState, useFormStatus, useOptimistic and the use API, plus what "stable Server Components" really means.]]></description><link>https://angelo.dini.dev/blog/react-19/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-19/</guid><pubDate>Sat, 21 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-one-that-took-two-years&quot;&gt;The one that took two years&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;React 19 went stable on 5 December 2024, and if you have been running the Canary channel or a recent Next.js, most of it will already be familiar. That is the interesting part of this release: almost nothing in it is new, it is just finally allowed to be in a version number.&lt;/p&gt;&lt;p&gt;When I wrote about &lt;a href=&quot;/blog/react-18/&quot;&gt;React 18&lt;/a&gt; I said it was a set of small changes laying foundations for later releases. This is the release those foundations were for. The difference is that the patterns arrived first, in frameworks, and the library caught up afterwards. Nearly everything below spent a year or more in Canary before it got a version number.&lt;/p&gt;
&lt;p&gt;So this is less an announcement than an inventory. Here is what you can now rely on.&lt;/p&gt;
&lt;h3 id=&quot;actions&quot;&gt;Actions&lt;/h3&gt;
&lt;p&gt;An Action is an async function passed straight to a form’s &lt;code&gt;action&lt;/code&gt; prop. React calls it with the &lt;code&gt;FormData&lt;/code&gt;, and handles the pending state, the errors and the reset for you.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server-side action&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;formData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'name'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// perform server-side operations&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// client-side component&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createUser &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;UserForm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;createUser&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Create User&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function can be a plain client-side async function too. &lt;code&gt;&amp;#39;use server&amp;#39;&lt;/code&gt; only matters when you want it to run on the server, and it is a common misreading to think the directive is what makes something an Action.&lt;/p&gt;
&lt;h3 id=&quot;useactionstate&quot;&gt;&lt;code&gt;useActionState&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;When you need the result of an Action rather than just firing it, &lt;code&gt;useActionState&lt;/code&gt; wraps it and gives you back the state, a wrapped action to hand to the form, and a pending flag.&lt;/p&gt;
&lt;p&gt;The catch is the signature. An action used with this hook takes the previous state as its first argument:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;previousState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; formData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'name'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Name is required'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// create the user&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Created &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useActionState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createUser &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;UserForm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; formAction&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isPending&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useActionState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;createUser&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;formAction&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        Create User
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have used this in Canary or in Next.js and remember it as &lt;code&gt;useFormState&lt;/code&gt; from &lt;code&gt;react-dom&lt;/code&gt;, that is the same hook. It moved to &lt;code&gt;react&lt;/code&gt; and was renamed on the way.&lt;/p&gt;
&lt;h3 id=&quot;useformstatus&quot;&gt;&lt;code&gt;useFormStatus&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useFormStatus&lt;/code&gt; reads the state of the nearest parent form from inside a child component. This is the one I reach for most, because it means a submit button can know it is submitting without the form passing anything down to it.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useFormStatus &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SubmitButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pending &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useFormStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      Submit
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the import path. It comes from &lt;code&gt;react-dom&lt;/code&gt;, not &lt;code&gt;react&lt;/code&gt;, which will catch you at least once. It also has to be in a component rendered &lt;em&gt;inside&lt;/em&gt; the form; calling it in the component that renders the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; itself returns nothing useful.&lt;/p&gt;
&lt;h3 id=&quot;useoptimistic&quot;&gt;&lt;code&gt;useOptimistic&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useOptimistic&lt;/code&gt; shows the result before the server confirms it, and rolls back automatically if the action fails.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useOptimistic &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CommentList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; comments&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; addComment &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;optimisticComments&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; addOptimisticComment&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useOptimistic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    comments&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newComment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newComment&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;formData&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; comment &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; formData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'comment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;addOptimisticComment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;comment&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addComment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;comment&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleSubmit&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;comment&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Add Comment&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;optimisticComments&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;c&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first argument has to be the real state, not an empty array. Pass &lt;code&gt;[]&lt;/code&gt; and you get an optimistic list that forgets everything the moment the action settles, which looks like a rendering bug and is not one.&lt;/p&gt;
&lt;h3 id=&quot;the-use-api&quot;&gt;The &lt;code&gt;use&lt;/code&gt; API&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;use&lt;/code&gt; reads a resource during render. Given a promise it suspends until the promise resolves, and given a context it reads it.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; use &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Comments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; commentsPromise &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; comments &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;commentsPromise&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; comments&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;comment&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;comment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;comment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It breaks the rules of hooks on purpose. You can call it inside a conditional or a loop, which is the entire reason it exists and reads wrong the first few times you see it. What you must not do is create the promise inside the component that consumes it, because a new promise on every render means suspending forever. Pass it in from a server component or a cache.&lt;/p&gt;
&lt;h3 id=&quot;static-rendering-apis&quot;&gt;Static rendering APIs&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;react-dom/static&lt;/code&gt; gains &lt;code&gt;prerender&lt;/code&gt; and &lt;code&gt;prerenderToNodeStream&lt;/code&gt;. Unlike &lt;code&gt;renderToString&lt;/code&gt;, these wait for all the data to load before producing output, which is what you want when you are generating static HTML rather than streaming it to a browser.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; prerender &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom/static'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; prelude &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;prerender&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;App&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;bootstrapScripts&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/main.js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prelude&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;'content-type'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'text/html'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worth noticing that &lt;code&gt;prerender&lt;/code&gt; resolves to an object, not an HTML string. &lt;code&gt;prelude&lt;/code&gt; is a Web Stream. If you want a string you have to read the stream yourself, and if you are on Node without Web Streams, &lt;code&gt;prerenderToNodeStream&lt;/code&gt; is the one you want.&lt;/p&gt;
&lt;h3 id=&quot;server-components-and-what-stable-means-here&quot;&gt;Server Components, and what “stable” means here&lt;/h3&gt;
&lt;p&gt;React 19 includes the Server Components work that has been sitting in Canary for two years. Stable, in this case, means the feature will not break between minor versions.&lt;/p&gt;
&lt;p&gt;It does not mean you can turn it on. Server Components need a bundler and a router that understand them, so in practice you get them through a framework. What React ships is the contract, not the implementation.&lt;/p&gt;
&lt;p&gt;There is a second bit of small print worth knowing. The APIs a framework uses to &lt;em&gt;implement&lt;/em&gt; Server Components do not follow semver in 19.x and may change between minors. If you maintain a framework or a bundler, pin your React version. If you are building an application on top of Next.js, this is somebody else’s problem.&lt;/p&gt;
&lt;p&gt;The other thing to get straight: there is no directive for Server Components. Components are server components by default in an RSC setup, &lt;code&gt;&amp;#39;use client&amp;#39;&lt;/code&gt; opts out, and &lt;code&gt;&amp;#39;use server&amp;#39;&lt;/code&gt; marks a Server Action, which is a different thing entirely. Nearly everyone gets this backwards once.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server action&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createUser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;formData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// server-side logic&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// client component&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createUser &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;UserForm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;createUser&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Create User&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-smaller-changes-that-will-save-you-the-most-time&quot;&gt;The smaller changes that will save you the most time&lt;/h3&gt;
&lt;p&gt;The headline features get the blog posts. These are the ones that will show up in your day-to-day work first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ref&lt;/code&gt; is now a regular prop on function components. &lt;code&gt;forwardRef&lt;/code&gt; is no longer needed, and a good chunk of wrapper boilerplate disappears.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;Context&amp;gt;&lt;/code&gt; renders as a provider directly. &lt;code&gt;&amp;lt;Context.Provider&amp;gt;&lt;/code&gt; still works, but is on its way out.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; rendered anywhere in a component get hoisted into the document head. No more helmet library for simple cases.&lt;/li&gt;
&lt;li&gt;Errors from hydration mismatches finally tell you what did not match instead of printing two walls of HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;upgrading&quot;&gt;Upgrading&lt;/h3&gt;
&lt;p&gt;The codemods handle most of it and the official upgrade guide is worth the twenty minutes. Budget the time for everything that is not React, though: type packages, testing libraries, and any dependency with an opinion about &lt;code&gt;forwardRef&lt;/code&gt; or &lt;code&gt;Context.Provider&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I would not rush a large application over the line this month. The library is ready. The ecosystem around it is still catching up, and there is no prize for being early.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Using Prisma with Next.js]]></title><description><![CDATA[Setting up Prisma 7 in a Next.js project, from the schema, config file and driver adapter through migrations and server actions to the deploy steps you forget.]]></description><link>https://angelo.dini.dev/blog/prisma/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/prisma/</guid><pubDate>Fri, 29 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;coming-to-prisma-from-the-django-orm&quot;&gt;Coming to Prisma from the Django ORM&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;My roots are in Django, and the Django ORM spoils you. Models are the schema, migrations are generated from them, and the query API knows what your tables look like. Moving to a Node stack, the thing I missed most was not a framework. It was that.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.prisma.io/&quot;&gt;Prisma&lt;/a&gt; is the closest the JavaScript world has come. You write a schema, it generates a typed client from it, and your editor knows the shape of every row before you run anything. Paired with &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt;, where server components and server actions have made database access on the server the normal thing again, it fits well enough that I stopped looking for alternatives.&lt;/p&gt;
&lt;p&gt;Here is how I set it up, and the parts I get wrong when I am not paying attention.&lt;/p&gt;
&lt;p&gt;Revised in September 2026. The original was written against Prisma 5, and Prisma 7 changed the setup underneath it: a different generator, a config file, a database driver you install yourself, and a &lt;code&gt;generate&lt;/code&gt; step that no longer runs on its own. Everything below is what works with 7. If you have the older setup, the section near the end covers moving it across, and it is shorter than you would expect.&lt;/p&gt;
&lt;h3 id=&quot;why-it-works-with-nextjs-in-particular&quot;&gt;Why it works with Next.js in particular&lt;/h3&gt;
&lt;p&gt;The schema is a single file, and everything derives from it. Change a model, run one command, and the generated client, the types and the migration all follow. There is no second place where the shape of your data is written down, which is the failure mode of every hand-rolled query builder I have used.&lt;/p&gt;
&lt;p&gt;The rest of the fit comes from Next.js rather than Prisma:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server components and server actions run on the server, which is the only place a database client belongs.&lt;/li&gt;
&lt;li&gt;API route handlers are still there when you want a real HTTP endpoint.&lt;/li&gt;
&lt;li&gt;The generated types flow straight into your components, so a renamed column becomes a type error rather than an undefined at runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one thing worth saying out loud: none of this makes Prisma safe to import in a client component. More on that below, because it is the mistake everyone makes once.&lt;/p&gt;
&lt;h3 id=&quot;setting-it-up&quot;&gt;Setting it up&lt;/h3&gt;
&lt;h4 id=&quot;install-and-initialise&quot;&gt;Install and initialise&lt;/h4&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; prisma --save-dev
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @prisma/client @prisma/adapter-pg dotenv
npx prisma init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives you a &lt;code&gt;prisma/&lt;/code&gt; folder with &lt;code&gt;schema.prisma&lt;/code&gt;, a &lt;code&gt;prisma.config.ts&lt;/code&gt; at the project root, and a &lt;code&gt;.env&lt;/code&gt; file for the connection string. The config file is new in 7 and it is where the CLI now looks for the database, so the schema no longer knows the connection string at all.&lt;/p&gt;
&lt;p&gt;The adapter package is the other newcomer. Prisma 7 talks to Postgres through the same &lt;code&gt;pg&lt;/code&gt; driver you would use without Prisma, and &lt;code&gt;@prisma/adapter-pg&lt;/code&gt; is the wrapper that hands it over. There is one per database; SQLite gets &lt;code&gt;@prisma/adapter-better-sqlite3&lt;/code&gt;, and MongoDB is not supported in 7 at the time of writing, so a Mongo project stays on 6.&lt;/p&gt;
&lt;h4 id=&quot;configure-the-database&quot;&gt;Configure the database&lt;/h4&gt;
&lt;p&gt;In &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;postgresql://user:password@localhost:5432/mydb&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then tell the CLI where things are, in &lt;code&gt;prisma.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// prisma.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'dotenv/config'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineConfig&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; env &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'prisma/config'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  schema&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'prisma/schema.prisma'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  migrations&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'prisma/migrations'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  datasource&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'DATABASE_URL'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;dotenv&lt;/code&gt; import is not decoration. Prisma 7 stopped loading &lt;code&gt;.env&lt;/code&gt; on its own, so without that line &lt;code&gt;env(&amp;#39;DATABASE_URL&amp;#39;)&lt;/code&gt; comes back empty and the error talks about a missing URL rather than a missing file. Next.js loads &lt;code&gt;.env&lt;/code&gt; for the application itself; this import only serves the CLI.&lt;/p&gt;
&lt;p&gt;Now describe your data in &lt;code&gt;prisma/schema.prisma&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-prisma&quot;&gt;&lt;code class=&quot;language-prisma&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;generator&lt;/span&gt; client &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  provider &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prisma-client&quot;&lt;/span&gt;
  output   &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../generated/prisma&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;datasource&lt;/span&gt; db &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  provider &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;postgresql&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Post&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id        &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;      &lt;span class=&quot;token attribute function&quot;&gt;@id&lt;/span&gt; &lt;span class=&quot;token attribute function&quot;&gt;@default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;autoincrement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  title     &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;
  content   &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;
  published &lt;span class=&quot;token builtin&quot;&gt;Boolean&lt;/span&gt;  &lt;span class=&quot;token attribute function&quot;&gt;@default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  createdAt &lt;span class=&quot;token builtin&quot;&gt;DateTime&lt;/span&gt; &lt;span class=&quot;token attribute function&quot;&gt;@default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things changed here compared with the versions people copy from older tutorials. The generator is &lt;code&gt;prisma-client&lt;/code&gt;, the one without a Rust engine, and &lt;code&gt;output&lt;/code&gt; is required: the client is written into your project instead of into &lt;code&gt;node_modules&lt;/code&gt;, which is why the import in the next section points at a folder rather than a package. The path is relative to the schema file, so &lt;code&gt;../generated/prisma&lt;/code&gt; lands next to &lt;code&gt;prisma/&lt;/code&gt; in the project root. Add &lt;code&gt;generated/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;; it is build output.&lt;/p&gt;
&lt;p&gt;And the &lt;code&gt;url&lt;/code&gt; line is gone from the datasource block, because the config file owns it now. If you leave it in, the CLI tells you it is deprecated and ignores it, which is polite but not helpful when the two disagree.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; on &lt;code&gt;content&lt;/code&gt; is the nullable marker, and it propagates all the way into the generated TypeScript as &lt;code&gt;string | null&lt;/code&gt;. Small thing, but it is the reason the types are worth having.&lt;/p&gt;
&lt;h4 id=&quot;migrate&quot;&gt;Migrate&lt;/h4&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma migrate dev &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; init
npx prisma generate&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two commands where there used to be one. &lt;code&gt;migrate dev&lt;/code&gt; writes a migration file and applies it, but since 7 it no longer regenerates the client afterwards. You run &lt;code&gt;generate&lt;/code&gt; yourself, you will forget, and the type error that follows is the reminder.&lt;/p&gt;
&lt;p&gt;The migration lands in &lt;code&gt;prisma/migrations/&lt;/code&gt; as plain SQL, which means you can read it before it touches anything you care about. I do read it. Prisma is good at working out what changed, but it cannot know that the column you renamed had data in it.&lt;/p&gt;
&lt;h3 id=&quot;the-client-singleton-first&quot;&gt;The client singleton, first&lt;/h3&gt;
&lt;p&gt;Before writing a single query, create this file. Every example after it depends on it.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// lib/prisma.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PrismaPg &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@prisma/adapter-pg'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PrismaClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../generated/prisma/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; globalForPrisma &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalThis &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; prisma&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PrismaClient &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;createClient&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PrismaClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    adapter&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PrismaPg&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; connectionString&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;DATABASE_URL&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prisma &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalForPrisma&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prisma &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;NODE_ENV&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'production'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; globalForPrisma&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prisma &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prisma&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The adapter in the constructor is the Prisma 7 change with the most reasoning behind it. Earlier versions carried a query engine written in Rust, downloaded as a binary for your platform at install time, and it did the talking to the database. Seven drops it. The client is TypeScript all the way down, the database connection is a plain &lt;code&gt;pg&lt;/code&gt; pool, and you hand that pool to Prisma. No more binary to fetch during &lt;code&gt;npm install&lt;/code&gt;, which is one deploy-time failure I do not miss.&lt;/p&gt;
&lt;p&gt;The rest of the file is about hot reloading. In development, Next.js re-evaluates modules on every save, and a bare &lt;code&gt;new PrismaClient()&lt;/code&gt; at module scope means a fresh client and a fresh connection pool each time. After twenty saves your database starts refusing connections and the error message points at nothing useful. Stashing the instance on &lt;code&gt;globalThis&lt;/code&gt; in development only survives the reload; in production the module is evaluated once and the guard does nothing.&lt;/p&gt;
&lt;p&gt;Plenty of tutorials instantiate the client inline in each example because it reads more clearly. It also reproduces the bug.&lt;/p&gt;
&lt;h3 id=&quot;querying&quot;&gt;Querying&lt;/h3&gt;
&lt;p&gt;Server actions are where most of my database code lives now.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/actions.ts&lt;/span&gt;
&lt;span class=&quot;token string&quot;&gt;'use server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; prisma &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/prisma'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPublishedPosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; prisma&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    where&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; published&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    orderBy&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createdAt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'desc'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then call it from a server component and render the result during the server render:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/posts/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; getPublishedPosts &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../actions'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;PostsPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPublishedPosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;main&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;Published Posts&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;h1&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;li key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;li&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;ul&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;main&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No fetch, no API route, no serialisation boundary in the middle. The query runs on the server, the HTML comes back with the data in it, and the connection string never leaves the machine.&lt;/p&gt;
&lt;p&gt;Route handlers are still the right answer when something outside your application needs the data:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app/api/posts/route.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NextResponse &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'next/server'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; prisma &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/lib/prisma'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; prisma&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findMany&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; NextResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are on the Pages Router, the same client works inside &lt;code&gt;getServerSideProps&lt;/code&gt; and &lt;code&gt;pages/api/*&lt;/code&gt; handlers. Nothing about Prisma changes; only where you call it from.&lt;/p&gt;
&lt;h3 id=&quot;deploying&quot;&gt;Deploying&lt;/h3&gt;
&lt;p&gt;Prisma runs fine on &lt;a href=&quot;https://www.divio.com/&quot;&gt;Divio&lt;/a&gt;, &lt;a href=&quot;https://vercel.com/&quot;&gt;Vercel&lt;/a&gt; and &lt;a href=&quot;https://railway.app/&quot;&gt;Railway&lt;/a&gt;. I have written before about &lt;a href=&quot;/blog/hosting-on-divio-cloud/&quot;&gt;hosting on Divio&lt;/a&gt;, and the same rules apply wherever you land:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run &lt;code&gt;prisma generate&lt;/code&gt; in the build.&lt;/strong&gt; The generated client lives in a folder you have ignored, so the build has to create it. Put it in the build script or a &lt;code&gt;postinstall&lt;/code&gt;. Prisma 7 makes this less forgiving than before, because nothing else runs it for you any more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run &lt;code&gt;prisma migrate deploy&lt;/code&gt; in the pipeline, not &lt;code&gt;migrate dev&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;dev&lt;/code&gt; command is interactive and will happily offer to reset your database. &lt;code&gt;deploy&lt;/code&gt; applies pending migrations and nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch the connection pool on serverless.&lt;/strong&gt; Each function instance opens its own pool, and a managed Postgres box has a low ceiling. A pooler such as PgBouncer or your provider’s own is not optional once you have real traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the connection string in environment variables.&lt;/strong&gt; The CLI reads it through &lt;code&gt;prisma.config.ts&lt;/code&gt;, the application reads it in the singleton, and both need it set in every environment. Obvious, and still worth writing down, because the build and the runtime are often configured in two different places.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;moving-an-older-project-to-7&quot;&gt;Moving an older project to 7&lt;/h3&gt;
&lt;p&gt;If you have the 2024 version of this setup, the change is mechanical and took me less time than reading the upgrade guide did. In order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Upgrade the packages and add the two new ones: &lt;code&gt;npm install prisma@7 @prisma/client@7 @prisma/adapter-pg dotenv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In the schema, rename the generator to &lt;code&gt;prisma-client&lt;/code&gt;, give it an &lt;code&gt;output&lt;/code&gt;, and delete &lt;code&gt;url&lt;/code&gt; from the datasource. Create &lt;code&gt;prisma.config.ts&lt;/code&gt; with the URL in it.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;import { PrismaClient } from &amp;#39;@prisma/client&amp;#39;&lt;/code&gt; with the generated path, and pass the adapter to the constructor.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;prisma generate&lt;/code&gt;, then the type checker. The renamed import surfaces every file that constructed its own client, which is the audit you should have done anyway.&lt;/li&gt;
&lt;li&gt;If you used &lt;code&gt;prisma.$use()&lt;/code&gt; middleware, it is gone in 7. Client extensions do the same job with a different shape.&lt;/li&gt;
&lt;li&gt;Delete the &lt;code&gt;generate&lt;/code&gt; step you never had, then add it to the build, because now you need it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The generated client is ESM. In a Next.js project that costs nothing, since Next handles modules itself. In a plain Node script you either set &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; or tell the generator you want CommonJS, and the guide covers both.&lt;/p&gt;
&lt;h3 id=&quot;habits-worth-keeping&quot;&gt;Habits worth keeping&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Import &lt;code&gt;prisma&lt;/code&gt; from &lt;code&gt;lib/&lt;/code&gt;, never construct a client anywhere else.&lt;/li&gt;
&lt;li&gt;Keep it out of client components. If a file has &lt;code&gt;&amp;#39;use client&amp;#39;&lt;/code&gt; at the top, Prisma has no business in it, and the error you get if you try is not a helpful one.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;prisma generate&lt;/code&gt; after every migration, and put it in the build so production does not depend on your memory.&lt;/li&gt;
&lt;li&gt;Use TypeScript. Prisma with plain JavaScript works, but you have given up the reason to use Prisma.&lt;/li&gt;
&lt;li&gt;Read the generated migration before applying it to anything with data in it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-still-annoys-me&quot;&gt;What still annoys me&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;prisma generate&lt;/code&gt; is a step you have to remember in every environment, and Prisma 7 made it a step you have to remember twice, once after each migration and once in the build. Coming from Django, where migrations and the ORM live inside the framework, having a separate CLI that has to run at the right moment feels like something that should not be my problem.&lt;/p&gt;
&lt;p&gt;The move to a client written in TypeScript and a driver I already understood is a real improvement, and the config file is where the connection string always belonged. That is a small complaint against a schema that types itself. I will take the trade.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Monorepos]]></title><description><![CDATA[When a monorepo pays off and when it does not, plus a working Turborepo setup, the tasks config that replaced pipeline, and how Nx, Lerna and Bazel compare.]]></description><link>https://angelo.dini.dev/blog/monorepos/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/monorepos/</guid><pubDate>Thu, 31 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;one-repository-several-projects&quot;&gt;One repository, several projects&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;A monorepo is one repository holding several related projects. That is the whole idea. Everything else (the tooling, the caching, the arguments on the internet) is about making that arrangement survive contact with a real codebase.&lt;/p&gt;&lt;p&gt;The version of this problem I know best comes from django CMS, which lives across a lot of separate repositories. Core in one, the editor in another, half a dozen packages beside them. A change that touches more than one of those means a pull request in each, a release in each, and a version bump in the right order. That coordination cost is exactly what a monorepo removes, and it is why the pattern keeps coming back.&lt;/p&gt;
&lt;p&gt;It also introduces problems of its own, which is the less fun half of the post.&lt;/p&gt;
&lt;h3 id=&quot;the-shape-of-one&quot;&gt;The shape of one&lt;/h3&gt;
&lt;p&gt;Take a project with two applications and a shared library:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;my-monorepo/
├── apps/
│   ├── web/      &lt;span class=&quot;token comment&quot;&gt;# the main application&lt;/span&gt;
│   ├── docs/     &lt;span class=&quot;token comment&quot;&gt;# documentation site&lt;/span&gt;
│   └── api/      &lt;span class=&quot;token comment&quot;&gt;# serves the API&lt;/span&gt;
├── packages/
│   └── ui/       &lt;span class=&quot;token comment&quot;&gt;# shared UI components&lt;/span&gt;
├── package.json  &lt;span class=&quot;token comment&quot;&gt;# workspaces live here&lt;/span&gt;
└── turbo.json    &lt;span class=&quot;token comment&quot;&gt;# task configuration&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;apps/&lt;/code&gt; holds things you deploy, &lt;code&gt;packages/&lt;/code&gt; holds things you import. That split is a convention rather than a rule, but every tool in this space assumes it, so go along with it.&lt;/p&gt;
&lt;h3 id=&quot;what-you-get&quot;&gt;What you get&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Changes land in one commit.&lt;/strong&gt; Update a shared component and the applications that consume it get the new version immediately, in the same pull request, reviewed together. No publish step, no version bump, no waiting for the registry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependencies stop drifting.&lt;/strong&gt; One lockfile at the root means one version of React across everything. Version mismatches between projects simply stop being a category of bug you have to think about.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Builds get cached.&lt;/strong&gt; This is the part that actually changes how the day feels. If a package has not changed, its build output is reused rather than recomputed. On a cold clone that saves nothing; on the tenth build of the afternoon it saves most of your time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Refactoring across boundaries becomes possible.&lt;/strong&gt; Renaming an exported function and fixing every call site in the same change is only realistic when the call sites are in front of you.&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pull requests get noisy.&lt;/strong&gt; With everything in one place, the repository is busier, review ownership blurs, and the person reviewing your change may not know the package you touched. Codeowners files help. They do not fix it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clones get slow.&lt;/strong&gt; The repository only grows, and git history is the thing that grows fastest. &lt;code&gt;git sparse-checkout&lt;/code&gt; and shallow clones take the edge off, but a five-year-old monorepo is not a cheap thing to clone on hotel wifi.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blast radius goes up.&lt;/strong&gt; A shared package is shared. Change it carelessly and you have broken three applications rather than one, and you find out in CI rather than in a dependency bump you could have deferred.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Without caching, it is worse than what you had.&lt;/strong&gt; Running every test in the repository on every commit is the failure mode. If you adopt a monorepo and skip the task graph, you have taken all of the costs and none of the benefits.&lt;/p&gt;
&lt;h3 id=&quot;monorepo-or-single-repos&quot;&gt;Monorepo or single repos&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Monorepo or single repos&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Monorepo&lt;/th&gt;
&lt;th&gt;Single repos&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Code reuse&lt;/td&gt;
&lt;td&gt;Import directly from a workspace package&lt;/td&gt;
&lt;td&gt;Publish to a registry, or copy and paste and regret it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency management&lt;/td&gt;
&lt;td&gt;One lockfile, one version of everything&lt;/td&gt;
&lt;td&gt;Independent, and drifts apart quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build efficiency&lt;/td&gt;
&lt;td&gt;Fast with a task graph and cache, slow without one&lt;/td&gt;
&lt;td&gt;Fast per repo, no way to share work across them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release cadence&lt;/td&gt;
&lt;td&gt;Everything moves together, which is good until it is not&lt;/td&gt;
&lt;td&gt;Each project releases on its own schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding&lt;/td&gt;
&lt;td&gt;One clone, one install, one command to run everything&lt;/td&gt;
&lt;td&gt;Find the repos, clone them all, hope the readme is current&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The honest summary is that single repos are the right default for projects that do not share code, and a monorepo starts paying the moment two of them do.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-turborepo&quot;&gt;Setting up Turborepo&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://turbo.build/repo/docs&quot;&gt;Turborepo&lt;/a&gt; handles the task graph and the caching, and leaves package management to your package manager’s workspaces. It does less than the alternatives, which I like.&lt;/p&gt;
&lt;p&gt;Scaffold one and run it:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-turbo@latest
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-turborepo
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run dev&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generated repository gives you two applications and a shared package, which is enough to see the caching work. Build twice and watch the second run report &lt;code&gt;FULL TURBO&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;the-tasks-config&quot;&gt;The tasks config&lt;/h4&gt;
&lt;p&gt;At the root you get a &lt;code&gt;turbo.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://turbo.build/schema.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;tasks&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;dependsOn&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;^build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;outputs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.next/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;!.next/cache/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dist/**&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;dependsOn&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the key is &lt;code&gt;tasks&lt;/code&gt;. It was called &lt;code&gt;pipeline&lt;/code&gt; until Turborepo 2.0 landed in June 2024, and a lot of tutorials still have not caught up. If you inherit an older repository, &lt;code&gt;npx @turbo/codemod migrate&lt;/code&gt; renames it for you.&lt;/p&gt;
&lt;p&gt;Two things earn their keep here. &lt;code&gt;dependsOn: [&amp;quot;^build&amp;quot;]&lt;/code&gt; with the caret means “build my dependencies first”, so Turborepo works out the order rather than you maintaining it by hand. And &lt;code&gt;outputs&lt;/code&gt; tells the cache what to save; get that list wrong and your cache hits will restore nothing useful, which is a confusing way to spend an afternoon.&lt;/p&gt;
&lt;h4 id=&quot;sharing-code&quot;&gt;Sharing code&lt;/h4&gt;
&lt;p&gt;Give the shared package a name in its &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@my-monorepo/ui&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./index.js&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Export a component from it:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// packages/ui/Button.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Button&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And import it in an application as if it came from npm:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@my-monorepo/ui'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;HomePage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Click me&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No build step for the package, no publish, no version. The workspace resolution does the work.&lt;/p&gt;
&lt;h3 id=&quot;the-alternatives&quot;&gt;The alternatives&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://nx.dev/&quot;&gt;Nx&lt;/a&gt;&lt;/strong&gt; does considerably more: a dependency graph you can visualise, generators for new packages, and framework integrations for React, Angular and NestJS. If you want the tool to have opinions about your project layout, this is the one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://lerna.js.org/&quot;&gt;Lerna&lt;/a&gt;&lt;/strong&gt; is the old guard and still the best answer if your actual problem is publishing versioned packages to npm. It runs on Nx internally now, which tells you something about how that story ended.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://bazel.build/&quot;&gt;Bazel&lt;/a&gt;&lt;/strong&gt; comes from Google and handles codebases in many languages at once. The caching is excellent and the learning curve is genuinely steep. Reach for it when JavaScript is not the only thing in the repository.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://rushjs.io/&quot;&gt;Rush&lt;/a&gt;&lt;/strong&gt; comes from Microsoft, targets large TypeScript repositories, and takes dependency policy seriously in a way the others do not.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a JavaScript or TypeScript repository under a few dozen packages, Turborepo. Beyond that, or with more than one language involved, look at Nx and Bazel in that order.&lt;/p&gt;
&lt;h3 id=&quot;things-worth-doing-early&quot;&gt;Things worth doing early&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enable remote caching before you need it.&lt;/strong&gt; A cache that only lives on your laptop helps you. A shared one helps CI, which is where the build minutes actually go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Get &lt;code&gt;outputs&lt;/code&gt; right in every task.&lt;/strong&gt; It is the most common configuration mistake and the least obvious one, because nothing fails, it just stays slow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write conventions down.&lt;/strong&gt; &lt;a href=&quot;/blog/conventional-commits/&quot;&gt;Conventional commits&lt;/a&gt; help more in a monorepo than anywhere else, because a scope in the commit message tells you which package a change touched without opening it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate dependency updates.&lt;/strong&gt; &lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt; or &lt;a href=&quot;https://github.com/dependabot&quot;&gt;Dependabot&lt;/a&gt; grouped across the workspace, so one pull request covers the repository instead of twenty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assign ownership per package.&lt;/strong&gt; Not to gatekeep, but so that reviews land with someone who knows the code.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-i-would-draw-the-line&quot;&gt;Where I would draw the line&lt;/h3&gt;
&lt;p&gt;If you have one application, keep one repository. A monorepo is a solution to a coordination problem, and if you do not have the problem you are just adding a build tool.&lt;/p&gt;
&lt;p&gt;The moment there are two deployables sharing real code, move. Waiting until it hurts means migrating a repository with history, CI, and everyone’s muscle memory attached to it, and that is a much worse afternoon than setting it up on day one.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Headless CMS]]></title><description><![CDATA[What a headless CMS gives you, what it quietly takes away, and how Strapi, Payload, Storyblok and Contentful compare when you have to choose between them.]]></description><link>https://angelo.dini.dev/blog/headless-cms/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/headless-cms/</guid><pubDate>Sat, 28 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;everything-except-the-page&quot;&gt;Everything except the page&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I have been a core developer on django CMS since 2014, which means I have spent a long time inside a CMS that renders its own pages. So when someone tells me that going headless solves everything, my first question is what they plan to do about preview, URLs and redirects. Those parts rarely make it onto the landing page.&lt;/p&gt;&lt;p&gt;That is not an argument against headless. It is an argument for knowing what you trade away. The trade is usually worth it these days, and the tooling has got genuinely good. But it is a trade, and the comparison tables you find online are written by the people selling you the API.&lt;/p&gt;
&lt;p&gt;So: what a headless CMS actually is, how the pieces fit together, and where four of the common ones differ once you get past the marketing.&lt;/p&gt;
&lt;h3 id=&quot;what-headless-actually-means&quot;&gt;What “headless” actually means&lt;/h3&gt;
&lt;p&gt;A headless CMS stores and edits content but does not render it. The “head” is the presentation layer, and it has been cut off. What is left is a content repository plus an API over it, usually REST or GraphQL, often both.&lt;/p&gt;
&lt;p&gt;Compare that with WordPress or django CMS in their default setup. The same application that stores an article also decides what the page looks like, owns the URL it lives at, and produces the HTML. That coupling is where a traditional CMS gets its best features. It is also where it gets its worst constraints, because everything you want to do has to be expressed as a theme or a template or a plugin.&lt;/p&gt;
&lt;p&gt;The practical definition is short. You get structured content over an API, and you build the front end yourself. Everything else follows from those two facts.&lt;/p&gt;
&lt;h3 id=&quot;how-the-pieces-fit&quot;&gt;How the pieces fit&lt;/h3&gt;
&lt;p&gt;There are two moving parts.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;content repository&lt;/strong&gt; holds your models and your data. You define content types (an article has a title, a body, a hero image, a handful of tags), and editors fill them in through an admin interface.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;API layer&lt;/strong&gt; exposes that content. Your front end queries it and decides what to do with the response. That front end can be Next.js, Nuxt, an iOS app, or the screen in a shop window that nobody has updated since 2019.&lt;/p&gt;
&lt;p&gt;The workflow that falls out of this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Someone models the content types. This is the step teams rush through and then regret for two years.&lt;/li&gt;
&lt;li&gt;Editors create and edit entries in the admin.&lt;/li&gt;
&lt;li&gt;The front end fetches entries over REST or GraphQL, at build time, at request time, or some mix of the two.&lt;/li&gt;
&lt;li&gt;The front end renders. Nobody in the CMS knows or cares what it looked like.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Step four is the entire difference. It is also the source of every complaint I have ever heard about a headless setup.&lt;/p&gt;
&lt;h3 id=&quot;traditional-against-headless&quot;&gt;Traditional against headless&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Traditional against headless&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Headless CMS&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Traditional CMS&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Front-end flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Decoupled from the CMS, use any framework you like&lt;/td&gt;
&lt;td&gt;Coupled to the CMS, limited to themes and templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content delivery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Delivered over an API&lt;/td&gt;
&lt;td&gt;Rendered by the CMS itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-channel support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One content source feeds web, apps and devices&lt;/td&gt;
&lt;td&gt;Built primarily for websites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Editing experience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Form-based, preview has to be built&lt;/td&gt;
&lt;td&gt;In-context editing and preview come for free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customisation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Total control over presentation&lt;/td&gt;
&lt;td&gt;Customisation means fighting the template layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Read the first row again, though. “Use any framework you like” also means “you have to build it”. And total control over presentation means total responsibility for it. The table is honest, it just does not tell you who pays.&lt;/p&gt;
&lt;h3 id=&quot;what-you-give-up&quot;&gt;What you give up&lt;/h3&gt;
&lt;p&gt;Being specific about this, because the vendor comparison pages will not be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Preview.&lt;/strong&gt; In a coupled CMS, preview is free. The CMS renders the page, so it can render the draft too. Headless, preview is a feature you build. Storyblok and Contentful both ship preview APIs, and Storyblok’s visual editor is the main reason people choose it, but you still wire the thing up yourself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In-context editing.&lt;/strong&gt; Clicking a paragraph on the live page and typing into it is the single thing editors love most about django CMS. It is also the hardest behaviour to reproduce over an API, because the CMS has no idea where that paragraph ended up on screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing and redirects.&lt;/strong&gt; Someone has to own the mapping from a slug to a page. If the CMS does not, your front end does, and that includes the redirect you owe the world every time marketing renames a page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “who broke it” question.&lt;/strong&gt; With two systems there are two places for a deploy to go wrong, and two teams who are fairly sure it was not them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these are dealbreakers. All of them are work that was not on the estimate.&lt;/p&gt;
&lt;h3 id=&quot;the-four-you-will-be-asked-about&quot;&gt;The four you will be asked about&lt;/h3&gt;
&lt;h4 id=&quot;strapi&quot;&gt;Strapi&lt;/h4&gt;
&lt;p&gt;Strapi is the open-source default. Node.js, self-hosted, and you can customise most of it including the admin UI. Content types are defined through a builder that writes schema files to disk, which means they are in version control where they belong.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST and GraphQL out of the box.&lt;/li&gt;
&lt;li&gt;Roles and permissions for content teams.&lt;/li&gt;
&lt;li&gt;A plugin ecosystem, of varying quality.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start one with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-strapi@latest my-strapi-project&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pick it when you want control and are happy to run the thing yourself.&lt;/p&gt;
&lt;h4 id=&quot;payload-cms&quot;&gt;Payload CMS&lt;/h4&gt;
&lt;p&gt;Payload is also Node.js and also self-hosted, but aimed squarely at developers rather than at content teams. Configuration is code, and the generated client is type-safe end to end, which is a real advantage if your front end is TypeScript.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authentication and access control built in.&lt;/li&gt;
&lt;li&gt;Content modelling defined in config files, not a UI.&lt;/li&gt;
&lt;li&gt;Rich text editing with Markdown support.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-payload-app@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pick it when the developers outnumber the editors and the types matter more than the admin polish.&lt;/p&gt;
&lt;h4 id=&quot;storyblok&quot;&gt;Storyblok&lt;/h4&gt;
&lt;p&gt;Storyblok is hosted, and its selling point is the visual editor. Editors click on the rendered page and edit the component they clicked, which is the closest anyone has got to bringing coupled-CMS editing to a decoupled stack. You pay for that with a component model you have to design around.&lt;/p&gt;
&lt;p&gt;Pick it when the editors have opinions and will voice them.&lt;/p&gt;
&lt;h4 id=&quot;contentful&quot;&gt;Contentful&lt;/h4&gt;
&lt;p&gt;Contentful is the enterprise option. Hosted, API-first, REST and GraphQL, a CDN in front of everything, and asset management that holds up at scale. The interface is competent rather than lovable, and the pricing model rewards you for planning your content model before you start.&lt;/p&gt;
&lt;p&gt;Pick it when the content is distributed globally and someone else’s uptime is the point.&lt;/p&gt;
&lt;h3 id=&quot;where-django-cms-sits-in-all-this&quot;&gt;Where django CMS sits in all this&lt;/h3&gt;
&lt;p&gt;I would be leaving something out if I did not mention it. There has been an active effort this year to expose django CMS content over a REST API so it can be run headless while keeping the editing model that makes it worth using. The request for comments went up in March and &lt;code&gt;djangocms-rest&lt;/code&gt; has been moving since May. It is early, and I am watching it with an obvious personal bias.&lt;/p&gt;
&lt;p&gt;That is the shape of the interesting problem, I think. Not “headless or not”, but whether you can keep the editing experience of a coupled CMS while serving the content over an API. Nobody has fully solved that yet.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-actually-do&quot;&gt;What I would actually do&lt;/h3&gt;
&lt;p&gt;For a marketing site with a small team and a real editorial workload, I would still reach for a coupled CMS first, because preview and in-context editing are worth more than framework freedom to the people using it every day.&lt;/p&gt;
&lt;p&gt;For anything feeding more than one front end, headless, without much hesitation. Start with Strapi if you want to host it and Storyblok if you do not, and spend an afternoon on the content model before you write a single component. That afternoon is the cheapest one in the project.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Modular Frontend Development]]></title><description><![CDATA[shadcn/ui copies component source into your project instead of shipping a package. What that changes when you build a React and Tailwind design system.]]></description><link>https://angelo.dini.dev/blog/modular-frontend-with-shadcn/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/modular-frontend-with-shadcn/</guid><pubDate>Fri, 30 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;components-you-own&quot;&gt;Components you own&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every component library I have adopted has eventually turned into a fight. You want a button that is 2px shorter, the library disagrees, and you end up with a stylesheet full of specificity hacks aimed at code you cannot read. shadcn/ui takes a different route: it hands you the source and gets out of the way.&lt;/p&gt;&lt;p&gt;That sounds like a small distinction. It changes almost everything about how the library behaves in a real project, and it is worth being precise about what it actually is, because a lot of write-ups (including one I nearly published) describe it wrongly.&lt;/p&gt;
&lt;h3 id=&quot;what-shadcnui-actually-is&quot;&gt;What shadcn/ui actually is&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://ui.shadcn.com/&quot;&gt;shadcn/ui&lt;/a&gt; is not an npm dependency. There is no &lt;code&gt;import { Button } from &amp;#39;shadcn-ui&amp;#39;&lt;/code&gt;. You run a CLI, it copies TypeScript source files into your repository under &lt;code&gt;components/ui/&lt;/code&gt;, and from that point on the code is yours. You import from your own project:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/components/ui/button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is also not headless, and this is the description I see most often and which is simply wrong. The headless layer is &lt;a href=&quot;https://www.radix-ui.com/&quot;&gt;Radix UI&lt;/a&gt;, which provides the unstyled behaviour: focus trapping, keyboard navigation, ARIA wiring, portal management. shadcn/ui is a set of opinionated Tailwind styles sitting on top of those primitives, plus a distribution mechanism. The components arrive styled, themed with CSS variables, and looking like something you could ship.&lt;/p&gt;
&lt;p&gt;So the pitch is not “unstyled components you can style”. It is “a good default implementation, in your repository, that you can edit”. Those are very different products.&lt;/p&gt;
&lt;h3 id=&quot;why-modular-in-the-first-place&quot;&gt;Why modular in the first place&lt;/h3&gt;
&lt;p&gt;The argument for breaking an interface into small, single-purpose components is not new and I won’t belabour it. Reuse, obviously. Components you can debug in isolation. A consistent visual language, so the fourth developer to add a form doesn’t invent a fifth shade of grey.&lt;/p&gt;
&lt;p&gt;The one benefit I’d underline is that decoupling behaviour from presentation lets you change one without touching the other. A dropdown’s keyboard handling and a dropdown’s padding have no business living in the same commit, and most of the time in a hand-rolled component they do.&lt;/p&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting started&lt;/h3&gt;
&lt;p&gt;The CLI was rewritten this month and the package moved from &lt;code&gt;shadcn-ui&lt;/code&gt; to plain &lt;code&gt;shadcn&lt;/code&gt;. If you follow an older tutorial you will see &lt;code&gt;npx shadcn-ui@latest&lt;/code&gt;, which still resolves but is the deprecated name. Use:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx shadcn@latest init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it inside an existing project and it detects the framework. Next.js, Vite, Remix and Laravel all work out of the box now, which was the main point of the rewrite. Run it in an empty directory and it will offer to scaffold a new project first.&lt;/p&gt;
&lt;p&gt;The prompts ask about your style preset, base colour and whether you want CSS variables for theming. Answers land in &lt;code&gt;components.json&lt;/code&gt; at the root, which is the file the CLI reads on every subsequent command. It also records your import alias, so &lt;code&gt;@/components/ui&lt;/code&gt; resolves correctly.&lt;/p&gt;
&lt;p&gt;Then add a component:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx shadcn@latest &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; button&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That writes &lt;code&gt;components/ui/button.tsx&lt;/code&gt; into your project and installs whatever it depends on, here &lt;code&gt;@radix-ui/react-slot&lt;/code&gt; and &lt;code&gt;class-variance-authority&lt;/code&gt;. It leans on the &lt;code&gt;cn&lt;/code&gt; helper that &lt;code&gt;init&lt;/code&gt; already put in &lt;code&gt;lib/utils.ts&lt;/code&gt;. Open the file. It is about forty lines and you will understand all of them, which is not something I can say about most component libraries.&lt;/p&gt;
&lt;h3 id=&quot;extending-the-button&quot;&gt;Extending the button&lt;/h3&gt;
&lt;p&gt;Here is where I disagree with most of the shadcn/ui examples floating around, including the ones I wrote first. The obvious move is to override the styles at the call site:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// don't do this&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  Click me
&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. It also scatters your design decisions across every file that renders a button, and in six months nobody can answer “what colour is a primary button” without grepping.&lt;/p&gt;
&lt;p&gt;The generated &lt;code&gt;button.tsx&lt;/code&gt; defines its styles with &lt;code&gt;class-variance-authority&lt;/code&gt;. Add your variant there instead:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// components/ui/button.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; buttonVariants &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cva&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    variants&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      variant&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'bg-primary text-primary-foreground hover:bg-primary/90'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        destructive&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token string&quot;&gt;'bg-destructive text-destructive-foreground hover:bg-destructive/90'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        outline&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'border border-input bg-background hover:bg-accent'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        brand&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'bg-brand-500 text-white hover:bg-brand-600'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      size&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'h-10 px-4 py-2'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        sm&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'h-9 px-3'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        lg&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'h-11 px-8'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    defaultVariants&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      variant&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'default'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      size&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'default'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then the call site says what it means:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/components/ui/button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;MyButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;variant&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;brand&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;lg&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      Click me
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One vocabulary, one file, and a &lt;code&gt;variant&lt;/code&gt; prop that is typed, so a typo is a build error rather than a button with no background.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;className&lt;/code&gt; prop still exists for the genuine one-offs, and it merges properly rather than fighting the base classes, because &lt;code&gt;cn()&lt;/code&gt; runs &lt;code&gt;tailwind-merge&lt;/code&gt; under the hood. Conflicting utilities get resolved instead of stacking.&lt;/p&gt;
&lt;h4 id=&quot;adding-state&quot;&gt;Adding state&lt;/h4&gt;
&lt;p&gt;Nothing special is needed for behaviour. It’s your component:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Loader2 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'lucide-react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/components/ui/button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;LoadingButton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;loading&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setLoading&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;loading&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setLoading&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;loading &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Loader2&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;mr-2 h-4 w-4 animate-spin&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;loading &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Loading...'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Submit'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lucide-react&lt;/code&gt; is the icon set shadcn/ui defaults to, and it comes along with the components that need it.&lt;/p&gt;
&lt;h3 id=&quot;a-dialog-not-a-modal&quot;&gt;A dialog, not a modal&lt;/h3&gt;
&lt;p&gt;There is no &lt;code&gt;Modal&lt;/code&gt; component in shadcn/ui, and &lt;code&gt;npx shadcn@latest add modal&lt;/code&gt; will get you nothing. The component is called &lt;code&gt;Dialog&lt;/code&gt;, and it is composed from several exports rather than being a single element with an &lt;code&gt;isOpen&lt;/code&gt; prop.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx shadcn@latest &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; dialog&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token string&quot;&gt;'use client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Dialog&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  DialogContent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  DialogDescription&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  DialogHeader&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  DialogTitle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  DialogTrigger&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/components/ui/dialog'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Button &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@/components/ui/button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DeleteProjectDialog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Dialog&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogTrigger&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;asChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;variant&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;outline&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Delete project&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogTrigger&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogContent&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogHeader&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogTitle&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Are you absolutely sure?&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogTitle&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogDescription&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
            This permanently deletes the project and everything in it.
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogDescription&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogHeader&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;variant&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;destructive&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Delete&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DialogContent&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Dialog&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The open state lives inside &lt;code&gt;Dialog&lt;/code&gt; unless you lift it out with &lt;code&gt;open&lt;/code&gt; and &lt;code&gt;onOpenChange&lt;/code&gt;. &lt;code&gt;asChild&lt;/code&gt; is a Radix idea worth learning: it tells the trigger to render its child instead of its own element, so you get a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; with the trigger’s behaviour rather than a button nested inside a button.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DialogTitle&lt;/code&gt; and &lt;code&gt;DialogDescription&lt;/code&gt; aren’t decorative. Radix uses them to wire up &lt;code&gt;aria-labelledby&lt;/code&gt; and &lt;code&gt;aria-describedby&lt;/code&gt;. Leave the description out and the console tells you off, which is the correct behaviour and mildly startling the first time.&lt;/p&gt;
&lt;h3 id=&quot;what-this-buys-you-and-what-it-costs&quot;&gt;What this buys you, and what it costs&lt;/h3&gt;
&lt;p&gt;The good parts are real:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You own the code.&lt;/strong&gt; The button that is 2px too tall is a file you can edit. No &lt;code&gt;!important&lt;/code&gt;, no wrapper component whose only job is to undo a style.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility is handled by Radix.&lt;/strong&gt; Focus management, escape-to-close, scroll locking, keyboard navigation. Getting a dialog right by hand takes longer than people expect, and most hand-rolled ones are subtly wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It composes.&lt;/strong&gt; Because they’re plain React components with no framework of their own, you can wrap, extend and combine them without the library having anticipated it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It reads as Tailwind.&lt;/strong&gt; If your project already uses Tailwind, nothing new arrives in the stylesheet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cost is the same fact from the other side. You own the code, so you own the maintenance. There is no &lt;code&gt;npm update&lt;/code&gt; that brings you upstream fixes. When shadcn/ui improves a component, you go and look at the diff yourself, and if you have edited that file, you merge it by hand. For a design system you intend to keep for years, that is fine and arguably what you wanted. For a team that wants to install a dependency and forget about it, it is the wrong tool and will feel like one within a quarter.&lt;/p&gt;
&lt;h3 id=&quot;growing-it-into-a-design-system&quot;&gt;Growing it into a design system&lt;/h3&gt;
&lt;p&gt;The natural progression is to stop treating &lt;code&gt;components/ui/&lt;/code&gt; as vendored code and start treating it as your library. Add your variants to the generated files. Add your own components alongside them using the same &lt;code&gt;cva&lt;/code&gt; pattern so the shape stays consistent. Point your Tailwind theme at the &lt;a href=&quot;/blog/css-custom-properties/&quot;&gt;CSS custom properties&lt;/a&gt; in &lt;code&gt;globals.css&lt;/code&gt; and change the whole palette from one place.&lt;/p&gt;
&lt;p&gt;At that point the question of whether shadcn/ui is a dependency stops making sense, which I think is the intent.&lt;/p&gt;
&lt;p&gt;I have not yet used the custom registry feature that arrived with the CLI rewrite, where you serve your own components over a URL and &lt;code&gt;add&lt;/code&gt; them the same way. That looks like the missing piece for sharing a design system across several repositories without publishing a package, and it is what I want to try next 🙂&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[CSS Container Queries]]></title><description><![CDATA[How CSS container queries let a component respond to its own box instead of the viewport, with the syntax, the units, the gotchas and browser support.]]></description><link>https://angelo.dini.dev/blog/css-container-queries/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/css-container-queries/</guid><pubDate>Wed, 31 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;asking-the-box-not-the-window&quot;&gt;Asking the box, not the window&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;A card component does not care how wide your monitor is. It cares how much room it has been given. Media queries have never been able to express that, and every workaround I have written for it involved JavaScript measuring a div and putting a class on it. Container queries finally make it a stylesheet problem.&lt;/p&gt;&lt;p&gt;I ended my post on &lt;a href=&quot;/blog/css-techniques/&quot;&gt;modern CSS techniques&lt;/a&gt; by complaining about exactly this gap, so it seems fair to spend a post on how it closes. What follows is the syntax, the units, the parts that behave unexpectedly, and where browser support actually stands.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;Put a product card in a three-column grid on a desktop and it wants a horizontal layout: image left, text right. Put the same card in a narrow sidebar and it wants to stack. With media queries, the card cannot know which situation it is in. All it knows is the width of the window, which tells you nothing about the width of the column the card landed in.&lt;/p&gt;
&lt;p&gt;So you end up with variant classes. &lt;code&gt;.card--wide&lt;/code&gt;, &lt;code&gt;.card--compact&lt;/code&gt;, &lt;code&gt;.card--sidebar&lt;/code&gt;, each one a promise about where the component will be used, and each one broken the first time somebody uses it somewhere new. Container queries remove the need for the promise.&lt;/p&gt;
&lt;h3 id=&quot;the-syntax&quot;&gt;The syntax&lt;/h3&gt;
&lt;p&gt;Two steps. Declare an element as a query container, then write rules that respond to its size.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;container-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline-size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;declaring-a-container&quot;&gt;Declaring a container&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;container-type&lt;/code&gt; takes three values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inline-size&lt;/code&gt; queries the inline dimension only, which in a horizontal writing mode means the width. This is what you want roughly nine times out of ten.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt; queries both dimensions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;normal&lt;/code&gt; is the default and makes the element a container for style queries only, not size queries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is also &lt;code&gt;container-name&lt;/code&gt;, and a &lt;code&gt;container&lt;/code&gt; shorthand that sets both:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.card-wrapper&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; card / inline-size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;querying-it&quot;&gt;Querying it&lt;/h4&gt;
&lt;p&gt;Inside an &lt;code&gt;@container&lt;/code&gt; block, the rules apply to descendants of the nearest ancestor container:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you gave the container a name, address it by name and the query stops depending on which container happens to be closest:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; card &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 500px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 120px 1fr&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have come round to naming containers by default. Unnamed queries work fine until somebody wraps your component in another container, at which point the styles start resolving against a box you have never heard of.&lt;/p&gt;
&lt;h4 id=&quot;both-dimensions&quot;&gt;Both dimensions&lt;/h4&gt;
&lt;p&gt;If you set &lt;code&gt;container-type: size&lt;/code&gt;, you can query height as well:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;container-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 400px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightblue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2px solid lightcoral&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the explicit &lt;code&gt;height&lt;/code&gt; on the container. It is not decoration, and I explain why in the gotchas below.&lt;/p&gt;
&lt;h4 id=&quot;nesting&quot;&gt;Nesting&lt;/h4&gt;
&lt;p&gt;Containers nest, and each query resolves against the nearest matching ancestor:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.outer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; layout / inline-size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.inner&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; panel / inline-size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; panel &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightgreen&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; layout &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 900px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightcoral&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With names, that reads unambiguously. Without them, both blocks would resolve against &lt;code&gt;.inner&lt;/code&gt; for any &lt;code&gt;.item&lt;/code&gt; inside it, and the second rule would never fire the way you expected.&lt;/p&gt;
&lt;h3 id=&quot;container-queries-against-media-queries&quot;&gt;Container queries against media queries&lt;/h3&gt;
&lt;p class=&quot;tbl-hint&quot; aria-hidden=&quot;true&quot;&gt;Swipe the table sideways&lt;/p&gt;&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Container queries against media queries&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Media queries&lt;/th&gt;
&lt;th&gt;Container queries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Responds to&lt;/td&gt;
&lt;td&gt;Viewport and device features&lt;/td&gt;
&lt;td&gt;Size of a specific ancestor element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;The whole document&lt;/td&gt;
&lt;td&gt;Descendants of that container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for&lt;/td&gt;
&lt;td&gt;Page-level layout shifts, print styles, &lt;code&gt;prefers-reduced-motion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Component-level layout, design systems, anything reused in more than one slot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component portability&lt;/td&gt;
&lt;td&gt;The component must know where it lives&lt;/td&gt;
&lt;td&gt;The component adapts wherever you put it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;They are not competitors. I still use media queries for the page skeleton and for things that genuinely are properties of the device. Everything inside a component is a container query now.&lt;/p&gt;
&lt;h3 id=&quot;the-units&quot;&gt;The units&lt;/h3&gt;
&lt;p&gt;Container queries bring their own length units, all relative to the query container:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cqw&lt;/code&gt; is 1% of the container’s width&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cqh&lt;/code&gt; is 1% of the container’s height&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cqi&lt;/code&gt; is 1% of the container’s inline size&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cqb&lt;/code&gt; is 1% of the container’s block size&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cqmin&lt;/code&gt; is the smaller of &lt;code&gt;cqi&lt;/code&gt; and &lt;code&gt;cqb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cqmax&lt;/code&gt; is the larger of the two&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prefer &lt;code&gt;cqi&lt;/code&gt; and &lt;code&gt;cqb&lt;/code&gt; over &lt;code&gt;cqw&lt;/code&gt; and &lt;code&gt;cqh&lt;/code&gt; if you care about writing modes; they are the logical versions of the same idea.&lt;/p&gt;
&lt;p&gt;Where these earn their place is in the declarations, not in the condition. Type that scales with the component is the obvious use:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.card&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; card / inline-size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; card &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 400px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.card__title&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1.5rem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1.2rem + 1.5cqi&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token selector&quot;&gt;.card__body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 4cqi&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using container units inside the &lt;code&gt;@container&lt;/code&gt; condition itself is legal but confusing, because they resolve against the &lt;em&gt;parent&lt;/em&gt; container rather than the one you are querying. I would avoid it. &lt;code&gt;px&lt;/code&gt; and &lt;code&gt;rem&lt;/code&gt; in the condition, &lt;code&gt;cqi&lt;/code&gt; in the rules, is a boundary that has never surprised me.&lt;/p&gt;
&lt;h3 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h3&gt;
&lt;p&gt;Four things bit me while I was working this out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An element cannot query itself.&lt;/strong&gt; The rules inside an &lt;code&gt;@container&lt;/code&gt; block apply to descendants of the container, never to the container element. If you want a card to restyle itself, you need a wrapper element to be the container, which is a slightly annoying extra div.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;container-type: size&lt;/code&gt; collapses your element.&lt;/strong&gt; Size containment in the block direction means the element’s height no longer depends on its content. Without an explicit height it computes to zero and everything inside vanishes. This looks catastrophic and takes ten minutes to work out the first time. Use &lt;code&gt;inline-size&lt;/code&gt; unless you genuinely need to query height.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;inline-size&lt;/code&gt; containment cuts the other way too.&lt;/strong&gt; The container’s own width can no longer be determined by its contents, so a shrink-to-fit element such as a floated box or an inline-block will not size itself as it did before. In a normal block or grid layout you will never notice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Query containers cost something.&lt;/strong&gt; Every element with a &lt;code&gt;container-type&lt;/code&gt; is an element the engine has to watch for size changes. Declaring one on a wrapper is nothing; declaring one on every list item in a table of two thousand rows is a decision you will feel.&lt;/p&gt;
&lt;h3 id=&quot;browser-support&quot;&gt;Browser support&lt;/h3&gt;
&lt;p&gt;Size container queries have been in stable browsers for a while now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chrome and Edge since version 105, from August 2022&lt;/li&gt;
&lt;li&gt;Safari since 16.0, from September 2022&lt;/li&gt;
&lt;li&gt;Firefox since 110, in February 2023&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That puts them in every evergreen browser, and has done for well over a year. In practice you can use them without a polyfill, provided your traffic isn’t heavy on Safari 15 or some old Chromium-based embedded browser you inherited.&lt;/p&gt;
&lt;p&gt;Style queries, which test a custom property’s value rather than a size, are the newer half of the specification. Chrome and Firefox have shipped them for custom properties; Safari had not at the time of writing. I’d treat them as a progressive enhancement for now.&lt;/p&gt;
&lt;p&gt;If you do need a fallback, &lt;code&gt;@supports&lt;/code&gt; handles it cleanly:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@supports&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;container-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline-size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;container-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline-size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;min-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightblue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@supports&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;container-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; inline-size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightgray&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Write the unsupported case as your baseline and layer the container query on top, rather than the other way round. Old browsers get a single-column layout that works, new ones get the adaptive version.&lt;/p&gt;
&lt;h3 id=&quot;where-ive-got-to&quot;&gt;Where I’ve got to&lt;/h3&gt;
&lt;p&gt;I have started removing size modifier classes from components as I touch them and replacing them with a named container plus two or three breakpoints. It is less code and the components stop lying about where they can be used.&lt;/p&gt;
&lt;p&gt;The wrapper-element requirement still bothers me. Every component that wants to respond to its own width needs an extra div whose only job is to be a container, and in a deeply nested React tree that adds up. There’s talk of allowing an element to query itself in a future revision of the specification. I’ll believe it when I can ship it 🙂&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Modern CSS Techniques]]></title><description><![CDATA[A tour of CSS Grid, subgrid, Flexbox, scroll snap, filters, transitions and shapes, with the syntax that trips people up and what I would reach for first.]]></description><link>https://angelo.dini.dev/blog/css-techniques/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/css-techniques/</guid><pubDate>Sat, 22 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;grid-flexbox-and-the-things-around-them&quot;&gt;Grid, Flexbox and the things around them&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;CSS spent about a decade being the part of the stack people apologised for. Then Flexbox landed, then Grid, then everything else arrived so quickly that I keep meeting developers who are still writing 2016 CSS out of habit. This is a tour of what I actually use.&lt;/p&gt;&lt;p&gt;None of this is exotic. All of it works in every evergreen browser as I write this. The point of the post is less “here is a new feature” and more “here is the syntax that catches people out”, because most of these tools are one small misunderstanding away from feeling broken.&lt;/p&gt;
&lt;h3 id=&quot;css-grid&quot;&gt;CSS Grid&lt;/h3&gt;
&lt;p&gt;Grid is a two-dimensional layout system. Rows and columns at the same time, which is the thing Flexbox deliberately doesn’t do.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.grid&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;3&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1fr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; auto&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.grid__item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #f0f0f0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px solid #ccc&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three equal columns, each taking one fraction of the free space. &lt;code&gt;repeat()&lt;/code&gt; saves you typing &lt;code&gt;1fr 1fr 1fr&lt;/code&gt;, and &lt;code&gt;gap&lt;/code&gt; puts space between the tracks without the negative-margin nonsense we all used to write.&lt;/p&gt;
&lt;p&gt;Once the tracks exist, you can place items across them by line number:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.grid&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;3&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1fr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100px 200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.grid__item1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-column&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1 / 3&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* span two columns */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-row&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1 / 2&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;/* occupy the first row */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.grid__item2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-column&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 3 / 4&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-row&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1 / 3&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;/* span both rows */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The numbers are grid &lt;em&gt;lines&lt;/em&gt;, not tracks. A three-column grid has four lines. That off-by-one is responsible for a genuinely embarrassing share of the time I have spent debugging layouts.&lt;/p&gt;
&lt;h4 id=&quot;template-areas&quot;&gt;Template areas&lt;/h4&gt;
&lt;p&gt;Named areas are the feature I’d show someone first, because they make a layout readable by people who don’t know CSS.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-areas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;header header&quot;&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;sidebar content&quot;&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;footer footer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100px 1fr 50px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px 1fr&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.header&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-area&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; header&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.sidebar&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-area&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sidebar&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.content&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-area&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.footer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-area&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; footer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the page in the stylesheet. Rearranging it inside a media query is a matter of rewriting those three strings, which is about as close to pleasant as responsive layout gets.&lt;/p&gt;
&lt;h4 id=&quot;subgrid&quot;&gt;Subgrid&lt;/h4&gt;
&lt;p&gt;Subgrid lets a nested grid use its parent’s tracks instead of defining its own. The classic use is a row of cards whose headings, bodies and footers should line up across the row even though the text lengths differ.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.cards&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;3&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1fr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;3&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; auto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.card&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-row&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; span 3&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-rows&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; subgrid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things to get right, because I got both of them wrong the first time. There is no &lt;code&gt;display: subgrid&lt;/code&gt;; the child is a normal &lt;code&gt;display: grid&lt;/code&gt; and &lt;code&gt;subgrid&lt;/code&gt; is a value of &lt;code&gt;grid-template-rows&lt;/code&gt; or &lt;code&gt;grid-template-columns&lt;/code&gt;. And the child has to actually span the parent tracks it wants to adopt, which is what &lt;code&gt;grid-row: span 3&lt;/code&gt; is doing. Miss that and the subgrid silently behaves like an ordinary grid.&lt;/p&gt;
&lt;p&gt;Firefox has had subgrid since 2019, Safari since 16, and Chrome and Edge since 117 in late 2023. So it is finally usable without a fallback.&lt;/p&gt;
&lt;h3 id=&quot;flexbox&quot;&gt;Flexbox&lt;/h3&gt;
&lt;p&gt;Flexbox is one-dimensional. Items in a row, or items in a column, with control over how the leftover space is distributed.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;justify-content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; space-between&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;align-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #f0f0f0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px solid #ccc&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;justify-content: space-between&lt;/code&gt; spreads the items along the main axis, &lt;code&gt;align-items: center&lt;/code&gt; centres them on the cross axis. The old joke about vertical centring being impossible in CSS died right here.&lt;/p&gt;
&lt;p&gt;For sizing, there are &lt;code&gt;flex-grow&lt;/code&gt;, &lt;code&gt;flex-shrink&lt;/code&gt; and &lt;code&gt;flex-basis&lt;/code&gt;, and in practice you almost always want the shorthand:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flex: 1&lt;/code&gt; expands to &lt;code&gt;flex-grow: 1; flex-shrink: 1; flex-basis: 0%&lt;/code&gt;. That last value is the one people miss. A basis of &lt;code&gt;0%&lt;/code&gt; means the item’s own content width is ignored when space is divided, so every item ends up the same size. If you want the content to influence the result, you want &lt;code&gt;flex: auto&lt;/code&gt;, which uses &lt;code&gt;flex-basis: auto&lt;/code&gt; instead. I have watched this exact distinction cause a two-hour argument about a navigation bar.&lt;/p&gt;
&lt;h3 id=&quot;scroll-snap&quot;&gt;Scroll snap&lt;/h3&gt;
&lt;p&gt;Scroll snap turns a scrollable area into something that settles on defined points rather than wherever the momentum happened to stop.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;scroll-snap-type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; x mandatory&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;overflow-x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; scroll&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;scroll-snap-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0 0 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a carousel, in seven declarations, with no JavaScript and with native keyboard and touch behaviour intact. Use &lt;code&gt;proximity&lt;/code&gt; rather than &lt;code&gt;mandatory&lt;/code&gt; if the content varies in size, because &lt;code&gt;mandatory&lt;/code&gt; will fight the user on a long item.&lt;/p&gt;
&lt;h3 id=&quot;filters&quot;&gt;Filters&lt;/h3&gt;
&lt;p&gt;Filters apply graphical effects without touching the source image.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.image&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;blur&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;5px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;contrast&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;1.2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They compose left to right, and they are cheap enough to animate on the compositor in most cases. The one to be careful with is &lt;code&gt;filter&lt;/code&gt; on a parent element, because it creates a containing block for descendants with &lt;code&gt;position: fixed&lt;/code&gt;, which is a fun afternoon when you don’t know it.&lt;/p&gt;
&lt;h3 id=&quot;transitions-and-animations&quot;&gt;Transitions and animations&lt;/h3&gt;
&lt;h4 id=&quot;transitions&quot;&gt;Transitions&lt;/h4&gt;
&lt;p&gt;A transition interpolates a property when its value changes, typically on hover or focus.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.button&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #3498db&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; background-color 0.3s ease&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.button:hover&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #2c3e50&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Name the properties you’re transitioning. &lt;code&gt;transition: all&lt;/code&gt; is tempting and will one day animate something you did not intend, usually a layout property, usually on a page with a hundred of these elements.&lt;/p&gt;
&lt;h4 id=&quot;animations&quot;&gt;Animations&lt;/h4&gt;
&lt;p&gt;Animations run from keyframes, on their own schedule, without needing a state change to trigger them.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@keyframes&lt;/span&gt; slideIn&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;-100%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.element&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; slideIn 0.5s forwards&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The element slides in from the left over half a second. &lt;code&gt;forwards&lt;/code&gt; is an &lt;code&gt;animation-fill-mode&lt;/code&gt; value and keeps the final keyframe applied after the animation ends; without it the element snaps back to its original position, which looks like a bug and is reported as one.&lt;/p&gt;
&lt;p&gt;Wrap decorative animation in &lt;code&gt;@media (prefers-reduced-motion: reduce)&lt;/code&gt; and give people a still version. It takes four lines.&lt;/p&gt;
&lt;h3 id=&quot;beyond-the-layout-systems&quot;&gt;Beyond the layout systems&lt;/h3&gt;
&lt;h4 id=&quot;custom-properties&quot;&gt;Custom properties&lt;/h4&gt;
&lt;p&gt;I covered &lt;a href=&quot;/blog/css-custom-properties/&quot;&gt;CSS variables&lt;/a&gt; properly in an earlier post, so here is the short version.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--primary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #3498db&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--primary&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--padding&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The thing that makes them more than a preprocessor variable is that they live in the cascade. You can redefine &lt;code&gt;--primary&lt;/code&gt; inside a &lt;code&gt;.dark&lt;/code&gt; class or a media query and everything downstream updates, at runtime, with no rebuild.&lt;/p&gt;
&lt;h4 id=&quot;shapes&quot;&gt;Shapes&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;shape-outside&lt;/code&gt; makes text wrap around a non-rectangular path.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.shape&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; left&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;shape-outside&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;circle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;50%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #f0f0f0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;clip-path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;circle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;50%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;shape-outside&lt;/code&gt; controls how surrounding content flows, &lt;code&gt;clip-path&lt;/code&gt; controls what you can see of the element itself. You usually want both, and you always need the float, because &lt;code&gt;shape-outside&lt;/code&gt; only applies to floated elements. That restriction is why this feature never quite caught on.&lt;/p&gt;
&lt;h3 id=&quot;combining-them&quot;&gt;Combining them&lt;/h3&gt;
&lt;p&gt;Grid for the page, Flexbox for the components inside it. That is the division of labour I use almost every time.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.grid-container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;grid-template-columns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1fr 2fr&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;gap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.flex-item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;flex-direction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; column&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;justify-content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;align-items&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; start&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Grid decides where the boxes go, Flexbox decides how the contents of each box arrange themselves. Trying to do the whole page in Flexbox is possible and produces stylesheets full of percentage widths that nobody wants to inherit.&lt;/p&gt;
&lt;h3 id=&quot;what-i-still-find-annoying&quot;&gt;What I still find annoying&lt;/h3&gt;
&lt;p&gt;Sizing an element based on its container rather than the viewport is still awkward, and every workaround I’ve used has involved either JavaScript or a resize observer pretending to be CSS. That is the gap container queries are supposed to close, and I want to spend some proper time with them before I write anything about them.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Testing Strategies in React]]></title><description><![CDATA[Unit, integration, end-to-end and screenshot testing in React, with working examples using Jest, Vitest, React Testing Library, Playwright and Puppeteer.]]></description><link>https://angelo.dini.dev/blog/testing-strategies-in-react/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/testing-strategies-in-react/</guid><pubDate>Sat, 25 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;four-kinds-of-test-and-when-each-earns-its-keep&quot;&gt;Four kinds of test, and when each earns its keep&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Nobody sets out to write tests. You write them because the third time you break the same checkout flow, someone in the team stops finding it funny. The question is never really whether to test, it's which kind of test to reach for, because they cost wildly different amounts to write and to keep alive.&lt;/p&gt;&lt;p&gt;This post walks through the four layers I use in React projects: unit, integration, end-to-end and screenshot tests. Each section has a working example. I’ve kept the examples small on purpose, because the interesting part is not the syntax, it’s knowing which layer a given bug would have been caught by.&lt;/p&gt;
&lt;h3 id=&quot;what-tests-actually-buy-you&quot;&gt;What tests actually buy you&lt;/h3&gt;
&lt;p&gt;Four things, roughly in order of how much I value them.&lt;/p&gt;
&lt;p&gt;They catch bugs while the code is still in your head. Fixing something the same afternoon you wrote it is trivially cheap. Fixing it after a release involves a support ticket, a bisect, and someone’s evening.&lt;/p&gt;
&lt;p&gt;They let you refactor. This is the one I’d defend hardest. A component with decent tests can be rewritten without ceremony. A component without them accretes workarounds because nobody dares touch the middle of it.&lt;/p&gt;
&lt;p&gt;They document intent. A test named &lt;code&gt;submits the trimmed value&lt;/code&gt; tells you something the implementation doesn’t.&lt;/p&gt;
&lt;p&gt;And they force a small amount of design discipline, because code that’s painful to test is usually code with too many responsibilities.&lt;/p&gt;
&lt;h3 id=&quot;unit-tests&quot;&gt;Unit tests&lt;/h3&gt;
&lt;p&gt;A unit test exercises one component or one function on its own. In React that usually means rendering a single component and poking at it.&lt;/p&gt;
&lt;p&gt;Two tools do the work. &lt;strong&gt;Jest&lt;/strong&gt; is the test runner: assertions, mocks, spies, snapshots. It came out of Facebook and moved to the OpenJS Foundation in 2022, which is worth knowing mainly because the governance change made the release cadence a lot calmer. &lt;strong&gt;React Testing Library&lt;/strong&gt; is the rendering layer, and its whole philosophy is that you should query the DOM the way a user would, by visible text and accessible role, rather than by class name or component internals.&lt;/p&gt;
&lt;p&gt;Here’s a button:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// button.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Button&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; onClick&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;onClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Button&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And its test:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// button.test.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; render&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fireEvent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@testing-library/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Button &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Button displays the correct text and handles click events'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleClick &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; jest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; getByText &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Click me&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Button&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; button &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Click me'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  fireEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;button&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;button&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeInTheDocument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveBeenCalledTimes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;toBeInTheDocument&lt;/code&gt; comes from &lt;code&gt;@testing-library/jest-dom&lt;/code&gt;, not from Jest itself. It’s in the setup file of every project I’ve worked on, so it’s easy to forget it needs installing at all.&lt;/p&gt;
&lt;p&gt;If your project is on Vite, use &lt;strong&gt;Vitest&lt;/strong&gt; instead of Jest. The API is close enough that most test files port over untouched, and it reuses your existing Vite config rather than asking you to maintain a parallel Babel setup. I went through that migration in &lt;a href=&quot;/blog/more-about-vite/&quot;&gt;more about Vite&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;integration-tests&quot;&gt;Integration tests&lt;/h3&gt;
&lt;p&gt;Integration tests check that components work together. Same tools, wider scope, which is why the boundary between “unit” and “integration” in a React codebase is mostly a matter of how much you rendered.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// form.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Form&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; onSubmit &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;inputValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setInputValue&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleSubmit&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;htmlFor&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Your name&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;inputValue&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setInputValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Submit&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Form&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// form.test.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; render&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fireEvent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@testing-library/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Form &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./form'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Form submits the correct value'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleSubmit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; jest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; getByLabelText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; getByText &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Form&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleSubmit&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; input &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getByLabelText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Your name'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; button &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Submit'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  fireEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'test value'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  fireEvent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;button&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handleSubmit&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'test value'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;htmlFor&lt;/code&gt;/&lt;code&gt;id&lt;/code&gt; pairing in the component isn’t decoration. &lt;code&gt;getByLabelText&lt;/code&gt; resolves through the accessibility tree, so an input without an associated label simply cannot be found by that query. Which is a nice property: if the test can’t find your field, a screen reader probably can’t either.&lt;/p&gt;
&lt;h3 id=&quot;end-to-end-tests&quot;&gt;End-to-end tests&lt;/h3&gt;
&lt;p&gt;End-to-end tests drive a real browser against a running application. They’re the slowest and the most brittle layer, and they’re also the only one that will ever catch a broken redirect or a misconfigured CSP header.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Playwright&lt;/strong&gt; is what I use. It’s from Microsoft, runs against Chromium, Firefox and WebKit, and its auto-waiting is good enough that you can mostly stop writing explicit sleeps.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// form.spec.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; test&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; expect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'@playwright/test'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Form submits the correct value'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; page &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'http://localhost:3000/form'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByLabel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Your name'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'test value'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByRole&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'button'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Submit'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByRole&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'Submission successful: test value'&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to watch: &lt;code&gt;toHaveText&lt;/code&gt; is a locator assertion, not a page assertion. &lt;code&gt;expect(page)&lt;/code&gt; only supports a handful of checks such as &lt;code&gt;toHaveTitle&lt;/code&gt; and &lt;code&gt;toHaveURL&lt;/code&gt;. Asserting text means pointing at an element first, as above. I’ve seen that mistake in more than one blog post, including an earlier draft of this one.&lt;/p&gt;
&lt;p&gt;Keep the number of these tests small and deliberate. Two or three journeys that would embarrass you if they broke in production, not a mirror of your unit test suite.&lt;/p&gt;
&lt;h3 id=&quot;screenshot-tests&quot;&gt;Screenshot tests&lt;/h3&gt;
&lt;p&gt;Screenshot testing, or visual regression testing, renders the app, takes a picture, and compares it against a stored baseline. It catches the class of bug no assertion will: the stylesheet change that quietly shifted every card in the grid by four pixels.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Puppeteer&lt;/strong&gt; is a Node library that drives headless Chrome, and it pairs with &lt;strong&gt;jest-image-snapshot&lt;/strong&gt; for the comparison. &lt;strong&gt;Storybook&lt;/strong&gt; is the other half of a sensible setup, because it lets you capture components in isolation and in specific states rather than fighting a full application into position.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// screenshot.test.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; puppeteer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'puppeteer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toMatchImageSnapshot &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'jest-image-snapshot'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

expect&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;extend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toMatchImageSnapshot &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Visual Regression Testing'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;beforeAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    browser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;launch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    page &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;afterAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'should match the previous screenshot'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; screenshot &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;screenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;screenshot&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMatchImageSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;expect.extend&lt;/code&gt; line is not optional. &lt;code&gt;toMatchImageSnapshot&lt;/code&gt; is not a Jest built-in, and without registering it you get a confusing “is not a function” error rather than anything helpful.&lt;/p&gt;
&lt;p&gt;Be warned that this layer is where flakiness lives. Font rendering differs between macOS and Linux, animations land mid-frame, and a caret blinking in a text input will fail a test at random. The fix for all three is the same: disable animations, hide the caret, and run the whole thing in a container so the rendering environment is identical everywhere. I went through that setup properly in &lt;a href=&quot;/blog/screenshot-testing/&quot;&gt;screenshot testing with React&lt;/a&gt;, including the Docker side of it.&lt;/p&gt;
&lt;h3 id=&quot;what-i-actually-do&quot;&gt;What I actually do&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Write tests that would fail for a reason you care about. A test asserting that a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; has the class you just gave it is theatre.&lt;/li&gt;
&lt;li&gt;Name them as sentences. &lt;code&gt;renders the empty state when the list is empty&lt;/code&gt; beats &lt;code&gt;test 3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep them independent. A suite where test 4 relies on test 2 having run is a suite that will eventually fail in a random order and take a morning to diagnose.&lt;/li&gt;
&lt;li&gt;Mock at the network boundary, not inside your components. Mocking a component’s internals bakes today’s implementation into the test.&lt;/li&gt;
&lt;li&gt;Run everything in CI, and run the screenshot tests in the same container image locally that CI uses. Otherwise you spend your life regenerating baselines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I had to keep one layer, it would be the integration tests. They catch the most per line of test code, they survive refactoring, and they don’t need a browser farm.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;/h3&gt;
&lt;p&gt;The official docs are all good, which is not something you can say about every ecosystem: &lt;a href=&quot;https://jestjs.io/&quot;&gt;Jest&lt;/a&gt;, &lt;a href=&quot;https://vitest.dev/&quot;&gt;Vitest&lt;/a&gt;, &lt;a href=&quot;https://testing-library.com/&quot;&gt;React Testing Library&lt;/a&gt;, &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt;, &lt;a href=&quot;https://pptr.dev/&quot;&gt;Puppeteer&lt;/a&gt; and &lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next on my list is trying Playwright’s component testing mode, which sits somewhere between the integration and end-to-end layers and might make one of them redundant. It was still experimental last I checked, so no promises 🙂&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Introduction to Docusaurus]]></title><description><![CDATA[Setting up a Docusaurus documentation site, running the whole thing in Docker with Compose, and where MkDocs, Sphinx or VuePress make more sense instead.]]></description><link>https://angelo.dini.dev/blog/docusaurus/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/docusaurus/</guid><pubDate>Tue, 30 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;dinosaurs-taking-over-documentation&quot;&gt;Dinosaurs taking over documentation&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every project I work on eventually sprouts a docs folder, and every docs folder eventually raises the same question: where does this get published, and who has to maintain the publishing? Docusaurus is Meta's answer to that, and if your team already writes React, it is a comfortable one.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://docusaurus.io/&quot;&gt;Docusaurus&lt;/a&gt; is a static site generator built on React, aimed squarely at project documentation rather than at blogs or marketing sites. You write Markdown, it gives you a sidebar, versioning, an internationalisation pipeline and a theme that already looks like documentation. The part I actually care about is MDX: a Markdown file can import a React component and render it inline, so a page can contain a working demo instead of a picture of one.&lt;/p&gt;
&lt;p&gt;That sounds like a gimmick until you have maintained the docs for a component library. Screenshots go stale quietly and nobody notices for a year. A live component either renders or breaks the build.&lt;/p&gt;
&lt;h3 id=&quot;getting-it-running&quot;&gt;Getting it running&lt;/h3&gt;
&lt;p&gt;You need &lt;a href=&quot;https://nodejs.org&quot;&gt;Node.js&lt;/a&gt; and nothing else. Open a terminal and run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-docusaurus@latest my-docs classic&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The “classic” template gives you a docs section, a blog, and the default theme. Somewhere in the prompts the installer asks whether you want JavaScript or TypeScript. Take TypeScript. It costs nothing at setup time and saves you an afternoon later, when you are squinting at &lt;code&gt;docusaurus.config.ts&lt;/code&gt; trying to remember what shape the &lt;code&gt;themeConfig&lt;/code&gt; object wants.&lt;/p&gt;
&lt;p&gt;Then start the development server:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-docs
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; start&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That serves the site on &lt;a href=&quot;http://localhost:3000/&quot;&gt;http://localhost:3000/&lt;/a&gt; with hot reload.&lt;/p&gt;
&lt;h4 id=&quot;things-worth-knowing-on-day-one&quot;&gt;Things worth knowing on day one&lt;/h4&gt;
&lt;p&gt;Three details caught me out early enough that they are worth writing down.&lt;/p&gt;
&lt;p&gt;The sidebar is either generated from your folder structure or declared by hand in &lt;code&gt;sidebars.ts&lt;/code&gt;. Auto-generation is fine right up to the moment you care about the order of pages, and then it isn’t. Switch to an explicit sidebar early; retrofitting one across forty files is dull work.&lt;/p&gt;
&lt;p&gt;Versioning is a CLI command rather than something clever with git:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run docusaurus docs:version &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It copies the current &lt;code&gt;docs/&lt;/code&gt; directory into &lt;code&gt;versioned_docs/version-1.0/&lt;/code&gt; and freezes it. That’s it. Run it before your documentation has settled and you will find yourself fixing the same typo in three places forever.&lt;/p&gt;
&lt;p&gt;Search isn’t included in the build. The classic template is wired for Algolia DocSearch, which is free for open source projects but requires an application and a crawl. For anything private, &lt;code&gt;@easyops-cn/docusaurus-search-local&lt;/code&gt; builds a static index at build time and needs no external service at all.&lt;/p&gt;
&lt;h3 id=&quot;running-it-in-docker&quot;&gt;Running it in Docker&lt;/h3&gt;
&lt;p&gt;I don’t enjoy installing a per-project Node toolchain on my laptop, and documentation sites are exactly the kind of thing you come back to eighteen months later on a machine with a different Node version. So I containerise it.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:20.13.0 &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; builder&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; NODE_PATH=/app/node_modules&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package*.json /app/&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm install&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm run build&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; nginx:latest&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;token options&quot;&gt;&lt;span class=&quot;token property&quot;&gt;--from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;builder&lt;/span&gt;&lt;/span&gt; /app/build /usr/share/nginx/html&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two stages: the first installs dependencies and produces the static build, the second serves that build through nginx and carries no Node runtime at all. If you pull packages from a private registry, add &lt;code&gt;.npmrc&lt;/code&gt; to the &lt;code&gt;COPY&lt;/code&gt; line, but only if the file genuinely exists in the repository. A &lt;code&gt;COPY&lt;/code&gt; of a missing source fails the build with an error message that takes longer to understand than it should.&lt;/p&gt;
&lt;p&gt;For local work, a &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
      &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; builder
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'8000:80'&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'.:/app:rw'&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'node_modules:/app/node_modules'&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm start &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;port 80 &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;host 0.0.0.0

&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  node_modules&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;target: builder&lt;/code&gt; line is the important one. It stops the build at the first stage, so you get the development server with hot reload instead of the nginx image. The named &lt;code&gt;node_modules&lt;/code&gt; volume stops your host directory from shadowing the modules that were installed inside the container, which is the single most common reason a mounted Node project refuses to start.&lt;/p&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose build
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can turn the same repository into a &lt;a href=&quot;https://github.com/divio/getting-started-with-reactjs&quot;&gt;template&lt;/a&gt; and push it to Divio Cloud, which handles certificates and environments for you. I wrote about that setup in more detail when I first moved this site over to &lt;a href=&quot;/blog/hosting-on-divio-cloud/&quot;&gt;Divio Cloud&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-alternatives&quot;&gt;The alternatives&lt;/h3&gt;
&lt;p&gt;Docusaurus is not the obvious answer for every project, and picking it for a Python codebase because you happen to like React is a decision your colleagues will resent.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.mkdocs.org/&quot;&gt;MkDocs&lt;/a&gt; is the one I reach for most often. Markdown, one YAML config file, no build toolchain to speak of. Paired with the Material theme it produces something that looks better than most hand-rolled documentation sites and takes about ten minutes to stand up.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sphinx-doc.org/&quot;&gt;Sphinx&lt;/a&gt; is the Python world’s default, and it earns that position. reStructuredText is more fiddly than Markdown, but the cross-referencing, autodoc and extension ecosystem have no real equivalent elsewhere. The django CMS documentation runs on it, as does most of the Python standard library’s.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; is worth mentioning mainly because GitHub Pages builds it natively. If your requirement is “documentation, on GitHub, with no CI pipeline”, that native support is hard to argue with.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://vuepress.vuejs.org/&quot;&gt;VuePress&lt;/a&gt; sits in roughly the same spot as Docusaurus for teams who write Vue rather than React. Same idea, same Markdown-centred structure, different component model.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-pick&quot;&gt;What I would pick&lt;/h3&gt;
&lt;p&gt;If the project is Python, use Sphinx or MkDocs and don’t overthink it. If the project is a React library and the documentation needs live, interactive examples, Docusaurus pays for itself quickly. If it is neither of those, MkDocs with Material is almost always enough, and “almost always enough” is an underrated property in a documentation tool.&lt;/p&gt;
&lt;p&gt;The thing I still don’t love about Docusaurus is how much of it is React all the way down. A broken MDX import fails the production build, which is correct behaviour and mildly infuriating at 17:45 on a Friday 🙃&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Exploring useEffectEvent]]></title><description><![CDATA[useEffectEvent pulls non-reactive logic out of Effects, but it is still canary-only. What it fixes, the rules that come with it, and how to cope until it ships.]]></description><link>https://angelo.dini.dev/blog/react-useeffectevent/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-useeffectevent/</guid><pubDate>Sun, 31 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-hook-you-cant-ship-yet&quot;&gt;A hook you can’t ship yet&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Before anything else: &lt;strong&gt;useEffectEvent&lt;/strong&gt; is experimental. It is not in React 18, it will not be in your next &lt;code&gt;npm install react&lt;/code&gt;, and the documentation page for it carries a warning that the API may change or be dropped entirely. Everything below is worth reading anyway, because the problem it solves is one you already have.&lt;/p&gt;&lt;p&gt;The React docs cover it under &lt;a href=&quot;https://react.dev/learn/separating-events-from-effects&quot;&gt;separating events from effects&lt;/a&gt;, with the API reference living at &lt;a href=&quot;https://react.dev/reference/react/experimental_useEffectEvent&quot;&gt;experimental_useEffectEvent&lt;/a&gt;. To try it you need the canary or experimental release channel, and the import is deliberately ugly so you can’t forget what you signed up for:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; experimental_useEffectEvent &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; useEffectEvent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;Here’s the situation the hook exists for. A chat room connects to a server, and when the connection succeeds you show a notification styled with the current theme:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ChatRoom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; roomId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; connection &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createConnection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;serverUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; roomId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'connected'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;showNotification&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Connected!'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;roomId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The linter is right to demand &lt;code&gt;theme&lt;/code&gt; in that dependency array; the Effect reads it. But the consequence is absurd. Toggle dark mode and the chat disconnects and reconnects, because as far as React is concerned the Effect’s inputs changed and it has to be torn down and set up again.&lt;/p&gt;
&lt;p&gt;Remove &lt;code&gt;theme&lt;/code&gt; from the array and the reconnection stops, but now you’ve lied to React and you’ll eventually show a notification in last week’s colours. Both options are wrong, and this is the trap I’ve watched people fall into over and over. The dependency array conflates two different questions: what should this Effect re-run for, and what values should it read when it does run.&lt;/p&gt;
&lt;h3 id=&quot;what-the-hook-does&quot;&gt;What the hook does&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useEffectEvent&lt;/code&gt; splits those questions apart. The non-reactive part moves out into an effect event, which always sees the latest props and state but never causes the Effect to re-synchronise:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; serverUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'https://localhost:1234'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ChatRoom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; roomId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; onConnected &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useEffectEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;showNotification&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Connected!'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; connection &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createConnection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;serverUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; roomId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'connected'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;onConnected&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; connection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;roomId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;theme&lt;/code&gt; is gone from the dependency array, and not by cheating. It’s read inside &lt;code&gt;onConnected&lt;/code&gt;, which is not reactive, so it genuinely isn’t an input to the Effect. Only &lt;code&gt;roomId&lt;/code&gt; is. Change the theme and the connection stays up; change the room and it reconnects. That’s the behaviour you wanted in the first place, finally expressible.&lt;/p&gt;
&lt;p&gt;Notice what’s missing: &lt;code&gt;onConnected&lt;/code&gt; itself does not go in the dependency array either. That’s not an oversight to be silenced with a lint comment, it’s the rule.&lt;/p&gt;
&lt;h3 id=&quot;the-rules&quot;&gt;The rules&lt;/h3&gt;
&lt;p&gt;There are three, and they’re narrower than most hooks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Only call effect events from inside Effects.&lt;/li&gt;
&lt;li&gt;Never pass them to another component or hook. Not as a prop, not as an argument.&lt;/li&gt;
&lt;li&gt;Never put them in a dependency array.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The second one catches people out, because passing a callback down is such a normal thing to do. The reasoning is that an effect event has no stable identity from the outside; it’s a hole in the reactivity graph, and letting it travel would make it impossible to reason about who triggers what. If you want to hand a callback to a child, that’s an ordinary prop and an ordinary event handler.&lt;/p&gt;
&lt;p&gt;Which is the other half of the picture. &lt;code&gt;useEffectEvent&lt;/code&gt; is not a replacement for &lt;code&gt;onClick&lt;/code&gt;. Logic that runs because a user did something belongs in an event handler and was never an Effect’s business. The hook is for the narrower case where the Effect owns something external, a socket, a subscription, an observer, and part of the code reacting to it should not participate in the Effect’s lifecycle.&lt;/p&gt;
&lt;h3 id=&quot;living-without-it&quot;&gt;Living without it&lt;/h3&gt;
&lt;p&gt;Since you can’t ship it, what do you do today? The usual approximation is the latest-ref pattern:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useCallback&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useLayoutEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useRef &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useEventCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ref &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useLayoutEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    ref&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fn&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; ref&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The returned function has a stable identity, so it’s safe to leave out of dependency arrays, and it always calls the most recent closure. I’ve used variations of this for years, and it’s closely related to the memoisation work I wrote about in &lt;a href=&quot;/blog/react-use-callback/&quot;&gt;optimizing React components&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is not the same thing, though, and it’s worth understanding where it falls short. Nothing stops you calling it during render, which reads a value that may not be committed yet. Nothing stops you passing it to a child. It’s a convention held together by discipline, whereas the real hook can enforce its rules because React owns the call site. That gap is precisely why the React team never blessed the userland version, and why this has taken so long to land.&lt;/p&gt;
&lt;p&gt;One practical annoyance while you wait: the stable &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt; doesn’t know what an effect event is, so if you do experiment with the canary build, &lt;code&gt;exhaustive-deps&lt;/code&gt; will cheerfully tell you to add &lt;code&gt;onConnected&lt;/code&gt; to the array. Ignore it there. That’s the one place the rule is wrong.&lt;/p&gt;
&lt;h3 id=&quot;where-this-leaves-me&quot;&gt;Where this leaves me&lt;/h3&gt;
&lt;p&gt;I’m not putting a canary React into anything that matters, so &lt;code&gt;useEventCallback&lt;/code&gt; stays in my utils folder for now. What has changed is how I read my own Effects. Whenever the dependency array has an entry that makes me wince, that entry is almost always a value the Effect reads rather than a value it should re-run for, and now I have a name for the distinction 🙂&lt;/p&gt;
&lt;p&gt;Worth watching the &lt;a href=&quot;https://github.com/facebook/react/releases&quot;&gt;React releases page&lt;/a&gt; for when this graduates. Until then it’s a useful way to think about Effects, and a hook you can’t have.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Integrating AI and Machine Learning]]></title><description><![CDATA[Running machine learning in the browser from React with TensorFlow.js and Brain.js. A worked sentiment example, what it costs, and when to call an API instead.]]></description><link>https://angelo.dini.dev/blog/ai-and-ml/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/ai-and-ml/</guid><pubDate>Thu, 29 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;running-a-model-in-the-browser&quot;&gt;Running a model in the browser&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Nearly every &quot;AI feature&quot; I've been asked about in the last year turned out to be a &lt;code&gt;fetch&lt;/code&gt; call to somebody else's API. That's usually the right answer. But there is a smaller category of feature where the model has to run on the user's machine, and React has no opinion about how you do that.&lt;/p&gt;&lt;p&gt;I wrote a while back about &lt;a href=&quot;/blog/how-ai-changes-coding/&quot;&gt;how AI changes the way we write code&lt;/a&gt;. This is the other half of it: shipping a model as part of the frontend rather than using one to produce the frontend. The tooling is more mature than I expected and the trade-offs are sharper than the marketing suggests.&lt;/p&gt;
&lt;h3 id=&quot;the-two-libraries&quot;&gt;The two libraries&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.tensorflow.org/js&quot;&gt;TensorFlow.js&lt;/a&gt; is the serious one. It runs Keras and TensorFlow models in the browser or in Node, either on WebGL or, with &lt;code&gt;@tensorflow/tfjs-backend-wasm&lt;/code&gt;, on WebAssembly. You can train in it, though mostly you won’t; the realistic use is loading a model somebody else trained and running inference on it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://brain.js.org/&quot;&gt;Brain.js&lt;/a&gt; is the small one. Neural networks in a few lines, trained in the browser on data you have to hand:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NeuralNetwork &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'brain.js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; net &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NeuralNetwork&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

net&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.03&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.09&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

net&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the whole API surface, more or less. If your problem fits in a small feedforward network and you can describe the training data in JSON, Brain.js will get you there faster than anything else. If it doesn’t fit, it won’t stretch, and you’ll be back on TensorFlow.js within a week.&lt;/p&gt;
&lt;h3 id=&quot;a-sentiment-component-properly&quot;&gt;A sentiment component, properly&lt;/h3&gt;
&lt;p&gt;The usual worked example is sentiment analysis, so let’s do that one and be honest about the sharp edges. TensorFlow.js publishes a &lt;a href=&quot;https://github.com/tensorflow/tfjs-examples/tree/master/sentiment&quot;&gt;sentiment example&lt;/a&gt; built on a small CNN trained against the IMDB reviews set, which is a reasonable thing to point at.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @tensorflow/tfjs&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, loading. Almost every tutorial I’ve read loads the model inside the prediction function, which means a fresh download and a fresh parse on every keystroke. Load it once and hold the promise:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; tf &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@tensorflow/tfjs'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; modelPromise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;loadModel&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  modelPromise &lt;span class=&quot;token operator&quot;&gt;??=&lt;/span&gt; tf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;loadLayersModel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/models/sentiment/model.json'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; modelPromise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the prediction. The thing that trips people up here is that &lt;code&gt;model.predict()&lt;/code&gt; returns a tensor, not a number. Render it straight into JSX and React will throw at you. You have to pull the value out with &lt;code&gt;data()&lt;/code&gt;, which is async, and dispose of the tensors afterwards because the WebGL backend does not garbage collect them for you:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;predictSentiment&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; model &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loadModel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Tokenise to word indices and pad to the length the model expects.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; input &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;preprocess&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; output &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; model&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;predict&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;score&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; output&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  tf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dispose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; output&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;preprocess&lt;/code&gt; is doing a lot of quiet work in that snippet. A text model doesn’t take text; it takes a fixed-length tensor of integers, mapped through the exact vocabulary the model was trained with, padded or truncated to the exact sequence length. Get the vocabulary wrong and nothing errors. You just get confident nonsense, which is a considerably worse failure mode than a stack trace.&lt;/p&gt;
&lt;p&gt;Finally, the component:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;SentimentAnalyzer&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setText&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;score&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setScore&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pending&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setPending&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleAnalyze&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setPending&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setScore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;predictSentiment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setPending&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;textarea&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleAnalyze&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pending&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        Analyze
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Positive'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Negative'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;score&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing about that is React-specific, which is rather the point. The model is a module-level singleton, the prediction is an async function, and the component is a form. There’s no hook to learn.&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h3&gt;
&lt;p&gt;Three things, in roughly the order they’ll annoy you.&lt;/p&gt;
&lt;p&gt;The library is a heavy dependency, and the model weights are a separate download on top of it, fetched at runtime as &lt;code&gt;model.json&lt;/code&gt; plus binary shards. Neither belongs in your initial bundle. Lazy-load the whole feature behind a dynamic import and don’t touch it until the user does something that needs it.&lt;/p&gt;
&lt;p&gt;Inference on the main thread will drop frames on a mid-range phone. WebGL helps, but the first call after loading is always the slow one while shaders compile, so warm the model up with a dummy prediction while the user is still typing.&lt;/p&gt;
&lt;p&gt;And tensors leak. &lt;code&gt;tf.dispose()&lt;/code&gt; and &lt;code&gt;tf.tidy()&lt;/code&gt; exist for a reason, and forgetting them in a component that re-renders is how you end up with a tab consuming a gigabyte.&lt;/p&gt;
&lt;h3 id=&quot;when-to-do-this-at-all&quot;&gt;When to do this at all&lt;/h3&gt;
&lt;p&gt;My honest position: run the model in the browser when the data must not leave the device, when the feature has to work offline, or when a round trip per keystroke would be absurd. Local search ranking, an offline classifier, live pose or gesture detection from a webcam. Those earn it.&lt;/p&gt;
&lt;p&gt;Everything else is better served by a request to a server, where the model is bigger, the hardware is yours, you can swap it without shipping a release, and nobody downloads several megabytes of weights to find out their review was positive.&lt;/p&gt;
&lt;p&gt;The nice thing about 2024 is that both options are genuinely easy now. The hard part was never the integration; it’s deciding which side of the wire the model belongs on, and that decision has almost nothing to do with React.&lt;/p&gt;
&lt;h3 id=&quot;next&quot;&gt;Next&lt;/h3&gt;
&lt;p&gt;I want to try the same component with the WASM backend on an older phone and see how far off WebGL it lands. If the gap is small it changes the calculus quite a bit for anything that has to work on hardware people actually own 📱&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Looking back at 2023]]></title><description><![CDATA[A short look back at 2023, a year of travelling and shifting relationships, plus every article I published on React, TypeScript, Vite and a few odd tools.]]></description><link>https://angelo.dini.dev/blog/looking-back-at-2023/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/looking-back-at-2023/</guid><pubDate>Wed, 31 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-good-year-mostly&quot;&gt;A good year, mostly&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;This year saw a plethora of changes, including a lot of travelling and ever-changing personal relationships. Overall, though, it was a good year, which is more than I could say for &lt;a href=&quot;/blog/looking-back-at-2022/&quot;&gt;the last one&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There’s not much I’d like to share beyond that. I’m moving forward, learning new skills and developing myself further. What I can point at is the writing, which for once I kept up all year without a single missed month.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-suspense/&quot;&gt;Understanding React Suspense&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/idiomatic-javascript/&quot;&gt;Idiomatic Programming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-use-callback/&quot;&gt;Optimizing React Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-transition/&quot;&gt;Understanding React Transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/how-ai-changes-coding/&quot;&gt;AI and Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/typescript-union/&quot;&gt;Understanding TypeScript Unions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/react-use-context/&quot;&gt;Exploring React’s useContext Hook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/migrate-to-vite/&quot;&gt;Migrate from Create React App to Vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/more-about-vite/&quot;&gt;Linting and Testing in Vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/mackup/&quot;&gt;Keeping Mac Settings in Sync with Mackup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/conventional-commits/&quot;&gt;Demystifying Conventional Commits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather more React than I intended. See you all in 2024 🎉&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Demystifying Conventional Commits]]></title><description><![CDATA[A look at Conventional Commits: the message format, the types worth using, how breaking changes are actually marked, and when the convention is not worth it.]]></description><link>https://angelo.dini.dev/blog/conventional-commits/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/conventional-commits/</guid><pubDate>Sat, 30 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;commit-messages-a-machine-can-read&quot;&gt;Commit messages a machine can read&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Most git histories I've inherited read like a diary written by five people who never spoke to each other. &quot;fix stuff&quot;, &quot;wip&quot;, &quot;final fix 2&quot;. You can live with that. What you can't do is hand it to a tool and ask what changed between two releases.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.conventionalcommits.org/&quot;&gt;Conventional Commits&lt;/a&gt; is a small specification that fixes exactly that one problem. It puts a machine-readable prefix on the first line of every commit and leaves the rest of the message alone. That’s it. There’s no framework to adopt and nothing to run; a team can start using it this afternoon and only later decide whether to automate anything on top.&lt;/p&gt;
&lt;p&gt;I’d like to go through the format, the parts people usually get wrong, and where I think the convention stops paying for itself.&lt;/p&gt;
&lt;h3 id=&quot;the-shape-of-a-message&quot;&gt;The shape of a message&lt;/h3&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;type&gt;[optional scope]: &amp;lt;description&gt;

[optional body]

[optional footer(s)]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The type is the only genuinely load-bearing part. &lt;code&gt;feat&lt;/code&gt; means a new capability and maps to a MINOR bump under semantic versioning. &lt;code&gt;fix&lt;/code&gt; means a bug fix and maps to a PATCH. Everything else is convention rather than specification, though most teams settle on roughly the same set: &lt;code&gt;docs&lt;/code&gt;, &lt;code&gt;refactor&lt;/code&gt;, &lt;code&gt;perf&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;ci&lt;/code&gt;, &lt;code&gt;chore&lt;/code&gt; and &lt;code&gt;revert&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The scope in parentheses narrows it down, which is the difference between a changelog you can skim and one you can’t:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;feat(auth): add support for hardware security keys
fix(parser): handle empty arrays in the query string
docs: describe the migration path from v1
refactor(api): collapse the two response builders into one&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the description in the imperative and short. Commitlint’s conventional config caps the header at 100 characters, which is generous enough that hitting it usually means you’re describing two commits.&lt;/p&gt;
&lt;h3 id=&quot;breaking-changes-properly&quot;&gt;Breaking changes, properly&lt;/h3&gt;
&lt;p&gt;This is the part I see done wrong most often, and it’s worth being precise about because a mistake here silently breaks your versioning.&lt;/p&gt;
&lt;p&gt;A breaking change is marked with an exclamation mark after the type or scope:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;feat(api)!: require Node 18 or newer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or with a &lt;code&gt;BREAKING CHANGE:&lt;/code&gt; footer, which gives you room to explain the migration:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;feat(api): switch the client to native fetch

BREAKING CHANGE: Node 16 is no longer supported. Upgrade to
Node 18 or pin the previous major.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use both together, and for anything with a real migration path you should; the &lt;code&gt;!&lt;/code&gt; catches the reader’s eye and the footer is what ends up in the changelog.&lt;/p&gt;
&lt;p&gt;What does &lt;strong&gt;not&lt;/strong&gt; work is dressing a breaking change up as a chore, which is the pattern I keep running into:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;chore: upgrade library to v2.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Semantically that’s just wrong, but the practical consequence is worse. Run that through semantic-release and its default rules produce no release at all. &lt;code&gt;chore&lt;/code&gt; isn’t in the list of types that trigger a version bump. Your dependency bump quietly ships to whoever installs your package next with no major, no minor, no changelog entry, and no warning. The commit that most needed to be visible is the one that disappears.&lt;/p&gt;
&lt;h3 id=&quot;what-the-convention-buys-you&quot;&gt;What the convention buys you&lt;/h3&gt;
&lt;p&gt;Once messages are structured, a few things become possible that weren’t before.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Versioning without a meeting.&lt;/strong&gt; Tools read the commits since the last tag and work out the bump themselves. Nobody argues about whether the release is a 1.4.0 or a 2.0.0, because the argument already happened when the commit was written.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Changelogs that write themselves.&lt;/strong&gt; &lt;a href=&quot;https://github.com/semantic-release/semantic-release/&quot;&gt;semantic-release&lt;/a&gt; groups commits by type, drops the noise, and produces something you can put in front of users.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A history you can query.&lt;/strong&gt; &lt;code&gt;git log --oneline --grep &amp;#39;^fix&amp;#39; v1.2.0..HEAD&lt;/code&gt; is a perfectly good bug report for a release, and it costs nothing to run.&lt;/p&gt;
&lt;p&gt;The subtler benefit is the one that shows up in review. Writing &lt;code&gt;feat&lt;/code&gt; or &lt;code&gt;fix&lt;/code&gt; at the start of a message forces you to decide what the change actually is, and if you can’t decide, the commit is usually doing two things and wants splitting.&lt;/p&gt;
&lt;h3 id=&quot;tooling&quot;&gt;Tooling&lt;/h3&gt;
&lt;p&gt;You need none of this to start. Once the habit is there, three tools do most of the work.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/commitizen/cz-cli&quot;&gt;Commitizen&lt;/a&gt; replaces &lt;code&gt;git commit&lt;/code&gt; with a prompt that walks you through type, scope and description. Good for onboarding, mildly irritating once you’ve internalised the format.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://commitlint.js.org/&quot;&gt;commitlint&lt;/a&gt; checks messages against the convention and rejects the ones that don’t fit. Paired with &lt;a href=&quot;https://typicode.github.io/husky/&quot;&gt;Husky&lt;/a&gt; it runs on the &lt;code&gt;commit-msg&lt;/code&gt; hook, not &lt;code&gt;pre-commit&lt;/code&gt;, since the message doesn’t exist yet when &lt;code&gt;pre-commit&lt;/code&gt; fires:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; --save-dev @commitlint/cli @commitlint/config-conventional husky
npx husky &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
npx husky &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; .husky/commit-msg &lt;span class=&quot;token string&quot;&gt;'npx --no -- commitlint --edit &quot;$1&quot;'&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a one-line config:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// commitlint.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'@commitlint/config-conventional'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;a href=&quot;https://github.com/semantic-release/semantic-release/&quot;&gt;semantic-release&lt;/a&gt; reads the log on CI, decides the next version, writes the changelog, tags and publishes. Its default mapping is worth memorising, because it’s the thing that turns your commit messages into real consequences:&lt;/p&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: Tooling&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Commit&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;perf:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;minor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!&lt;/code&gt; or &lt;code&gt;BREAKING CHANGE:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h3 id=&quot;where-id-skip-it&quot;&gt;Where I’d skip it&lt;/h3&gt;
&lt;p&gt;The convention has a cost, and it isn’t the learning curve. People pick the format up in a day. The cost is that once a tool is publishing releases off your commit messages, a sloppy message becomes a wrong version number, and fixing a bad tag on a published package is a considerably worse afternoon than writing a careful commit would have been.&lt;/p&gt;
&lt;p&gt;So: any library other people install, yes, without hesitation. A repository with more than three people committing to it, yes. My own weekend projects, no. Nothing downstream reads those commits and I’m not going to lint myself into writing &lt;code&gt;chore(deps):&lt;/code&gt; on a Sunday.&lt;/p&gt;
&lt;h3 id=&quot;starting-on-an-existing-repository&quot;&gt;Starting on an existing repository&lt;/h3&gt;
&lt;p&gt;You don’t need to rewrite history, and please don’t. Turn on commitlint, let the old messages stay as they are, and set the initial version by hand at the first release so the tooling has a baseline. Everything from that tag forward will be clean, which is all anyone actually needs.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Keeping Mac Settings in Sync with Mackup]]></title><description><![CDATA[Mackup keeps Mac application settings in sync through iCloud. How I configure it, what it actually moves around, and why symlinked preferences bite on macOS 14.]]></description><link>https://angelo.dini.dev/blog/mackup/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/mackup/</guid><pubDate>Thu, 30 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-settings-you-cant-script&quot;&gt;The settings you can’t script&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;My dotfiles get me most of the way to a working machine. Packages installed, shell configured, editors set up, all from a couple of commands. What they don't cover is the pile of preferences that live inside applications, buried in plist files and Application Support folders nobody wants to copy by hand.&lt;/p&gt;&lt;p&gt;That’s the gap &lt;a href=&quot;https://github.com/lra/mackup&quot;&gt;Mackup&lt;/a&gt; fills. It knows where a few hundred applications keep their configuration, moves those files into a folder you control, and symlinks them back to where the app expects to find them. Put that folder somewhere that syncs and a second machine picks up the same settings.&lt;/p&gt;
&lt;p&gt;I’ve mentioned it in passing in my post on &lt;a href=&quot;/blog/dotfiles/&quot;&gt;dotfiles&lt;/a&gt;, where it handles the parts I never managed to turn into shell scripts. Here’s the actual setup.&lt;/p&gt;
&lt;h3 id=&quot;installing-it&quot;&gt;Installing it&lt;/h3&gt;
&lt;p&gt;Nothing surprising. On both the old and the new machine:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; mackup&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the whole install. The interesting part is the configuration file, and Mackup does not create that for you. You write &lt;code&gt;~/.mackup.cfg&lt;/code&gt; yourself.&lt;/p&gt;
&lt;h3 id=&quot;telling-it-where-to-put-things&quot;&gt;Telling it where to put things&lt;/h3&gt;
&lt;p&gt;Mine goes to iCloud, so a dead machine doesn’t take the backup down with it:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;engine&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;icloud&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;Mackup&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;[storage]&lt;/code&gt; header matters. Drop it and the two keys below are silently ignored, which is a fun half hour to spend. &lt;code&gt;engine&lt;/code&gt; also takes &lt;code&gt;dropbox&lt;/code&gt;, &lt;code&gt;google_drive&lt;/code&gt; and &lt;code&gt;file_system&lt;/code&gt;; the last one needs an extra &lt;code&gt;path&lt;/code&gt; key and is what you’d reach for if you want the backup to land inside a folder your dotfiles already sync. &lt;code&gt;directory&lt;/code&gt; is only the folder name inside that storage, and &lt;code&gt;Mackup&lt;/code&gt; is the default anyway, so you can leave it out.&lt;/p&gt;
&lt;p&gt;Two more sections are worth knowing about:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;applications_to_ignore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
ssh
gnupg

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;applications_to_sync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
git
vim&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They don’t stack the way you’d expect. &lt;code&gt;applications_to_sync&lt;/code&gt; is an allowlist, and the moment you use it Mackup stops looking at anything else. I run the ignore list instead and let the default set do its thing, mostly because I’d rather forget to exclude something than forget to include it. The names come from &lt;code&gt;mackup list&lt;/code&gt;, not from the app’s display name.&lt;/p&gt;
&lt;h3 id=&quot;backup-transfer-restore&quot;&gt;Backup, transfer, restore&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;On the old machine, run &lt;code&gt;mackup backup&lt;/code&gt;. Your files move into the storage folder and symlinks take their place.&lt;/li&gt;
&lt;li&gt;Wait for iCloud to genuinely finish uploading. Check the folder in Finder; Mackup has no idea what your sync client is up to.&lt;/li&gt;
&lt;li&gt;On the new machine, install Mackup, put the same &lt;code&gt;.mackup.cfg&lt;/code&gt; in your home directory, then run &lt;code&gt;mackup restore&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;mackup uninstall&lt;/code&gt; reverses everything: the real files get copied back out of storage and the symlinks are removed. Run it before you wipe a machine, otherwise you leave applications pointing at links that resolve to nothing.&lt;/p&gt;
&lt;h3 id=&quot;applications-mackup-doesnt-know&quot;&gt;Applications Mackup doesn’t know&lt;/h3&gt;
&lt;p&gt;The supported list is long but it will not have your niche thing. Create a &lt;code&gt;~/.mackup&lt;/code&gt; directory and add one &lt;code&gt;.cfg&lt;/code&gt; file per application:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token value attr-value&quot;&gt;My personal synced files&lt;/span&gt;

&lt;span class=&quot;token section&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token section-name selector&quot;&gt;configuration_files&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
.gitignore
.config/someapp/config.toml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Paths are relative to your home directory, and directories work as well as files. Run &lt;code&gt;mackup list&lt;/code&gt; afterwards and your entry should show up alongside the built-in ones. If you name it after an application Mackup already supports, your file replaces theirs rather than adding to it.&lt;/p&gt;
&lt;h3 id=&quot;the-part-that-bites&quot;&gt;The part that bites&lt;/h3&gt;
&lt;p&gt;Symlinks. The entire model rests on them, and macOS 14 stopped playing along. Symlinked &lt;code&gt;.plist&lt;/code&gt; preference files get reset or quietly rewritten by the system, so an application you were sure was synced drifts back to its defaults, and you only notice weeks later when a keyboard shortcut is gone. There are open issues about this (&lt;a href=&quot;https://github.com/lra/mackup/issues/1924&quot;&gt;#1924&lt;/a&gt; and &lt;a href=&quot;https://github.com/lra/mackup/issues/1950&quot;&gt;#1950&lt;/a&gt;) and nothing Mackup can do about it from its side.&lt;/p&gt;
&lt;p&gt;So my rule now is dull but it holds: anything that keeps its settings in a plist goes into &lt;code&gt;[applications_to_ignore]&lt;/code&gt;, and Mackup gets the dotfile-shaped configs it was always good at. That’s a narrower job than the README suggests, and it’s still the job I needed doing.&lt;/p&gt;
&lt;h3 id=&quot;before-you-trust-it&quot;&gt;Before you trust it&lt;/h3&gt;
&lt;p&gt;Run &lt;code&gt;mackup backup&lt;/code&gt; once on a machine you can afford to break, then go and look at what actually moved. This is a tool that rearranges your home directory, and it’s worth knowing exactly which files it took before you find out the hard way at three in the morning on a fresh install 😅&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Linting and Testing in Vite]]></title><description><![CDATA[Finishing a Create React App to Vite migration by wiring up ESLint, Prettier and Jest by hand, and clearing out the leftovers react-scripts left behind.]]></description><link>https://angelo.dini.dev/blog/more-about-vite/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/more-about-vite/</guid><pubDate>Sat, 28 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-parts-react-scripts-took-with-it&quot;&gt;The parts react-scripts took with it&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;In my &lt;a href=&quot;/blog/migrate-to-vite/&quot;&gt;previous article&lt;/a&gt; I swapped react-scripts out for Vite and the app came back up, faster than before. What didn't come back up was everything react-scripts was quietly doing on the side.&lt;/p&gt;&lt;p&gt;Linting, formatting and tests were all bundled into that one dependency, with configuration you never saw and mostly never needed to. Uninstall it and they leave together. That’s the honest cost of the migration, and it’s the reason people abandon it halfway through.&lt;/p&gt;
&lt;p&gt;The good news is it’s an afternoon of work, once, and afterwards the configuration is yours.&lt;/p&gt;
&lt;h3 id=&quot;eslint&quot;&gt;ESLint&lt;/h3&gt;
&lt;p&gt;Start with the Vite plugin, which runs the linter as part of the dev server so mistakes appear in the browser overlay rather than only in CI:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; vite-plugin-eslint --save-dev&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; eslint &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite-plugin-eslint'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;eslint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then ESLint itself and its config generator:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; eslint --save-dev
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; init @eslint/config&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generator asks a handful of questions and writes an &lt;code&gt;.eslintrc.js&lt;/code&gt; that does not quite work. Here’s mine after fixing it:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;es2021&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'eslint:recommended'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'standard-with-typescript'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'plugin:react/recommended'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;'plugin:react/jsx-runtime'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;overrides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'.eslintrc.{js,cjs}'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;parserOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;sourceType&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'script'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;parserOptions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;ecmaVersion&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'latest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;sourceType&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'module'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./tsconfig.json'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;'@typescript-eslint/semi'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'off'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;settings&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'detect'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two of those lines are the ones worth pointing at, because without them you get errors that look like your code is broken when it isn’t.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parserOptions.project&lt;/code&gt; is required by &lt;code&gt;standard-with-typescript&lt;/code&gt;. It turns on the rules that need type information, and the parser can’t do that without knowing which &lt;code&gt;tsconfig.json&lt;/code&gt; you mean. Leave it out and every lint run fails with a wall of text about the rule requiring parser services, on files that are perfectly fine.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;plugin:react/jsx-runtime&lt;/code&gt; switches off &lt;code&gt;react/react-in-jsx-scope&lt;/code&gt;. Since React 17 the new JSX transform means you don’t import React just to write JSX, and without this line the linter demands an import on every component file. I’ve seen people add hundreds of pointless imports rather than one line of config.&lt;/p&gt;
&lt;h3 id=&quot;prettier&quot;&gt;Prettier&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;npm init @eslint/config&lt;/code&gt; pattern doesn’t carry over to Prettier, there’s no init command. Install it and write the config file yourself:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; prettier eslint-config-prettier --save-dev
&lt;span class=&quot;token function&quot;&gt;touch&lt;/span&gt; .prettierrc.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-yml&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;printWidth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tabWidth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;useTabs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;semi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;singleQuote&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;trailingComma&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none
&lt;span class=&quot;token key atrule&quot;&gt;bracketSpacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;bracketSameLine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;arrowParens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; avoid&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’re copying an older config from somewhere, note that &lt;code&gt;jsxBracketSameLine&lt;/code&gt; became &lt;code&gt;bracketSameLine&lt;/code&gt; back in Prettier 2.4 and no longer exists in Prettier 3. Prettier just ignores the key it doesn’t recognise, which is a pleasant way to spend twenty minutes wondering why your JSX is being formatted the way you asked it not to be.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;eslint-config-prettier&lt;/code&gt; is the other half. It turns off the ESLint rules that disagree with Prettier about whitespace, so the two tools stop rewriting each other’s output. Put it last in &lt;code&gt;extends&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'eslint:recommended'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'standard-with-typescript'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'plugin:react/recommended'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'plugin:react/jsx-runtime'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'prettier'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Order matters here. Last wins, and &lt;code&gt;prettier&lt;/code&gt; needs to win.&lt;/p&gt;
&lt;h3 id=&quot;tests&quot;&gt;Tests&lt;/h3&gt;
&lt;p&gt;Now the part I have opinions about.&lt;/p&gt;
&lt;p&gt;The obvious choice for a Vite project is &lt;a href=&quot;https://vitest.dev/&quot;&gt;Vitest&lt;/a&gt;. It reads your existing &lt;code&gt;vite.config.ts&lt;/code&gt;, so your aliases, your plugins and your SVG imports already work, and the API is close enough to Jest that most test files run unchanged. If you’re starting fresh, use it. I would.&lt;/p&gt;
&lt;p&gt;Jest is still what I reach for when the project has an existing suite, custom matchers and a CI pipeline built around it, because “close enough to Jest” is doing real work in that sentence and I’d rather not find the gaps on a deadline. So, Jest:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; jest ts-jest @types/jest jest-environment-jsdom --save-dev&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// jest.config.js&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/** @type {import('ts-jest').JestConfigWithTsJest} */&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;preset&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'ts-jest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;testEnvironment&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'jsdom'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;setupFilesAfterEnv&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'&amp;lt;rootDir&gt;/src/setupTests.ts'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;moduleDirectories&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'node_modules'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'src'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;moduleNameMapper&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;'&amp;lt;rootDir&gt;/__mocks__/file-mock.js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;'\\.(css|less|scss)$'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'&amp;lt;rootDir&gt;/__mocks__/style-mock.js'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;npx ts-jest config:init&lt;/code&gt; generates a bare version of this if you’d rather start from a stub.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;moduleNameMapper&lt;/code&gt; entries exist because Jest has no idea what to do with a &lt;code&gt;.png&lt;/code&gt; or a &lt;code&gt;.css&lt;/code&gt; import. Vite handles those; Node does not. Both mappings need to point at a module that exports something harmless:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// __mocks__/file-mock.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'test-file-stub'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// __mocks__/style-mock.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’ve seen these mapped at &lt;code&gt;setupTests.ts&lt;/code&gt; instead, and it does technically work, in the sense that requiring a file that exports nothing gives you nothing. It also means every stylesheet import in your app pulls in your entire test setup, and when that setup grows a global mock or a timer, the reason your test suite got strange will not be anywhere near where you’re looking.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;setupTests.ts&lt;/code&gt; has an actual job of its own:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/setupTests.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@testing-library/jest-dom'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;moduleDirectories&lt;/code&gt; includes &lt;code&gt;src&lt;/code&gt;, which is how Jest resolves the same &lt;code&gt;import Button from &amp;#39;components/button&amp;#39;&lt;/code&gt; paths that &lt;code&gt;vite-tsconfig-paths&lt;/code&gt; handles for the build. Two tools, two configurations, same aliases. Keeping them in step is genuinely annoying and is another point in Vitest’s favour.&lt;/p&gt;
&lt;h3 id=&quot;leftovers&quot;&gt;Leftovers&lt;/h3&gt;
&lt;p&gt;A few things react-scripts left in the project that no longer belong:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;web-vitals&lt;/code&gt; and &lt;code&gt;reportWebVitals&lt;/code&gt;.&lt;/strong&gt; CRA generated &lt;code&gt;src/reportWebVitals.ts&lt;/code&gt; and called it from the entry file. Unless you were actually sending those numbers somewhere, uninstall the package, delete the file and remove the import.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser targets.&lt;/strong&gt; Your &lt;code&gt;package.json&lt;/code&gt; still has a &lt;code&gt;browserslist&lt;/code&gt; field, but Vite’s build target lives in &lt;code&gt;vite.config.ts&lt;/code&gt; and doesn’t look at it. Install &lt;code&gt;browserslist-to-esbuild&lt;/code&gt; and it converts the list you already have into esbuild targets, so you keep one source of truth instead of two lists that drift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything else.&lt;/strong&gt; &lt;code&gt;npx npm-check-updates&lt;/code&gt; will show you what’s out of date, &lt;code&gt;ncu -u&lt;/code&gt; writes the new ranges. Worth a look every few months rather than once a year in a panic.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;the-finished-files&quot;&gt;The finished files&lt;/h3&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vite.config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; react &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@vitejs/plugin-react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; viteTsconfigPaths &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite-tsconfig-paths'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; svgrPlugin &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite-plugin-svgr'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; eslint &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite-plugin-eslint'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; browserslistToEsbuild &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'browserslist-to-esbuild'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// see more at https://vitejs.dev/config/&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  plugins&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;viteTsconfigPaths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;svgrPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;eslint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  build&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    target&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;browserslistToEsbuild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// package.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;my-app&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0.1.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;private&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^18.2.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;react-dom&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^18.2.0&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;dev&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tsc &amp;amp;&amp;amp; vite build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;serve&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vite preview&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;eslint src/**/*.{ts,tsx}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;format&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prettier --write src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;jest&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;browserslist&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;production&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;0.2%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;not dead&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;not op_mini all&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;development&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;last 1 chrome version&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;last 1 firefox version&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;last 1 safari version&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;devDependencies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@testing-library/jest-dom&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^6.1.4&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@testing-library/react&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^14.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@testing-library/user-event&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^14.5.1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@types/jest&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^29.5.6&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@types/node&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^20.8.9&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@types/react&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^18.2.33&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@types/react-dom&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^18.2.14&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@typescript-eslint/eslint-plugin&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^6.9.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@vitejs/plugin-react&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^4.1.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;browserslist-to-esbuild&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^1.2.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^8.52.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint-config-prettier&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^9.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint-config-standard-with-typescript&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^39.1.1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint-plugin-import&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^2.29.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint-plugin-n&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^16.2.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint-plugin-promise&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^6.1.1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;eslint-plugin-react&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^7.33.2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;jest&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^29.7.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;jest-environment-jsdom&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^29.7.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;prettier&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^3.0.3&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;ts-jest&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^29.1.1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;typescript&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^5.2.2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;vite&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^4.5.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;vite-plugin-eslint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^1.8.1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;vite-plugin-svgr&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^4.1.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;vite-tsconfig-paths&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^4.2.1&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; are real dependencies. Everything else builds, lints or tests the thing and belongs in &lt;code&gt;devDependencies&lt;/code&gt;, which the CRA-generated file never got right either.&lt;/p&gt;
&lt;h3 id=&quot;where-this-leaves-me&quot;&gt;Where this leaves me&lt;/h3&gt;
&lt;p&gt;The whole setup is now four config files I can read in five minutes, instead of one dependency I couldn’t change. That was the point of the exercise.&lt;/p&gt;
&lt;p&gt;What I’m less pleased about is having Jest and Vite each resolving modules their own way. It works today, and it’s exactly the sort of duplication that quietly rots. The next project I start goes on Vitest.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Switch to Vite]]></title><description><![CDATA[Moving a Create React App project to Vite: the packages to swap, the config to write, the index.html shuffle and the environment variables that break.]]></description><link>https://angelo.dini.dev/blog/migrate-to-vite/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/migrate-to-vite/</guid><pubDate>Sun, 24 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-starter-that-stopped-moving&quot;&gt;The starter that stopped moving&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;&quot;Create React App&quot; got a lot of people, me included, past the part of React that used to be genuinely miserable: deciding what a build looks like. It answered the question so you didn't have to. It has also barely moved in a year, and that's now the problem.&lt;/p&gt;&lt;p&gt;Every few months I run &lt;code&gt;npm audit&lt;/code&gt; on an older project and get a list of transitive vulnerabilities I can’t fix, because they live under &lt;code&gt;react-scripts&lt;/code&gt; and &lt;code&gt;react-scripts&lt;/code&gt; isn’t being updated. Waiting for someone else to bump a dependency isn’t a plan.&lt;/p&gt;
&lt;h3 id=&quot;is-it-dead&quot;&gt;Is it dead?&lt;/h3&gt;
&lt;p&gt;Not dead. Dying, quietly, in the way open source projects usually do. The &lt;a href=&quot;https://github.com/facebook/create-react-app/issues/13080&quot;&gt;TypeScript 5 upgrade&lt;/a&gt; sat open for over a year. The React team’s own &lt;a href=&quot;https://github.com/reactjs/react.dev/pull/5487&quot;&gt;documentation pull request&lt;/a&gt; moved the recommendation towards frameworks, and the mentions of CRA came out of the official docs entirely:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you’re still not convinced, or your app has unusual constraints not served well by these frameworks and you’d like to roll your own custom setup, we can’t stop you—go for it! Grab react and react-dom from npm, set up your custom build process with a bundler like Vite or Parcel, and add other tools as you need them for routing, static generation or server-side rendering, and more.
&lt;br /&gt;&lt;br /&gt;&lt;cite&gt;&lt;a href=&quot;https://react.dev/learn/start-a-new-react-project&quot;&gt;React Website&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For small projects with low complexity I still reach for &lt;a href=&quot;https://parceljs.org/&quot;&gt;Parcel&lt;/a&gt;, because zero config really does mean zero config there. This walkthrough uses &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt; instead, because once a project has path aliases, SVG imports, a proxy and three environments, I want the config file to exist.&lt;/p&gt;
&lt;h3 id=&quot;start-with-a-create-react-app&quot;&gt;Start with a Create React App&lt;/h3&gt;
&lt;p&gt;So there’s something to migrate, generate a standard CRA TypeScript project:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-react-app my-app &lt;span class=&quot;token parameter variable&quot;&gt;--template&lt;/span&gt; typescript
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-app
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run start&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now run &lt;code&gt;npx npm-check-updates&lt;/code&gt; on that brand new project and look at how much of it is already out of date. That list is the whole argument for this article.&lt;/p&gt;
&lt;h3 id=&quot;swap-the-packages&quot;&gt;Swap the packages&lt;/h3&gt;
&lt;p&gt;Out with &lt;code&gt;react-scripts&lt;/code&gt;, in with Vite and the two plugins I always end up wanting:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; uninstall react-scripts
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; vite @vitejs/plugin-react --save-dev
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; vite-tsconfig-paths vite-plugin-svgr --save-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of these belong in &lt;code&gt;devDependencies&lt;/code&gt;. Nothing here ships to the browser, it only builds the things that do.&lt;/p&gt;
&lt;p&gt;There are &lt;a href=&quot;https://vitejs.dev/plugins/&quot;&gt;plenty more plugins&lt;/a&gt; available, but these two replace behaviour CRA gave you for free:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vite-tsconfig-paths&lt;/code&gt; makes Vite honour the &lt;code&gt;baseUrl&lt;/code&gt; and &lt;code&gt;paths&lt;/code&gt; you already have in &lt;code&gt;tsconfig.json&lt;/code&gt;, so &lt;code&gt;import Button from &amp;#39;components/button&amp;#39;&lt;/code&gt; keeps resolving instead of turning into &lt;code&gt;../../components/button&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vite-plugin-svgr&lt;/code&gt; lets you import an SVG as a React component rather than a URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One thing that will bite you on the SVG plugin. Version 4 landed a few days ago and changed the import syntax. The CRA-style named export is gone:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// CRA, and vite-plugin-svgr v3&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ReactComponent &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Logo &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./logo.svg'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// vite-plugin-svgr v4&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Logo &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./logo.svg?react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’re migrating an app with a hundred icon imports, that’s a find-and-replace across the codebase, and the error message you get otherwise (“does not provide an export named ‘ReactComponent’”) does not point at the plugin version at all. Pin the major version deliberately rather than discovering this later.&lt;/p&gt;
&lt;h3 id=&quot;write-the-config&quot;&gt;Write the config&lt;/h3&gt;
&lt;p&gt;Create a config file with &lt;code&gt;touch vite.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; react &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@vitejs/plugin-react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; viteTsconfigPaths &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite-tsconfig-paths'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; svgrPlugin &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'vite-plugin-svgr'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// see more at https://vitejs.dev/config/&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  plugins&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;viteTsconfigPaths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;svgrPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  server&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    port&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    open&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;server&lt;/code&gt; block is optional and I add it every time. Vite serves on 5173 by default, CRA used 3000, and if you have a backend with CORS rules or an OAuth callback registered against port 3000 you will notice immediately.&lt;/p&gt;
&lt;h3 id=&quot;move-indexhtml&quot;&gt;Move index.html&lt;/h3&gt;
&lt;p&gt;This is the part that feels wrong the first time. CRA keeps &lt;code&gt;index.html&lt;/code&gt; in &lt;code&gt;public/&lt;/code&gt; and treats it as a template. Vite treats it as the entry point of the application, so it goes in the project root:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; public/index.html index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vite still serves everything else in &lt;code&gt;public/&lt;/code&gt; at the root of the site, so &lt;code&gt;%PUBLIC_URL%&lt;/code&gt; has no job any more. Strip it out:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- before --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;icon&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;%PUBLIC_URL%/favicon.ico&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- after --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;icon&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/favicon.ico&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then point the page at your entry module. CRA injected this at build time, Vite expects it to be in the HTML:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;You need to enable JavaScript to run this app.&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- add this line below the root declaration --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;module&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/src/index.tsx&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the page loads blank with no errors in the console, this line is what’s missing. I’ve made that mistake more than once.&lt;/p&gt;
&lt;h3 id=&quot;sort-out-typescript&quot;&gt;Sort out TypeScript&lt;/h3&gt;
&lt;p&gt;Open &lt;code&gt;tsconfig.json&lt;/code&gt; and update the compiler target, because there’s no point shipping ES5 through a tool built around native modules:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;target&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ESNext&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;&quot;lib&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;dom&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dom.iterable&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;esnext&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CRA leaves behind &lt;code&gt;src/react-app-env.d.ts&lt;/code&gt;, which references &lt;code&gt;react-scripts&lt;/code&gt; types you just uninstalled. Delete it and write the Vite equivalent in its place:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; src/react-app-env.d.ts
&lt;span class=&quot;token function&quot;&gt;touch&lt;/span&gt; src/vite-env.d.ts&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/// &amp;lt;reference types=&quot;vite/client&quot; /&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/// &amp;lt;reference types=&quot;vite-plugin-svgr/client&quot; /&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line gives you types for &lt;code&gt;import.meta.env&lt;/code&gt;, the second for the &lt;code&gt;?react&lt;/code&gt; SVG imports. Put the file in &lt;code&gt;src/&lt;/code&gt;, not the project root, or it won’t be picked up by the default &lt;code&gt;include&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;environment-variables&quot;&gt;Environment variables&lt;/h3&gt;
&lt;p&gt;The quiet one. CRA exposed anything prefixed with &lt;code&gt;REACT_APP_&lt;/code&gt; on &lt;code&gt;process.env&lt;/code&gt;. Vite exposes anything prefixed with &lt;code&gt;VITE_&lt;/code&gt; on &lt;code&gt;import.meta.env&lt;/code&gt;, and &lt;code&gt;process&lt;/code&gt; doesn’t exist in the browser at all:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// CRA&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;REACT_APP_API_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Vite&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;VITE_API_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both the prefix and the object change, so every variable needs renaming in &lt;code&gt;.env&lt;/code&gt; and in the code. There’s no warning for this. You get &lt;code&gt;undefined&lt;/code&gt; at runtime, usually in the one code path that isn’t covered by a test.&lt;/p&gt;
&lt;p&gt;While you’re in there, &lt;code&gt;import.meta.env.MODE&lt;/code&gt;, &lt;code&gt;.DEV&lt;/code&gt; and &lt;code&gt;.PROD&lt;/code&gt; replace the &lt;code&gt;NODE_ENV&lt;/code&gt; checks.&lt;/p&gt;
&lt;h3 id=&quot;scripts&quot;&gt;Scripts&lt;/h3&gt;
&lt;p&gt;Finally, the entries in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;dev&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tsc &amp;amp;&amp;amp; vite build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;serve&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vite preview&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I keep &lt;code&gt;start&lt;/code&gt; as an alias for &lt;code&gt;dev&lt;/code&gt; so that muscle memory and any CI job referring to &lt;code&gt;npm start&lt;/code&gt; both keep working. &lt;code&gt;tsc &amp;amp;&amp;amp; vite build&lt;/code&gt; runs the type check before the bundle, because Vite strips types with esbuild and will happily build code that doesn’t type-check.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;npm install &amp;amp;&amp;amp; npm run start&lt;/code&gt; and you should be looking at the same app, starting considerably faster.&lt;/p&gt;
&lt;p&gt;Note there’s no &lt;code&gt;test&lt;/code&gt; script here. &lt;code&gt;react-scripts test&lt;/code&gt; was Jest with a pile of configuration behind it, and that configuration left with the package. That’s the subject of the next article.&lt;/p&gt;
&lt;h3 id=&quot;starting-fresh-instead&quot;&gt;Starting fresh instead&lt;/h3&gt;
&lt;p&gt;If you’re not migrating anything and just want a new project, skip all of the above:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; create vite@latest my-app -- &lt;span class=&quot;token parameter variable&quot;&gt;--template&lt;/span&gt; react-ts
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; my-app
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run dev&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note it’s &lt;code&gt;npm run dev&lt;/code&gt; and not &lt;code&gt;npm run start&lt;/code&gt;, which catches me out roughly every second time. Pick &lt;code&gt;react-ts&lt;/code&gt; rather than &lt;code&gt;react&lt;/code&gt; unless you have a reason to avoid TypeScript. The full list of templates lives in the &lt;a href=&quot;https://github.com/vitejs/vite/tree/main/packages/create-vite&quot;&gt;create-vite&lt;/a&gt; repository, and the React one is a genuinely small amount of code, which after years of squinting at CRA’s ejected webpack config is a relief.&lt;/p&gt;
&lt;h3 id=&quot;whats-still-missing&quot;&gt;What’s still missing&lt;/h3&gt;
&lt;p&gt;The app runs. Linting, formatting and tests do not, because all three came bundled with &lt;code&gt;react-scripts&lt;/code&gt; and left with it. That’s what I’ll set up in the next article, along with the leftovers in &lt;code&gt;package.json&lt;/code&gt; that nobody needs any more.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Exploring React's useContext Hook]]></title><description><![CDATA[How React's useContext hook removes prop drilling, plus the provider guard and the re-render trap that catch most people out on their first attempt at it.]]></description><link>https://angelo.dini.dev/blog/react-use-context/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-use-context/</guid><pubDate>Sun, 20 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;four-levels-down-and-still-passing-props&quot;&gt;Four levels down and still passing props&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;There's a moment in every React app where you add a prop to a component that doesn't use it, purely so it can hand it to a child that hands it to a child. That's the point at which I go looking for context.&lt;/p&gt;&lt;p&gt;&lt;code&gt;useContext&lt;/code&gt; lets a component read a value from an ancestor without every component in between having to carry it. Themes, the current user, a locale, an open/closed flag for something global. It’s a transport mechanism, not a state manager, and keeping that distinction straight saves a lot of grief later.&lt;/p&gt;
&lt;h3 id=&quot;the-version-with-prop-drilling&quot;&gt;The version with prop drilling&lt;/h3&gt;
&lt;p&gt;Take a navigation bar that tracks which item is active. Three files, and two of them are only involved because of the wiring:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Navbar &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'components/nav'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Home'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;app&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Navbar&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;activeItem&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;setActiveItem&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* other stuff */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; App&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// nav.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; NavItem &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'components/nav-item'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Navbar&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;nav&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavItem&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Home&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;activeItem&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;setActiveItem&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavItem&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;About&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;activeItem&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;setActiveItem&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;nav&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Navbar&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// nav-item.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;NavItem&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleClick&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setActiveItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; activeItem &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'active'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; NavItem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Navbar&lt;/code&gt; does nothing with &lt;code&gt;activeItem&lt;/code&gt; or &lt;code&gt;setActiveItem&lt;/code&gt;. It is a courier. With two levels that’s tolerable, and by the time there’s a &lt;code&gt;NavGroup&lt;/code&gt; and a &lt;code&gt;NavDropdown&lt;/code&gt; in the middle it isn’t. Every new piece of navigation state means touching every file in the chain.&lt;/p&gt;
&lt;h3 id=&quot;the-same-thing-with-context&quot;&gt;The same thing with context&lt;/h3&gt;
&lt;p&gt;Put the state in a provider and let the components that care read it directly:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// nav-context.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; NavContext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;NavContextProvider&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Home'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContext.Provider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContext.Provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;useNavContext&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; context &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;NavContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'useNavContext must be used inside a NavContextProvider'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; context&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NavContextProvider&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useNavContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// app.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Navbar &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'components/nav'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; NavContextProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'components/nav-context'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContextProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;app&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Navbar&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* other stuff */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContextProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; App&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// nav.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; NavItem &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'components/nav-item'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Navbar&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;nav&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavItem&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Home&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavItem&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;About&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;nav&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Navbar&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// nav-item.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useNavContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'components/nav-context'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;NavItem&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useNavContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleClick&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setActiveItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; activeItem &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'active'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; NavItem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Navbar&lt;/code&gt; is back to describing navigation instead of relaying props. Adding a third level of nesting costs nothing.&lt;/p&gt;
&lt;h4 id=&quot;always-export-a-hook-never-the-context&quot;&gt;Always export a hook, never the context&lt;/h4&gt;
&lt;p&gt;The thing I’d insist on from that file is &lt;code&gt;useNavContext&lt;/code&gt;. Exporting the raw context and calling &lt;code&gt;useContext(NavContext)&lt;/code&gt; at every call site works, but it gives you no place to put the guard.&lt;/p&gt;
&lt;p&gt;Without a guard, a component rendered outside the provider gets the default value back and fails somewhere else entirely, usually as &lt;code&gt;Cannot destructure property &amp;#39;activeItem&amp;#39; of undefined&lt;/code&gt;. With the guard you get a sentence telling you exactly what went wrong. That’s the difference between a ten-second fix and twenty minutes with the component tree open.&lt;/p&gt;
&lt;p&gt;It’s also the seam where you’d later add a selector, a &lt;code&gt;console.count&lt;/code&gt; while debugging, or a TypeScript type, without touching a single consumer.&lt;/p&gt;
&lt;h3 id=&quot;the-catch-nobody-mentions-in-the-tutorial&quot;&gt;The catch nobody mentions in the tutorial&lt;/h3&gt;
&lt;p&gt;Look at the provider again:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContext.Provider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That object literal is rebuilt on every single render of the provider. Context compares by reference, so every consumer re-renders every time, even the ones reading a field that didn’t change. &lt;code&gt;React.memo&lt;/code&gt; on a consumer does not help, because the update comes through the context rather than through props.&lt;/p&gt;
&lt;p&gt;For a navbar with four items, this genuinely does not matter and I would leave it alone. For a provider sitting near the root of a large tree, it does. The fix is to keep the value’s identity stable:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useMemo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;NavContextProvider&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Home'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useMemo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContext.Provider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavContext.Provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setActiveItem&lt;/code&gt; is stable across renders because React guarantees the identity of a &lt;code&gt;useState&lt;/code&gt; setter, so in practice only &lt;code&gt;activeItem&lt;/code&gt; moves the needle. I still list it in the dependency array to keep the lint rule quiet. If you put callbacks of your own in the value, wrap them in &lt;code&gt;useCallback&lt;/code&gt; for the same reason, which I went into in &lt;a href=&quot;/blog/react-use-callback/&quot;&gt;Optimizing React Components&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;splitting-state-from-setters&quot;&gt;Splitting state from setters&lt;/h4&gt;
&lt;p&gt;There’s a second move worth knowing, and it beats memoisation when the state changes often. Use two contexts:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; NavStateContext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; NavDispatchContext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;NavContextProvider&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Home'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavDispatchContext.Provider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;setActiveItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavStateContext.Provider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;activeItem&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavStateContext.Provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NavDispatchContext.Provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A component that only dispatches, like a button that resets the navigation, subscribes to &lt;code&gt;NavDispatchContext&lt;/code&gt; and never re-renders when &lt;code&gt;activeItem&lt;/code&gt; changes. The setter’s identity is stable, so that context’s value never changes at all. Two providers is slightly more typing for a real reduction in work.&lt;/p&gt;
&lt;h3 id=&quot;when-i-dont-reach-for-it&quot;&gt;When I don’t reach for it&lt;/h3&gt;
&lt;p&gt;Context is not free and it is not a store. Some cases where I leave it alone:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The tree is two levels deep.&lt;/strong&gt; Passing a prop twice is clearer than a provider, an exported hook and an extra file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The data changes on every keystroke.&lt;/strong&gt; Form state in context makes the whole subtree re-render as you type. Keep it local, lift it only as far as it needs to go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want a store.&lt;/strong&gt; If you need selectors, middleware, devtools or persistence, use something built for that. Context plus &lt;code&gt;useReducer&lt;/code&gt; gets you a surprising distance, and then it stops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server data.&lt;/strong&gt; Caching, revalidation and request deduplication are a different problem, and a query library solves it properly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The failure mode I’ve watched happen is one giant &lt;code&gt;AppContext&lt;/code&gt; holding everything, which re-renders the application whenever anything anywhere changes. That’s a worse position than the prop drilling it replaced, and it’s harder to unpick.&lt;/p&gt;
&lt;h3 id=&quot;what-id-take-away&quot;&gt;What I’d take away&lt;/h3&gt;
&lt;p&gt;Small, focused providers close to where they’re used. One hook per context, with a guard in it. And a &lt;code&gt;useMemo&lt;/code&gt; on the value if the provider sits anywhere near the root.&lt;/p&gt;
&lt;p&gt;That’s about it. &lt;code&gt;useContext&lt;/code&gt; is one of the few React APIs that’s genuinely simple, and almost all the trouble people have with it comes from asking it to be a state manager.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Understanding TypeScript Unions]]></title><description><![CDATA[A practical guide to TypeScript union types in React, covering narrowing, discriminated unions, intersections and the pitfalls that quietly cost you time.]]></description><link>https://angelo.dini.dev/blog/typescript-union/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/typescript-union/</guid><pubDate>Tue, 18 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-pipe-character-does-a-lot-of-work&quot;&gt;The pipe character does a lot of work&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;A union type is the smallest possible piece of TypeScript syntax and one of the easiest to get subtly wrong. You write a pipe between two types and the compiler starts asking questions you didn't expect.&lt;/p&gt;&lt;p&gt;I want to go through how I actually use unions in React work: on props, on reducer actions, and in the places where the compiler stops helping and you have to give it a hand. There is also a small pile of things I got wrong for longer than I’d like to admit, so those get their own section at the end.&lt;/p&gt;
&lt;h3 id=&quot;what-a-union-actually-is&quot;&gt;What a union actually is&lt;/h3&gt;
&lt;p&gt;A union says a value is one of several types, but not which one. You write it with a pipe:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MyUnion&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MyUnion&lt;/code&gt; holds either a number or a string. The important half of that sentence is the part people skip: until you prove which one it is, TypeScript will only let you use the members that are common to both. &lt;code&gt;.toFixed()&lt;/code&gt; is off limits, &lt;code&gt;.toUpperCase()&lt;/code&gt; is off limits, and &lt;code&gt;.toString()&lt;/code&gt; is fine because both have it.&lt;/p&gt;
&lt;p&gt;Proving which one it is called narrowing, and it is most of the work.&lt;/p&gt;
&lt;h4 id=&quot;function-parameters&quot;&gt;Function parameters&lt;/h4&gt;
&lt;p&gt;The classic case is a parameter that accepts one thing or a list of them:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;greet&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;names&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;names&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    names&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Hello, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Hello, &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;names&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;greet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Alice'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// output: Hello, Alice!&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;greet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Bob'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Charlie'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// output: Hello, Bob! Hello, Charlie!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;names&lt;/code&gt; is &lt;code&gt;string[]&lt;/code&gt;. Inside the &lt;code&gt;else&lt;/code&gt;, it’s &lt;code&gt;string&lt;/code&gt;. &lt;code&gt;Array.isArray&lt;/code&gt; is one of the checks TypeScript understands natively, along with &lt;code&gt;typeof&lt;/code&gt;, &lt;code&gt;instanceof&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, and a plain truthiness test.&lt;/p&gt;
&lt;h4 id=&quot;conditional-rendering-in-react&quot;&gt;Conditional rendering in React&lt;/h4&gt;
&lt;p&gt;Same idea, applied to props. A component that renders differently depending on what it was handed:&lt;/p&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DisplayProps&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Display&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;DisplayProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'number'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Number: &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;String: &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Display&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;42&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* output: Number: 42 */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;

      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Display&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Hello, TypeScript!&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* output: String: Hello, TypeScript! */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;typeof&lt;/code&gt; check does the narrowing, so both branches know what they’re holding.&lt;/p&gt;
&lt;p&gt;Where unions really earn their place on props is string literals. &lt;code&gt;variant: &amp;#39;primary&amp;#39; | &amp;#39;secondary&amp;#39; | &amp;#39;danger&amp;#39;&lt;/code&gt; gives you autocomplete in the editor and a compile error on a typo, which is roughly a hundred times more useful than &lt;code&gt;variant: string&lt;/code&gt; and costs nothing.&lt;/p&gt;
&lt;h3 id=&quot;reducer-actions&quot;&gt;Reducer actions&lt;/h3&gt;
&lt;p&gt;This is where I use unions most, and it’s the shape that made them click for me. A reducer takes one of a fixed set of actions, and the action’s &lt;code&gt;type&lt;/code&gt; field tells you which:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterAction&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; payload&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'decrement'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; payload&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'reset'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterState&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;reset&lt;/code&gt; has no payload at all. That’s deliberate, and it’s the part a single flat interface with optional fields cannot express. With &lt;code&gt;{ type: string; payload?: number }&lt;/code&gt; you can dispatch &lt;code&gt;{ type: &amp;#39;reset&amp;#39;, payload: 42 }&lt;/code&gt; and nobody stops you.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; counterReducer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CounterState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  action&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CounterAction
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CounterState &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;action&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; action&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'decrement'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; action&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'reset'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;increment&lt;/code&gt; case, &lt;code&gt;action.payload&lt;/code&gt; is a number and the compiler knows it. Inside &lt;code&gt;reset&lt;/code&gt;, reaching for &lt;code&gt;action.payload&lt;/code&gt; is an error, because that member has no such field.&lt;/p&gt;
&lt;h3 id=&quot;discriminated-unions&quot;&gt;Discriminated unions&lt;/h3&gt;
&lt;p&gt;What makes the reducer above work is that every member has a property with a literal type, and the literals are all different. TypeScript calls that a discriminant, and a union built this way is a discriminated union. Switch on it and each &lt;code&gt;case&lt;/code&gt; narrows the whole object rather than only the field you tested.&lt;/p&gt;
&lt;p&gt;The discriminant does not have to be called &lt;code&gt;type&lt;/code&gt; and does not have to be a string. Booleans work fine, which is handy for a two-state shape:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Guest&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  isGuest&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RegisteredUser&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  isGuest&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Guest &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; RegisteredUser&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getContact&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; User&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isGuest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note &lt;code&gt;isGuest: true&lt;/code&gt;, not &lt;code&gt;isGuest: boolean&lt;/code&gt;. That distinction is the entire mechanism. A &lt;code&gt;boolean&lt;/code&gt; field is not a discriminant, it’s just a field, and narrowing will not happen.&lt;/p&gt;
&lt;h4 id=&quot;exhaustiveness-checking&quot;&gt;Exhaustiveness checking&lt;/h4&gt;
&lt;p&gt;Here’s the feature that pays for the rest of it. Assign the narrowed value to &lt;code&gt;never&lt;/code&gt; in the default branch:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; counterReducer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CounterState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  action&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CounterAction
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CounterState &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;action&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; action&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'decrement'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; action&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'reset'&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; count&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; unhandled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;never&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; action&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Unhandled action: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;unhandled&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If every case is covered, &lt;code&gt;action&lt;/code&gt; in the default branch has type &lt;code&gt;never&lt;/code&gt; and the assignment compiles. The moment somebody adds &lt;code&gt;{ type: &amp;#39;set&amp;#39;; payload: number }&lt;/code&gt; to &lt;code&gt;CounterAction&lt;/code&gt; and forgets the reducer, that line stops compiling and names the type it can’t assign. I’ve caught more bugs with this five-line pattern than with any amount of unit testing around reducers.&lt;/p&gt;
&lt;h3 id=&quot;combining-unions-with-intersections&quot;&gt;Combining unions with intersections&lt;/h3&gt;
&lt;p&gt;Unions and intersections are often introduced as a pair, which is a bit misleading because they do opposite things. A union is “one of these”. An intersection, written with &lt;code&gt;&amp;amp;&lt;/code&gt;, is “all of these at once”.&lt;/p&gt;
&lt;p&gt;They compose usefully. If several members share fields, factor the shared part out and intersect it with the union of the variable parts:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;WithId&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Guest&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; kind&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'guest'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Registered&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; kind&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'registered'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; WithId &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Guest &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Registered&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;User&lt;/code&gt; is equivalent to &lt;code&gt;(WithId &amp;amp; Guest) | (WithId &amp;amp; Registered)&lt;/code&gt;, so &lt;code&gt;id&lt;/code&gt; is available everywhere and &lt;code&gt;email&lt;/code&gt; only appears once you’ve narrowed on &lt;code&gt;kind&lt;/code&gt;. The intersection distributes over the union. For two members this is more ceremony than it’s worth, but at five or six shared fields it stops the shape from drifting apart.&lt;/p&gt;
&lt;h4 id=&quot;distribution-and-why-exclude-works&quot;&gt;Distribution, and why &lt;code&gt;Exclude&lt;/code&gt; works&lt;/h4&gt;
&lt;p&gt;Most of the utility types you already use are built on the fact that conditional types distribute over unions. &lt;code&gt;Exclude&amp;lt;&amp;#39;a&amp;#39; | &amp;#39;b&amp;#39; | &amp;#39;c&amp;#39;, &amp;#39;a&amp;#39;&amp;gt;&lt;/code&gt; gives &lt;code&gt;&amp;#39;b&amp;#39; | &amp;#39;c&amp;#39;&lt;/code&gt; because the condition is applied to each member separately and the results are unioned back together.&lt;/p&gt;
&lt;p&gt;That’s worth knowing mostly so you’re not surprised by it. It’s how you take an existing prop union and derive a narrower one, rather than writing the list out twice and letting the two copies drift.&lt;/p&gt;
&lt;h3 id=&quot;pitfalls&quot;&gt;Pitfalls&lt;/h3&gt;
&lt;h4 id=&quot;a-union-of-object-types-without-a-discriminant&quot;&gt;A union of object types without a discriminant&lt;/h4&gt;
&lt;p&gt;This is the mistake I made for a long time:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Success&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Failure&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Success &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Failure&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It reads perfectly well and it is annoying to use. You cannot write &lt;code&gt;result.data&lt;/code&gt;, because &lt;code&gt;Failure&lt;/code&gt; has no &lt;code&gt;data&lt;/code&gt;. There is nothing to switch on. You end up with &lt;code&gt;&amp;#39;data&amp;#39; in result&lt;/code&gt; checks scattered through the code, which works but reads like an apology.&lt;/p&gt;
&lt;p&gt;Add a discriminant and the problem disappears:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'success'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'error'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you’re modelling API responses this way, it pairs nicely with the shape normalisation I wrote about in &lt;a href=&quot;/blog/normalizr/&quot;&gt;Using Normalizr&lt;/a&gt;: decide the shape once, at the boundary, and the rest of the app stops guessing.&lt;/p&gt;
&lt;h4 id=&quot;widening-swallows-your-literals&quot;&gt;Widening swallows your literals&lt;/h4&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Size&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'sm'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'md'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That looks like “one of these two, or any other string”. It is not. &lt;code&gt;&amp;#39;sm&amp;#39;&lt;/code&gt; and &lt;code&gt;&amp;#39;md&amp;#39;&lt;/code&gt; are both assignable to &lt;code&gt;string&lt;/code&gt;, so the whole thing collapses to &lt;code&gt;string&lt;/code&gt; and your autocomplete quietly disappears. There’s no error, which is what makes it nasty.&lt;/p&gt;
&lt;p&gt;If you genuinely want the suggestions plus an escape hatch, the workaround is to stop the collapse:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Size&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'sm'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'md'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s ugly. It works, the editor keeps suggesting &lt;code&gt;sm&lt;/code&gt; and &lt;code&gt;md&lt;/code&gt;, and I use it perhaps twice a year.&lt;/p&gt;
&lt;h4 id=&quot;too-many-members&quot;&gt;Too many members&lt;/h4&gt;
&lt;p&gt;A union with fifteen members is a signal, not an achievement. Somewhere in there is a second concept trying to get out. Split it, or find the field the members actually vary on and make that the discriminant.&lt;/p&gt;
&lt;p&gt;The same thing applies to a component whose props are a union of four completely different shapes. That’s usually four components sharing a name because nobody wanted to pick three more. Some of this is just &lt;a href=&quot;/blog/idiomatic-javascript/&quot;&gt;idiomatic programming&lt;/a&gt; applied to types: the clearest version is the one the next person can read without a map.&lt;/p&gt;
&lt;h3 id=&quot;where-id-start&quot;&gt;Where I’d start&lt;/h3&gt;
&lt;p&gt;If you take unions no further than string literal props and one discriminated reducer action type with a &lt;code&gt;never&lt;/code&gt; check in the default branch, you have most of the value. Everything above that is refinement.&lt;/p&gt;
&lt;p&gt;The one thing I’d add on top is the habit of asking, whenever a field is optional, whether it’s actually optional or whether there are two shapes hiding behind one interface. It usually is two shapes.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[AI and Development]]></title><description><![CDATA[A working look at AI coding tools in 2023, from Copilot completions to code review, test generation and refactoring, and where the marketing runs ahead.]]></description><link>https://angelo.dini.dev/blog/how-ai-changes-coding/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/how-ai-changes-coding/</guid><pubDate>Tue, 13 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-autocomplete-grew-a-personality&quot;&gt;The autocomplete grew a personality&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Half the internet spent this spring announcing that programming is finished, and the other half announcing that the whole thing is a parlour trick. I don't think either group has had the tools switched on in a real project for very long.&lt;/p&gt;&lt;p&gt;So instead of guessing, I want to walk through the four places this stuff actually touches my day: writing code, reviewing it, testing it and rewriting it. Some of it has earned a keyboard shortcut. A surprising amount of it is a demo with a pricing page attached.&lt;/p&gt;
&lt;p&gt;One thing up front, and it connects to what I wrote about &lt;a href=&quot;/blog/idiomatic-javascript/&quot;&gt;idiomatic programming&lt;/a&gt;: a machine that produces code nobody on your team would write by hand has not saved you anything. It has moved the work from typing to reading, and reading is the expensive part.&lt;/p&gt;
&lt;h3 id=&quot;completion-and-what-it-is-good-at&quot;&gt;Completion, and what it is good at&lt;/h3&gt;
&lt;p&gt;The current generation of tooling starts with Codex, the model OpenAI trained on public code and shipped in 2021. It is the engine that GitHub Copilot was built on. If you went looking for the Codex API this spring, you found a tombstone: OpenAI deprecated it in March with three days’ notice and pointed everyone at GPT-3.5 and GPT-4 instead. Three days. That says more about building on somebody else’s model endpoint than any benchmark could, and it has nothing to do with whether the model was any good.&lt;/p&gt;
&lt;p&gt;What survived is the editor integration, and that part I genuinely like. Copilot is at its best on the code I resent writing: the fifth case in a reducer, a test fixture with twelve fields, the regular expression I would otherwise have gone looking for. It reads the open buffers, notices the pattern I set up two files ago, and finishes the shape.&lt;/p&gt;
&lt;p&gt;It is much worse at anything it has not seen a thousand times before. It will invent a method on a library that has no such method, give it the right naming convention and a completely plausible signature, and offer it with total confidence. Wrong code that looks like your code is more expensive to review than wrong code that looks wrong. That is the actual tax, and nobody puts it on the landing page.&lt;/p&gt;
&lt;p&gt;Worth having a look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt; suggests completions inline as you type, using the surrounding files as context. It’s the one I’d start with. &lt;a href=&quot;https://github.com/features/copilot/&quot;&gt;Their site&lt;/a&gt; has the current plans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tabnine&lt;/strong&gt; does the same job and will run models locally, which matters a lot if your employer has feelings about source code leaving the building. &lt;a href=&quot;https://www.tabnine.com/&quot;&gt;tabnine.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;ChatGPT&lt;/strong&gt; tab is a different tool entirely. No editor context, so no completions, but far better for “explain this stack trace” or “what is this build error actually telling me”.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;review-is-where-half-the-category-is-a-rebrand&quot;&gt;Review is where half the category is a rebrand&lt;/h3&gt;
&lt;p&gt;Snyk Code is the interesting one here, and it has a Swiss origin story. DeepCode came out of the Secure, Reliable and Intelligent Systems lab at ETH Zurich, learned issue patterns from a very large pile of open source repositories, and got bought by Snyk in 2020. It now ships as Snyk Code inside their platform. Nice to see something out of Zurich end up in that many editors.&lt;/p&gt;
&lt;p&gt;The rest of the category is thinner than the marketing suggests. Code Climate is a maintainability and static analysis platform. DeepSource is a static analyser with automatic fixes. Both are useful, and neither is doing anything a linter author would recognise as machine learning. Putting “AI” on the box sells seats.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Snyk Code&lt;/strong&gt; (formerly DeepCode) flags security and reliability issues from learned patterns rather than hand-written rules. &lt;a href=&quot;https://snyk.io/platform/deepcode-ai/&quot;&gt;snyk.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Climate&lt;/strong&gt; grades maintainability and tracks churn against complexity. &lt;a href=&quot;https://codeclimate.com/&quot;&gt;codeclimate.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeepSource&lt;/strong&gt; analyses on every push and can open the fix as a pull request. &lt;a href=&quot;https://deepsource.io/&quot;&gt;deepsource.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The honest pitch for all three is that they raise the floor. They catch the null dereference at two in the morning that a tired reviewer waves through. They will not tell you that the abstraction is wrong, and that is still the review comment that matters most.&lt;/p&gt;
&lt;h3 id=&quot;tests-are-where-it-surprised-me&quot;&gt;Tests are where it surprised me&lt;/h3&gt;
&lt;p&gt;I expected test generation to be the weakest of the four and it is the one that changed my mind.&lt;/p&gt;
&lt;p&gt;Diffblue Cover reads Java bytecode, uses reinforcement learning to choose inputs that reach each path through a method, then writes JUnit tests with the mocks and assertions in place. The important detail is that it executes what it writes, so the output compiles and runs. That is a very different bargain from asking a language model for a test file and discovering the problem three commits later.&lt;/p&gt;
&lt;p&gt;Testim goes at the other end of the pipeline. The real cost of an end-to-end suite is not writing it, it is the selectors breaking every time somebody restructures a component, and Testim uses a model to pick locators that survive that. It’s a neighbour of the problem I looked at in &lt;a href=&quot;/blog/screenshot-testing/&quot;&gt;screenshot testing&lt;/a&gt;, where the suite tells you something changed without telling you whether it matters.&lt;/p&gt;
&lt;p&gt;One correction while I’m here, because I keep seeing it repeated. DeepMind’s AlphaCode, from early 2022, is not a bug-finding system. It solves competitive programming problems by sampling an enormous number of candidate programs and filtering them against the example tests, and it placed roughly in the middle of the Codeforces field. Genuinely impressive research. It is not something you can point at your repository, and citing it as automated QA is just wrong.&lt;/p&gt;
&lt;h3 id=&quot;refactoring-is-still-hand-work&quot;&gt;Refactoring is still hand work&lt;/h3&gt;
&lt;p&gt;Facebook published Aroma back in 2019 and it gets described badly almost everywhere, including by me until I went and read the paper. It indexes a large corpus of code, takes a partial snippet as input, finds method bodies that are structurally similar, and clusters them into a handful of recommendations. It is code-to-code search. It does not remove redundant operations or improve your memory management, whatever the summaries say.&lt;/p&gt;
&lt;p&gt;Sourcery is the closest thing to a daily-use refactoring assistant I’ve found, and it’s Python only. It sits in the editor and suggests the tidier form of what you just wrote, which is pleasant and occasionally slightly humbling.&lt;/p&gt;
&lt;p&gt;But refactoring worth the name is not about rewriting a loop as a comprehension. It’s about moving a boundary, splitting a module that grew two responsibilities, deciding that a prop should have been context all along. That work needs the argument you had in the planning meeting, and none of these tools were in the room.&lt;/p&gt;
&lt;h3 id=&quot;what-stays-switched-on&quot;&gt;What stays switched on&lt;/h3&gt;
&lt;p&gt;Copilot stays on, because the boilerplate tax is real and it pays that for me. Snyk Code and DeepSource stay in CI, because a second opinion that never gets tired is worth the money. Test generation I would reach for on a Java service, not on a React frontend.&lt;/p&gt;
&lt;p&gt;What I don’t do is let a suggestion into a commit I haven’t read line by line. That rule has not changed and I can’t see it changing.&lt;/p&gt;
&lt;p&gt;This article is in part written using AI technologies 🤣&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Understanding React Transitions]]></title><description><![CDATA[How startTransition marks React 18 state updates as non-urgent, why the callback has to be synchronous, and where transitions help and where they do not.]]></description><link>https://angelo.dini.dev/blog/react-transition/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-transition/</guid><pubDate>Sat, 27 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;not-every-update-is-urgent&quot;&gt;Not every update is urgent&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Type into a search box that filters a long list and you can usually feel the problem. The characters lag behind your fingers, because React is busy re-rendering a few thousand rows for a query you have already moved on from. &lt;code&gt;startTransition&lt;/code&gt; is how you tell React which of those two jobs matters more.&lt;/p&gt;&lt;p&gt;I mentioned transitions in passing when I wrote about &lt;a href=&quot;/blog/react-18/&quot;&gt;React 18&lt;/a&gt;, but a paragraph does not do the idea justice, and the API has one sharp edge that is easy to walk into.&lt;/p&gt;
&lt;h3 id=&quot;what-a-transition-is&quot;&gt;What a transition is&lt;/h3&gt;
&lt;p&gt;Before React 18, every state update was equally important. React took them, rendered them, committed them, and the rendering could not be interrupted once it started. If a render took 300ms, your keystroke waited 300ms.&lt;/p&gt;
&lt;p&gt;The concurrent renderer changed that by letting React work on an update, pause, handle something more important, and come back. &lt;code&gt;startTransition&lt;/code&gt; is the label you attach to an update to say “this one can wait”:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; startTransition &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// urgent: the input must reflect the keystroke immediately&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;setQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// non-urgent: the filtered results can catch up&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setSearchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two state updates, two priorities. React commits &lt;code&gt;setQuery&lt;/code&gt; straight away so the input stays responsive. The transition update renders in the background, and if another keystroke arrives before it finishes, React throws that work away and starts again with the newer value. Nothing half-rendered ever reaches the screen.&lt;/p&gt;
&lt;p&gt;That interruptibility is the whole feature. It is not batching, and it is worth being clear about that because the two get confused. Automatic batching, also new in React 18, groups multiple &lt;code&gt;setState&lt;/code&gt; calls into one render. Transitions decide which renders get to go first. You get automatic batching whether or not you use transitions.&lt;/p&gt;
&lt;h3 id=&quot;the-callback-has-to-be-synchronous&quot;&gt;The callback has to be synchronous&lt;/h3&gt;
&lt;p&gt;Here is the sharp edge. React marks every state update that happens &lt;em&gt;while the callback runs&lt;/em&gt; as a transition. Updates scheduled after an &lt;code&gt;await&lt;/code&gt; or inside a &lt;code&gt;.then&lt;/code&gt; are not part of it, because by the time they fire, the callback has long since returned.&lt;/p&gt;
&lt;p&gt;So this does not do what it looks like it does:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// this does NOT mark setData as a transition&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;fetchData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fetchData()&lt;/code&gt; is called synchronously, the promise is handed back, and &lt;code&gt;startTransition&lt;/code&gt; returns. Whenever the response eventually arrives, React has no idea it was supposed to be related to a transition. &lt;code&gt;setData&lt;/code&gt; is treated as a perfectly ordinary urgent update.&lt;/p&gt;
&lt;p&gt;The fix is to wrap the update itself, on the other side of the await:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;fetchData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The React team calls this a known limitation rather than a design decision, so I expect it to change eventually. Until it does, the rule is simple enough: if you can see an &lt;code&gt;await&lt;/code&gt;, a &lt;code&gt;.then&lt;/code&gt; or a &lt;code&gt;setTimeout&lt;/code&gt; inside your &lt;code&gt;startTransition&lt;/code&gt; callback, the updates after it are not transitions.&lt;/p&gt;
&lt;p&gt;A subscription callback is fine, because the update inside it is synchronous:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; startTransition&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;ChatApplication&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMessages&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; subscription &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;subscribeToNewMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;setMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;previous&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;previous&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; subscription&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unsubscribe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;messages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Message&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a busy channel this keeps the message list from monopolising the main thread. If someone is typing a reply while forty messages land, the input wins.&lt;/p&gt;
&lt;h3 id=&quot;usetransition-when-you-want-a-spinner&quot;&gt;useTransition when you want a spinner&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;startTransition&lt;/code&gt; on its own gives you no way to know whether the work is still going. The hook version does:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useTransition&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Tabs&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startTransition&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;tab&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setTab&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'home'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;selectTab&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;next&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setTab&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; isPending &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.6&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TabButtons&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onSelect&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;selectTab&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TabPanel&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;tab&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;tab&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tab button responds instantly, the old panel stays on screen while the new one renders, and &lt;code&gt;isPending&lt;/code&gt; lets you dim it or show an indicator instead of a blank gap. This is the pattern I use most. Dimming the outgoing content reads far better than swapping it for a skeleton, and it is a one-line change once the transition is in place.&lt;/p&gt;
&lt;p&gt;There is also &lt;code&gt;useDeferredValue&lt;/code&gt;, which comes at the same problem from the other end. Rather than marking the update, you take a value and get back a copy that lags behind:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; deferredQuery &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useDeferredValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use it when you do not own the &lt;code&gt;setState&lt;/code&gt; call, typically because the value arrives as a prop from somewhere above you. Same underlying machinery, different handle on it.&lt;/p&gt;
&lt;h3 id=&quot;caveats-worth-knowing&quot;&gt;Caveats worth knowing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You can only wrap updates whose setter you have direct access to. If the value comes in as a prop, &lt;code&gt;useDeferredValue&lt;/code&gt; is the tool, not &lt;code&gt;startTransition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Transitions cannot control a controlled text input. The value has to update urgently or typing breaks, which is exactly why the search example splits into two state updates rather than one.&lt;/li&gt;
&lt;li&gt;A transition will be interrupted by any urgent update, and the work done so far is discarded. That is the point, but it means a transition can render several times before it commits once. Keep the render function pure and free of side effects, which you should be doing anyway.&lt;/li&gt;
&lt;li&gt;If several transitions are in flight, React currently batches them together rather than tracking them separately.&lt;/li&gt;
&lt;li&gt;Errors thrown while rendering a transition are caught by the nearest error boundary, the same as any other render error. No special handling needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-it-does-not-help&quot;&gt;Where it does not help&lt;/h3&gt;
&lt;p&gt;Transitions reorder work. They do not make work smaller. If your list takes 300ms to render, wrapping the update in a transition keeps the input responsive, but the results still take 300ms to appear, and now they appear at an unpredictable moment.&lt;/p&gt;
&lt;p&gt;So before reaching for this, check whether the render is expensive for a reason you can fix: virtualise the long list, memoise the row component, stop recomputing a derived array on every pass. Those give you an outright faster app. A transition redistributes the cost so the user notices it less, which is genuinely valuable, but it is a second step and not a substitute for the first.&lt;/p&gt;
&lt;p&gt;I have found the honest test to be whether you can name the urgent update. If there is a keystroke, a click or a tab switch that must respond immediately, a transition has something to protect. If there is no urgent update in the picture, wrapping things in &lt;code&gt;startTransition&lt;/code&gt; is just moving deck chairs.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Optimising React Components]]></title><description><![CDATA[What useCallback really does in React 18, why it only pays off next to React.memo or a hook dependency array, and the cases where you can safely skip it.]]></description><link>https://angelo.dini.dev/blog/react-use-callback/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-use-callback/</guid><pubDate>Fri, 14 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-hook-you-probably-do-not-need-yet&quot;&gt;The hook you probably do not need yet&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;useCallback is the hook people reach for first when a React app feels slow, and it is the one that does nothing at all in most of the places it gets used. It is worth understanding exactly what it buys you before sprinkling it over a component tree.&lt;/p&gt;&lt;p&gt;React re-renders a component when its state or props change, and re-renders every child underneath it while it is at it. That is the default, and it is usually fine. When it stops being fine, &lt;code&gt;useCallback&lt;/code&gt; is one of the tools available, alongside &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;React.memo&lt;/code&gt;, all of which arrived properly into everyday use around the same time as the &lt;a href=&quot;/blog/react-18/&quot;&gt;React 18&lt;/a&gt; concurrent renderer.&lt;/p&gt;
&lt;h3 id=&quot;what-it-actually-does&quot;&gt;What it actually does&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useCallback&lt;/code&gt; returns the same function reference across renders, as long as the values in its dependency array have not changed:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useCallback &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setCount&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleClick &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;count &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;Count&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Increment&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the hook, &lt;code&gt;handleClick&lt;/code&gt; would be a brand new function object on every render. With it, React hands back the one it stored last time until &lt;code&gt;count&lt;/code&gt; changes.&lt;/p&gt;
&lt;p&gt;Now the awkward part. In the example above that is worth precisely nothing. The only consumer of &lt;code&gt;handleClick&lt;/code&gt; is a DOM &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, and the DOM does not care whether the handler is the same object as last time. React will attach it either way. The component re-renders exactly as often as it did before, the counter behaves identically, and you have added a hook, an array and a dependency to maintain.&lt;/p&gt;
&lt;p&gt;I keep this example in because it is the shape you will find in most tutorials, and it is worth knowing that the shape is a no-op.&lt;/p&gt;
&lt;h3 id=&quot;making-it-do-something&quot;&gt;Making it do something&lt;/h3&gt;
&lt;p&gt;Function identity only matters when something is watching it. In practice that is one of two things: a child component wrapped in &lt;code&gt;React.memo&lt;/code&gt;, or another hook’s dependency array.&lt;/p&gt;
&lt;p&gt;Here is the &lt;code&gt;React.memo&lt;/code&gt; case:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; memo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useCallback &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Child &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;memo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; onClick &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'rendering'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Parent&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;selectedName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setSelectedName&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleChildClick &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setSelectedName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Selected name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;selectedName&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Child&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Thor&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleChildClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Child&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Loki&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleChildClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Child&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Odin&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleChildClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;memo&lt;/code&gt; tells React to skip re-rendering &lt;code&gt;Child&lt;/code&gt; when its props are shallowly equal to last time. Click one of the buttons, &lt;code&gt;selectedName&lt;/code&gt; changes, &lt;code&gt;Parent&lt;/code&gt; re-renders. Because &lt;code&gt;handleChildClick&lt;/code&gt; came out of &lt;code&gt;useCallback&lt;/code&gt; with an empty dependency array, all three children get the same &lt;code&gt;onClick&lt;/code&gt; and the same &lt;code&gt;name&lt;/code&gt; they had before, so none of them re-render. The console stays quiet.&lt;/p&gt;
&lt;p&gt;Take &lt;code&gt;memo&lt;/code&gt; off &lt;code&gt;Child&lt;/code&gt; and all three log again on every click, &lt;code&gt;useCallback&lt;/code&gt; or not. Take &lt;code&gt;useCallback&lt;/code&gt; off and leave &lt;code&gt;memo&lt;/code&gt; on, and you get the same result: a fresh function each render means the props are never shallowly equal, so &lt;code&gt;memo&lt;/code&gt; never gets to skip anything. &lt;strong&gt;Both halves are required.&lt;/strong&gt; This is the single most common way the optimisation gets applied and quietly does nothing.&lt;/p&gt;
&lt;p&gt;The second case is a dependency array:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; load &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/api/articles?page=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;page&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the effect depends on &lt;code&gt;load&lt;/code&gt;. If &lt;code&gt;load&lt;/code&gt; were recreated on every render, the effect would run on every render, which is a fetch loop rather than an optimisation. &lt;code&gt;useCallback&lt;/code&gt; is what makes that dependency stable.&lt;/p&gt;
&lt;h3 id=&quot;the-dependency-array-is-the-interesting-bit&quot;&gt;The dependency array is the interesting bit&lt;/h3&gt;
&lt;p&gt;Look again at the first example. &lt;code&gt;[count]&lt;/code&gt; means a new function on every single increment, which is every render that matters. The memoisation is technically working and practically pointless.&lt;/p&gt;
&lt;p&gt;The functional update form fixes it:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleClick &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;current&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; current &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setCount&lt;/code&gt; from &lt;code&gt;useState&lt;/code&gt; is guaranteed stable by React, so it does not need to be in the array, and passing a function to it means we no longer need to read &lt;code&gt;count&lt;/code&gt; during render. Empty dependencies, one function for the lifetime of the component. Whenever a &lt;code&gt;useCallback&lt;/code&gt; has a dependency that changes as often as the component re-renders, this is the first thing to try.&lt;/p&gt;
&lt;p&gt;One more detail that surprises people: the inline arrow function is still created on every render. It has to be, because it is the argument you are passing to &lt;code&gt;useCallback&lt;/code&gt;. The hook then throws it away and returns the cached one. You are not saving the allocation, you are stabilising the identity. If someone tells you &lt;code&gt;useCallback&lt;/code&gt; avoids creating functions, they have it backwards.&lt;/p&gt;
&lt;h3 id=&quot;when-to-skip-it&quot;&gt;When to skip it&lt;/h3&gt;
&lt;p&gt;The honest default is to skip it and add it when you have a measured reason. More specifically, leave it out when:&lt;/p&gt;
&lt;h4 id=&quot;nothing-is-observing-the-identity&quot;&gt;Nothing is observing the identity&lt;/h4&gt;
&lt;p&gt;If the function is only used inside the component, or handed to a plain DOM element, or passed to a child that is not memoised, &lt;code&gt;useCallback&lt;/code&gt; changes nothing about how often anything renders:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Button&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleClick&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Button clicked!'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Click me&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine as it is. Wrapping &lt;code&gt;handleClick&lt;/code&gt; would add a hook call, a closure held in memory for the life of the component, and no behaviour change.&lt;/p&gt;
&lt;h4 id=&quot;the-child-is-cheap-to-render&quot;&gt;The child is cheap to render&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;memo&lt;/code&gt; is not free. It runs a shallow comparison of every prop on every render of the parent. For a component that renders one button and some text, the comparison can cost more than the render it is avoiding. Reach for &lt;code&gt;memo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; around genuinely expensive subtrees: long lists, charts, anything doing real work in render.&lt;/p&gt;
&lt;h4 id=&quot;you-would-have-to-memoise-half-the-tree-to-make-it-work&quot;&gt;You would have to memoise half the tree to make it work&lt;/h4&gt;
&lt;p&gt;Shallow equality means an inline object or array prop breaks &lt;code&gt;memo&lt;/code&gt; just as effectively as an unstable function does:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Child&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleChildClick&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;style&lt;/code&gt; object is new every render, so &lt;code&gt;memo&lt;/code&gt; never skips, and the &lt;code&gt;useCallback&lt;/code&gt; next to it is wasted. If making one child memoisable requires wrapping four other values in &lt;code&gt;useMemo&lt;/code&gt;, the cure is worse than the disease. That is usually a signal to restructure, moving state closer to where it is used, so fewer components re-render in the first place.&lt;/p&gt;
&lt;h3 id=&quot;what-i-actually-do&quot;&gt;What I actually do&lt;/h3&gt;
&lt;p&gt;I write components without any of this, and I add &lt;code&gt;useCallback&lt;/code&gt; in two situations: when a function is a dependency of &lt;code&gt;useEffect&lt;/code&gt; or &lt;code&gt;useMemo&lt;/code&gt;, or when the React DevTools profiler shows me a subtree re-rendering that has no business re-rendering. The first is a correctness concern and the second is a measured one. Everything in between is guesswork that makes the code harder to read.&lt;/p&gt;
&lt;p&gt;Which is my one real irritation with this hook. It is not that it is slow, it is that it looks like diligence. A file full of &lt;code&gt;useCallback&lt;/code&gt; reads as careful, performance-minded code, and most of the time it is decoration with a dependency array attached.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Idiomatic Programming]]></title><description><![CDATA[Idiomatic TypeScript in practice - interfaces, inference, promises, readonly and generics, plus why I reach for union types instead of enums most days.]]></description><link>https://angelo.dini.dev/blog/idiomatic-javascript/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/idiomatic-javascript/</guid><pubDate>Fri, 31 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;code-that-looks-like-the-language-it-is-written-in&quot;&gt;Code that looks like the language it is written in&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Idiomatic code is code that follows the conventions of the language it is written in, rather than the conventions of the language you learned first. It is the difference between TypeScript that reads like TypeScript and TypeScript that reads like Java with the type annotations moved around.&lt;/p&gt;&lt;p&gt;Nobody enforces this. The compiler does not care, the tests do not care, and the code ships either way. What you get from it is that the next person, quite possibly you in eight months, can read the file without stopping to work out what the author was doing. Conventions are worth knowing even when they are not enforced, which is roughly the point I was making about &lt;a href=&quot;/blog/red-or-blue/&quot;&gt;class name ordering&lt;/a&gt; a few months back.&lt;/p&gt;
&lt;p&gt;Below are the patterns I lean on most in TypeScript. Most of them have equivalents in other languages, so the habit transfers even if the syntax does not.&lt;/p&gt;
&lt;h3 id=&quot;describe-your-shapes-with-interfaces&quot;&gt;Describe your shapes with interfaces&lt;/h3&gt;
&lt;p&gt;An interface gives a name to a shape, and that name then shows up in every error message and every autocomplete popup instead of a wall of inline braces:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sendEmail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; User&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// do stuff&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; user&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; User &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  email&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'john@wick.com'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;sendEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Hello!'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note where the &lt;code&gt;void&lt;/code&gt; goes. The return type belongs to the function, not to the constant holding it. Writing &lt;code&gt;const sendEmail: void = ...&lt;/code&gt; is a type error, and it is one I see a lot in code that was ported over from somewhere else.&lt;/p&gt;
&lt;h3 id=&quot;let-the-compiler-do-the-inferring&quot;&gt;Let the compiler do the inferring&lt;/h3&gt;
&lt;p&gt;TypeScript is good at working out types on its own, and annotating things it already knows is noise:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// inferred as string&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// inferred as (a: number, b: number) =&gt; number&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Annotate the boundaries, infer the middle. Parameters need annotations because there is nothing to infer from. Return types usually do not, unless the function is part of a public API and you want the compiler to shout at you when the shape drifts.&lt;/p&gt;
&lt;h3 id=&quot;promises-and-the-catch-that-quietly-eats-your-errors&quot;&gt;Promises, and the catch that quietly eats your errors&lt;/h3&gt;
&lt;p&gt;Async work is everywhere, and there is a specific mistake worth calling out:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// don't do this&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fetchData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/data'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;.catch&lt;/code&gt; swallows the rejection. The promise it returns resolves with &lt;code&gt;undefined&lt;/code&gt;, so any &lt;code&gt;.catch&lt;/code&gt; the caller attaches never fires and the caller happily carries on with nothing. If you are going to handle an error, handle it. If you are not, let it propagate:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fetchData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Article&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/data'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Request failed with &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two other things happen here. &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; flattens the chain, and the return type is &lt;code&gt;Article[]&lt;/code&gt; rather than &lt;code&gt;any&lt;/code&gt;. &lt;code&gt;response.json()&lt;/code&gt; returns &lt;code&gt;Promise&amp;lt;any&amp;gt;&lt;/code&gt;, which means every value that flows out of it is unchecked from that point on. Give it a type, or use &lt;code&gt;unknown&lt;/code&gt; and validate.&lt;/p&gt;
&lt;h3 id=&quot;readonly-where-it-earns-its-place&quot;&gt;readonly, where it earns its place&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;readonly&lt;/code&gt; modifier stops a property from being reassigned after construction:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;readonly&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; person&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Person &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
person&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Bob'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ERROR: Cannot assign to 'name' because it is a read-only property&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a compile-time check only. Nothing stops the value being mutated at runtime, and nothing stops a plain &lt;code&gt;Person&lt;/code&gt; object being passed somewhere that expects a mutable one. It is a hint to readers backed by the compiler, which is still worth having. &lt;code&gt;ReadonlyArray&amp;lt;T&amp;gt;&lt;/code&gt; (or &lt;code&gt;readonly T[]&lt;/code&gt;) does the same job for arrays and is the more useful of the two in my experience, because accidental &lt;code&gt;push&lt;/code&gt; into a prop is a real bug and accidental reassignment of a field usually is not.&lt;/p&gt;
&lt;h3 id=&quot;enums-and-why-i-mostly-avoid-them&quot;&gt;Enums, and why I mostly avoid them&lt;/h3&gt;
&lt;p&gt;The usual advice is to use an enum for a fixed set of named constants:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; Color &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;RED&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'red'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;GREEN&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'green'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token constant&quot;&gt;BLUE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'blue'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;setBackgroundColor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Color&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;backgroundColor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; color&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;setBackgroundColor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BLUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, and if your codebase already uses enums everywhere then consistency beats my preference. But enums are the one part of TypeScript that emits actual JavaScript. Everything else erases to nothing; an enum leaves an object behind in your bundle. And &lt;code&gt;const enum&lt;/code&gt;, which is the version that does not, is unsupported under &lt;code&gt;isolatedModules&lt;/code&gt;, so it breaks the moment you build with esbuild, Babel or anything built on them.&lt;/p&gt;
&lt;p&gt;A union of string literals gives you the same exhaustiveness checking with no runtime cost:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Color&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'red'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'green'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'blue'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;setBackgroundColor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Color&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;backgroundColor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; color&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;setBackgroundColor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'blue'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You lose the &lt;code&gt;Color.BLUE&lt;/code&gt; namespacing. If you want it back, an object with &lt;code&gt;as const&lt;/code&gt; and a derived type gets you most of the way there. You might not agree, and I have worked in codebases where the enum version reads better. This is a preference, not a rule.&lt;/p&gt;
&lt;h3 id=&quot;generics-for-the-things-that-genuinely-vary&quot;&gt;Generics for the things that genuinely vary&lt;/h3&gt;
&lt;p&gt;A generic lets one function serve many types without collapsing into &lt;code&gt;any&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;array&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; array&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; numbers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firstNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;numbers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// number | undefined&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; strings &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'hello'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'world'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firstString &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;strings&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// string | undefined&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;| undefined&lt;/code&gt; in the return type is the interesting part. Indexing an array in TypeScript is not checked by default, so &lt;code&gt;array[0]&lt;/code&gt; on an empty array is typed &lt;code&gt;T&lt;/code&gt; while actually being &lt;code&gt;undefined&lt;/code&gt;. Writing it out is honest. Turning on &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; makes the compiler do it for you everywhere, which is the sort of setting that produces a hundred errors on day one and saves you a production incident later.&lt;/p&gt;
&lt;p&gt;Generics are also where idiomatic code goes wrong most often. Three type parameters with conditional types and a mapped type on top will typecheck beautifully and be unreadable. If you cannot explain the signature out loud, it is too clever.&lt;/p&gt;
&lt;h3 id=&quot;a-few-more-worth-knowing&quot;&gt;A few more worth knowing&lt;/h3&gt;
&lt;p&gt;I will not list every pattern in the language, but these come up constantly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Type guards and narrowing.&lt;/strong&gt; A function returning &lt;code&gt;value is Article&lt;/code&gt; teaches the compiler something it cannot work out alone. This is how you get from &lt;code&gt;unknown&lt;/code&gt; to a real type safely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;unknown&lt;/code&gt; instead of &lt;code&gt;any&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;any&lt;/code&gt; switches type checking off; &lt;code&gt;unknown&lt;/code&gt; forces you to narrow first. Anything crossing the boundary into your app, JSON, form data, a query string, should arrive as &lt;code&gt;unknown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nullish coalescing and optional chaining.&lt;/strong&gt; &lt;code&gt;a?.b ?? fallback&lt;/code&gt; handles &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; without also swallowing &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;&amp;#39;&amp;#39;&lt;/code&gt; the way &lt;code&gt;||&lt;/code&gt; does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Destructuring.&lt;/strong&gt; Cheap readability, especially for props and function options objects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;never&lt;/code&gt; for exhaustiveness.&lt;/strong&gt; Assign to a &lt;code&gt;never&lt;/code&gt; in the default branch of a switch and the compiler fails the build when someone adds a new case to the union. Pairs perfectly with string literal unions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tuples, decorators, namespaces and currying all have their moments, but reaching for them without a reason is how you end up with the clever generic signature nobody can read.&lt;/p&gt;
&lt;h3 id=&quot;where-to-actually-start&quot;&gt;Where to actually start&lt;/h3&gt;
&lt;p&gt;Do not try to adopt all of this at once. Pick the two settings that pay off immediately, &lt;code&gt;strict&lt;/code&gt; and &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt;, and let the compiler tell you where the codebase disagrees with itself. Fix those, and most of the patterns above turn up on their own because they are the shortest path to making the errors go away.&lt;/p&gt;
&lt;p&gt;The rest is reading. Whatever language you are in, the fastest way to learn its idioms is to read its standard library and a couple of well-regarded projects, and notice what they do that you would not have.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Understanding React Suspense]]></title><description><![CDATA[How React 18 Suspense shows a fallback while part of your tree loads, why it needs a Suspense-enabled source, and a worked example built on React.lazy.]]></description><link>https://angelo.dini.dev/blog/react-suspense/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-suspense/</guid><pubDate>Thu, 23 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;it-all-comes-down-to-fallbacks&quot;&gt;It all comes down to fallbacks&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Every app I have worked on ends up with the same scruffy corner: a pile of &lt;code&gt;isLoading&lt;/code&gt; booleans, each one owned by a different component, each one rendering a slightly different spinner. Suspense is React's attempt to make &quot;this part isn't ready yet&quot; a thing the framework knows about rather than something you hand-roll.&lt;/p&gt;&lt;p&gt;I already touched on it when I wrote about &lt;a href=&quot;/blog/react-18/&quot;&gt;React 18&lt;/a&gt;, but only in passing. The concurrent renderer is what makes it interesting, so it deserves its own article.&lt;/p&gt;
&lt;h3 id=&quot;what-it-actually-does&quot;&gt;What it actually does&lt;/h3&gt;
&lt;p&gt;You wrap a part of your tree in &lt;code&gt;Suspense&lt;/code&gt; and give it a fallback:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;LoadingArticles&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Articles&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React renders &lt;code&gt;&amp;lt;LoadingArticles /&amp;gt;&lt;/code&gt; until &lt;code&gt;&amp;lt;Articles /&amp;gt;&lt;/code&gt;, or anything below it, is ready. When the tree is ready, React swaps the fallback out for the real thing in a single commit. No intermediate state where half the list has appeared and half hasn’t.&lt;/p&gt;
&lt;p&gt;The important bit is who decides “ready”. Suspense is not polling your component or inspecting your state. A child signals that it is not ready by throwing a promise during render, and Suspense catches it. You will almost never write that by hand. Something else does it for you.&lt;/p&gt;
&lt;h3 id=&quot;not-every-loading-state-counts&quot;&gt;Not every loading state counts&lt;/h3&gt;
&lt;p&gt;This is the part that trips people up. Suspense only reacts to a &lt;strong&gt;Suspense-enabled source&lt;/strong&gt;. As of React 18 that means two things in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data fetching through a framework that has integrated with it, such as &lt;a href=&quot;https://relay.dev/docs/guided-tour/rendering/loading-states/&quot;&gt;Relay&lt;/a&gt; or &lt;a href=&quot;https://nextjs.org/docs/advanced-features/react-18&quot;&gt;Next.js&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Lazy-loading component code with &lt;a href=&quot;https://beta.reactjs.org/reference/react/lazy&quot;&gt;lazy&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A plain &lt;code&gt;fetch&lt;/code&gt; inside a &lt;code&gt;useEffect&lt;/code&gt; is not one of them. If you write this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/articles'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;setArticles&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…then no Suspense boundary anywhere above you will ever show its fallback. The effect runs &lt;em&gt;after&lt;/em&gt; the component has already rendered, so as far as React is concerned that render succeeded. You are back to your own &lt;code&gt;isLoading&lt;/code&gt; flag.&lt;/p&gt;
&lt;p&gt;I find this genuinely annoying, and it is the single thing that keeps Suspense from being useful in the average non-framework app. So for the example below I’ll use &lt;code&gt;lazy&lt;/code&gt;, which works everywhere and shows the same mechanics.&lt;/p&gt;
&lt;h3 id=&quot;a-worked-example-with-lazy&quot;&gt;A worked example with lazy&lt;/h3&gt;
&lt;p&gt;A well-built React app splits its code into several smaller bundles and ships only what the user needs for the page in front of them. You get that from a builder like &lt;a href=&quot;https://webpack.js.org/&quot;&gt;Webpack&lt;/a&gt; or Vite, and &lt;code&gt;lazy&lt;/code&gt; is how you tell React about the split point.&lt;/p&gt;
&lt;p&gt;Start with something to load. Nothing clever here on purpose:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Articles.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Articles&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; list &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; articles&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Articles&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the app that lazily imports it. I’ve added an artificial two-second delay so the fallback is actually visible on a fast connection, otherwise you blink and miss it:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// App.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lazy &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Articles &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'./Articles'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;DemoAppLoading&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;🌀 Loading&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;DemoApp&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Articles &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;lazy&lt;/code&gt; is called at module scope, not inside the component. Calling it during render creates a brand new lazy component on every pass, which means the chunk is treated as a fresh import every time and the fallback never goes away. It is an easy mistake to make and a miserable one to debug, because nothing errors. You just get a spinner forever.&lt;/p&gt;
&lt;p&gt;Finally, wire it up:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// index.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Suspense &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOM &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react-dom/client&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DemoApp&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; DemoAppLoading &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./App&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ReactDOM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; HTMLElement
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;StrictMode&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Suspense fallback&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;DemoAppLoading &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;DemoApp &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Suspense&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;StrictMode&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DemoAppLoading&lt;/code&gt; stays on screen until the &lt;code&gt;./Articles&lt;/code&gt; chunk has been fetched and evaluated, plus the two seconds I added. &lt;code&gt;App.tsx&lt;/code&gt; itself is a normal static import, so it is already in the main bundle and never suspends. Only the lazy child does.&lt;/p&gt;
&lt;h3 id=&quot;where-you-put-the-boundary-matters&quot;&gt;Where you put the boundary matters&lt;/h3&gt;
&lt;p&gt;The fallback replaces the entire subtree, so a boundary near the root means the whole page disappears while one small chunk downloads. That is rarely what you want.&lt;/p&gt;
&lt;p&gt;Boundaries nest, and each one reveals independently:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;PageSkeleton&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Header&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SidebarSkeleton&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Sidebar&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Articles&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the sidebar can still be loading while the header and the articles are on screen. The nearest boundary above a suspending component wins.&lt;/p&gt;
&lt;p&gt;Worth pairing them with an error boundary as well. If the chunk fails to download, because you deployed while someone had the tab open and the old hashed file is gone, &lt;code&gt;lazy&lt;/code&gt; throws and the nearest error boundary catches it. Without one you get a blank page.&lt;/p&gt;
&lt;h3 id=&quot;keeping-content-on-screen&quot;&gt;Keeping content on screen&lt;/h3&gt;
&lt;p&gt;There is a second behaviour that took me a while to notice. If content is already visible and an update would cause it to suspend again, React will hide it and show the fallback, which reads as a flicker back to the spinner.&lt;/p&gt;
&lt;p&gt;Marking that update as a transition avoids it:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;isPending&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startTransition&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTab&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'articles'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React keeps the previous content on screen while the new tab loads, and gives you &lt;code&gt;isPending&lt;/code&gt; so you can grey it out or show a small indicator instead of blowing the whole section away.&lt;/p&gt;
&lt;h3 id=&quot;what-id-still-like&quot;&gt;What I’d still like&lt;/h3&gt;
&lt;p&gt;Suspense on the server is the part I am most pleased with in React 18. Combined with streaming SSR, which I covered in the &lt;a href=&quot;/blog/react-hydration/&quot;&gt;hydration article&lt;/a&gt;, the server can send HTML for the fast parts of the page and stream the slow parts in as they resolve.&lt;/p&gt;
&lt;p&gt;On the client it is still half a feature unless you are on a framework that has done the integration for you. The &lt;a href=&quot;https://beta.reactjs.org/reference/react/Suspense&quot;&gt;documentation&lt;/a&gt; is honest about this, which I appreciate, but it does mean “use Suspense for data fetching” is advice you cannot act on in most codebases yet. I’ll keep using it for code splitting and wait.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Looking back at 2022]]></title><description><![CDATA[A short flashback to 2022 - routing work on the Control Panel, a break from formal study in favour of online courses, and a hard year that turned into travel.]]></description><link>https://angelo.dini.dev/blog/looking-back-at-2022/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/looking-back-at-2022/</guid><pubDate>Tue, 31 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;not-the-year-i-ordered&quot;&gt;Not the year I ordered&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;The past year hasn't been kind, and I'm looking forward to a more enjoyable one. I'll stick to the positive here so I can keep moving steadily onward. Thanks to everyone who has been there for me 🤗.&lt;/p&gt;&lt;p&gt;I kept journaling work, school and personal life, the same as I did for &lt;a href=&quot;/blog/looking-back-at-2021/&quot;&gt;2021&lt;/a&gt;. For my sanity and everyone else’s, here is the short version.&lt;/p&gt;
&lt;h3 id=&quot;work&quot;&gt;Work&lt;/h3&gt;
&lt;p&gt;We steadily continued to improve our product. The most noteworthy piece was the overhaul of the entire routing for the Control Panel, which made it exceptionally fast. The addons and boilerplates sections, the project overview, mirrors and metrics were all reworked along the way. We also added dark mode 🤣.&lt;/p&gt;
&lt;p&gt;Beyond that: a long list of smaller improvements across the board, a lot of resolved issues, and a handful of internal tools that make our own lives easier.&lt;/p&gt;
&lt;h3 id=&quot;study&quot;&gt;Study&lt;/h3&gt;
&lt;p&gt;2021 was a straining and busy year of studying. In 2022 I took a break, mostly because of everything else that was going on. I also expected more from my school, and corona did nothing to improve the experience.&lt;/p&gt;
&lt;p&gt;That did not stop the learning. I worked through several classes on Udemy, MIT’s OpenCourseWare and Harvard’s CS50. I’m more convinced than ever that this is the future of learning rather than the classical route 🤷‍♂️&lt;/p&gt;
&lt;h3 id=&quot;personal&quot;&gt;Personal&lt;/h3&gt;
&lt;p&gt;Privately, 2022 was horrendous. I had to deal with an immense loss and with existential doubts about my studies. What came out of it was travel, and a lot of it: Spain, Hungary, Germany, France, Sweden, Denmark, Portugal, Cuba and Egypt. Plenty of concerts and festivals with friends, too.&lt;/p&gt;
&lt;h3 id=&quot;looking-forward&quot;&gt;Looking forward&lt;/h3&gt;
&lt;p&gt;Yeah. Looking forward to 2023. That’s about the gist of it &lt;code&gt;(╯°□°)╯︵ ┻━┻&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Self-Hosted Jitsi Server]]></title><description><![CDATA[How I set up a self-hosted Jitsi Meet server on Ubuntu, including dynamic DNS with dnsimple, firewall and port forwarding, and Let's Encrypt certificates.]]></description><link>https://angelo.dini.dev/blog/jitsi-server/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/jitsi-server/</guid><pubDate>Sun, 18 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-personal-jitsi-server-on-ubuntu&quot;&gt;A personal Jitsi Server on Ubuntu&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Are you tired of Google Meets, Skype or Zoom? Want an open-source solution that doesn't spy on you in the background? Say welcome to Jitsi Meet.&lt;/p&gt;&lt;p&gt;I’m not a privacy advocate, but I wanted to play around with &lt;a href=&quot;https://jitsi.org/jitsi-meet/&quot;&gt;Jitsi Meet&lt;/a&gt; for a while now that SWITCH’s meet.switch.ch has closed its doors. This is a great excuse to see if I can host Jitsi on my own and allow access from the outside.&lt;/p&gt;
&lt;p&gt;Setting up Jitsi Meet isn’t that difficult. The team provides a great &lt;a href=&quot;https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-quickstart/&quot;&gt;quickstart guide&lt;/a&gt;. That doesn’t mean it is without its challenges, especially when you are not following the installation instructions 🫣. You can run into encryption issues when the domain isn’t set up correctly, or connection troubles when a package is missing.&lt;/p&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting started&lt;/h3&gt;
&lt;p&gt;I will start from a standard &lt;strong&gt;Ubuntu server&lt;/strong&gt; installation on a minted VM. Always ensure Ubuntu is up to date, SSH into the VM and run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; upgrade
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; full-upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we must figure out how to reach the server before installing Jitsi Meet. My scenario revolves around &lt;a href=&quot;https://meet.dini.dev/&quot;&gt;https://meet.dini.dev/&lt;/a&gt;. The service should be reachable through that domain from the outside, encrypted. For that to happen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I need to configure my DNS provider to point to my WAN IP and refresh it, as it is not a static IP.&lt;/li&gt;
&lt;li&gt;I must ensure my firewall accepts and forwards incoming connections to the Ubuntu server.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;configuring-the-dns&quot;&gt;Configuring the DNS&lt;/h3&gt;
&lt;p&gt;I use &lt;a href=&quot;https://dnsimple.com/&quot;&gt;dnsimple&lt;/a&gt; to manage my domains. The first step is to go to the configuration settings and set up a new subdomain for &lt;code&gt;https://meet.dini.dev/&lt;/code&gt; that points to my current WAN IP address. I can get that quickly by running &lt;code&gt;curl -s https://icanhazip.com&lt;/code&gt; in a terminal window, as &lt;code&gt;ip a&lt;/code&gt; would only show me the local addresses.&lt;/p&gt;
&lt;p&gt;The problem is that my WAN IP is dynamic and changes over time. To solve this, I could either get a static IP or let dnsimple know about any changes. dnsimple &lt;a href=&quot;https://developer.dnsimple.com/tools/&quot;&gt;provides some tools&lt;/a&gt; to help us with this. Their &lt;code&gt;ddns.sh&lt;/code&gt; script is what I’m looking for. SSH into the Ubuntu instance, run &lt;code&gt;vim ~/ddns.sh&lt;/code&gt; and add the following content:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;TOKEN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{% token from user settings &gt; user access tokens %}&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{% account id shown under account &gt; automation %}&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;ZONE_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{% your primary domain name %}&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;RECORD_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{% the record id from one of the DNS records %}&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;IP&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--ipv4&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; http://icanhazip.com/&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Authorization: Bearer &lt;span class=&quot;token variable&quot;&gt;$TOKEN&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
     &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
     &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Accept: application/json&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
     &lt;span class=&quot;token parameter variable&quot;&gt;-X&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;PATCH&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
     &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://api.dnsimple.com/v2/&lt;span class=&quot;token variable&quot;&gt;$ACCOUNT_ID&lt;/span&gt;/zones/&lt;span class=&quot;token variable&quot;&gt;$ZONE_ID&lt;/span&gt;/records/&lt;span class=&quot;token variable&quot;&gt;$RECORD_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
     &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;content&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;:&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$IP&lt;/span&gt;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;}&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;TOKEN&lt;/code&gt;, &lt;code&gt;ACCOUNT_ID&lt;/code&gt;, &lt;code&gt;ZONE_ID&lt;/code&gt; and &lt;code&gt;RECORD_ID&lt;/code&gt; need to be replaced accordingly. I added hints in the script on where to get it from dnsimple.&lt;/p&gt;
&lt;p&gt;Next, run &lt;code&gt;sudo chmod +x ./ddns.sh&lt;/code&gt; to allow the script to be executed through crontab. A crontab can easily be set up by running &lt;code&gt;crontab -e&lt;/code&gt;. I decided to set up a crontab that runs the script every 5 minutes, so add the following at the end of the file:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;*/5 * * * * /path/to/ddns.sh &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will update dnsimple’s records every 5 minutes with my current WAN IP address 🥳. A simple way of creating a dynamic DNS. Test if the crontab is working through &lt;code&gt;sudo service cron start&lt;/code&gt; and check the logs by running &lt;code&gt;systemctl status cron&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;configuring-the-firewall&quot;&gt;Configuring the Firewall&lt;/h3&gt;
&lt;p&gt;I’m running Pi-hole as my primary local DNS Server. As such, I can trick my local network into forwarding an incoming request from &lt;code&gt;https://meet.dini.dev/&lt;/code&gt; to redirect to my Jitsi server.&lt;/p&gt;
&lt;p&gt;Log in to Pi-hole and select “DNS records” from the menu. Choose to add a “new domain/IP combination” and add the hostname (meet.dini.dev) and IP address of the Ubuntu server accordingly.&lt;/p&gt;
&lt;p&gt;Next, we need to ensure traffic through the ports is routed correctly. Go to your router’s firewall settings and make sure the following ports are forwarded from your WAN IP to the Jitsi server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP: 80,443,5349&lt;/li&gt;
&lt;li&gt;UDP: 3478,10000&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To enable additional security, enable &lt;code&gt;ufw&lt;/code&gt; by running &lt;code&gt;sudo ufw enable&lt;/code&gt; on the Jitsi server followed by:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;/tcp
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;443&lt;/span&gt;/tcp
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;3478&lt;/span&gt;/udp
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;5349&lt;/span&gt;/tcp
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt;/udp&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the status using &lt;code&gt;sudo ufw status&lt;/code&gt;. Next, we install Jitsi Meet!&lt;/p&gt;
&lt;h3 id=&quot;installing-jitsi-meet&quot;&gt;Installing Jitsi Meet&lt;/h3&gt;
&lt;p&gt;Before starting the installation, ensure the steps above are working correctly. I made a mistake installing Jitsi first, which required setting up the encryption manually afterwards.&lt;/p&gt;
&lt;p&gt;To get started, run the following commands in sequence:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# preparing the server&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; apt-transport-https gnupg2 nginx-full socat
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; apt-add-repository universe
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update

&lt;span class=&quot;token comment&quot;&gt;# install dependencies&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; deb http://packages.prosody.im/debian &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;lsb_release &lt;span class=&quot;token parameter variable&quot;&gt;-sc&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; main &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; /etc/apt/sources.list
&lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; https://prosody.im/files/prosody-debian-packages.key -O- &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; apt-key &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; -
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; lua5.2

&lt;span class=&quot;token comment&quot;&gt;# setup the repository&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; https://download.jitsi.org/jitsi-key.gpg.key &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'gpg --dearmor &gt; /usr/share/keyrings/jitsi-keyring.gpg'&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'deb [signed-by=/usr/share/keyrings/jitsi-keyring.gpg] https://download.jitsi.org stable/'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/jitsi-stable.list &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; /dev/null
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update

&lt;span class=&quot;token comment&quot;&gt;# start installation&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; jitsi-meet&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will be prompted to configure your hostname and SSL/TLS certificate. For the hostname, enter the domain you decided to go with (for me, meet.dini.dev). For the certificate, choose “Let’s Encrypt” if you do not want to upload your certificate.&lt;/p&gt;
&lt;p&gt;The verification process will only be configured appropriately when the domain is set up correctly. However, you can create the certificates manually afterwards by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; certbot
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; /usr/share/jitsi-meet/scripts/install-letsencrypt-cert.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this stage, the Jitsi Server is running and can be reached through the local IP address of the Ubuntu server or the hostname you have set up. That’s all there is to it 🫣.&lt;/p&gt;
&lt;h3 id=&quot;yatta&quot;&gt;Yatta&lt;/h3&gt;
&lt;p&gt;I now have my own personal Jitsi Meet server running on &lt;a href=&quot;https://meet.dini.dev/&quot;&gt;https://meet.dini.dev/&lt;/a&gt; 🎉.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Red pill or blue pill?]]></title><description><![CDATA[A question I ask my CAS Frontend Engineering students: does the order of class names in an HTML attribute change which CSS rule wins? Short answer, it does not.]]></description><link>https://angelo.dini.dev/blog/red-or-blue/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/red-or-blue/</guid><pubDate>Wed, 30 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;does-class-name-ordering-matter&quot;&gt;Does class name ordering matter?&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Adding class names to an HTML document is as natural as almost any other HTML attribute. However, I've often encountered that the ordering of the class names needs to be understood.&lt;/p&gt;&lt;p&gt;One of the questions I ask my students during the &lt;strong&gt;CAS Frontend Engineering&lt;/strong&gt; class revolves around the following example:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.red&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; red&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.blue&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; blue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;red blue&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;First example&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;blue red&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Second example&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Have a closer look at the code above. Which colour is applied?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The first example is &lt;strong&gt;red&lt;/strong&gt;, and the second &lt;strong&gt;blue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The first example is &lt;strong&gt;blue&lt;/strong&gt;, and the second &lt;strong&gt;red&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The first and second examples are both &lt;strong&gt;blue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The first and second examples are both &lt;strong&gt;red&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The answer follows below.&lt;/p&gt;
&lt;h3 id=&quot;understanding-the-order-of-css-classes&quot;&gt;Understanding the order of CSS classes&lt;/h3&gt;
&lt;p&gt;The order of CSS classes in HTML does not matter. What matters is the order in which styles are defined in the stylesheet. When two rules set the same property and carry the same specificity, the one declared last in the stylesheet always wins.&lt;/p&gt;
&lt;p&gt;This concept can be tricky when dealing with multiple files and cause issues where developers misuse &lt;code&gt;!important&lt;/code&gt; from time to time. That is why naming conventions, such as &lt;a href=&quot;https://getbem.com/&quot;&gt;BEM&lt;/a&gt;, are essential. Additional tools and frameworks make encapsulation easier these days, &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind&lt;/a&gt; being a good example.&lt;/p&gt;
&lt;p&gt;As such, the answer to the example above is the first and second examples are both &lt;em&gt;blue&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is a concise post with an important concept to understand, as more and more developers use CSS-in-JS solutions and seem to get this little fact wrong. 🫣&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Screenshot Testing with React]]></title><description><![CDATA[A walkthrough of screenshot testing a React app with Jest, Puppeteer and Storybook, then running it all in Docker so results stay consistent across machines.]]></description><link>https://angelo.dini.dev/blog/screenshot-testing/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/screenshot-testing/</guid><pubDate>Sat, 15 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;using-jest-puppeteer-storybook-and-docker&quot;&gt;Using Jest, Puppeteer, Storybook and Docker&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Testing your React app using Puppeteer, Jest, and Storybook enhances existing unit tests and saves you headaches in the future, while Docker makes everything consistent.&lt;/p&gt;&lt;p&gt;In an ideal world, we write code without any issues at all. Sadly reality is often disappointing, and we need to write tests. In this article, I’ll focus on screenshot testing. The tutorial was tested using Node 16 and 18.&lt;/p&gt;
&lt;h3 id=&quot;the-basics&quot;&gt;The basics&lt;/h3&gt;
&lt;p&gt;The goal is to take snapshots and compare them to a reference screenshot stored alongside the test or in a centralised folder. The test will fail if the two screenshots do not match. Either the change is unexpected, or the reference screenshot needs to be updated to the new version.&lt;/p&gt;
&lt;p&gt;We will start with a generic React boilerplate through &lt;strong&gt;create-react-app&lt;/strong&gt; using TypeScript. Open a terminal, navigate to your projects folder and run the following command to get started:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-react-app react-screenshot &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;--template&lt;/span&gt; typescript &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; react-screenshot&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command will set up our React application together with some sample files. The following sections will guide us to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create a simple screenshot-capturing test using &lt;strong&gt;jest-image-snapshot&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;integrate &lt;strong&gt;Storybook&lt;/strong&gt; to isolate testing from a development server&lt;/li&gt;
&lt;li&gt;run everything through &lt;strong&gt;Docker&lt;/strong&gt; to get a consistent result&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;screenshots-using-jest-and-puppeteer&quot;&gt;Screenshots using Jest and Puppeteer&lt;/h3&gt;
&lt;p&gt;Let’s install some dependencies into our project before we get started. Inside the terminal, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; jest-image-snapshot &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  puppeteer &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  puppeteer-core &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  @types/jest-image-snapshot &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  @types/puppeteer &lt;span class=&quot;token parameter variable&quot;&gt;--save&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check out the documentation for &lt;a href=&quot;https://github.com/americanexpress/jest-image-snapshot&quot;&gt;jest-image-snapshot&lt;/a&gt; and &lt;a href=&quot;https://pptr.dev/&quot;&gt;puppeteer&lt;/a&gt; to learn more about their configuration options. I will keep this tutorial as simple as possible, with only minor tweaks.&lt;/p&gt;
&lt;p&gt;We’re ready to create our first screenshot test with our dependencies installed. Open &lt;code&gt;src/App.test.tsx&lt;/code&gt; and replace the content of the file with:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; puppeteer &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'puppeteer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; render&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; screen &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@testing-library/react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toMatchImageSnapshot &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'jest-image-snapshot'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; App &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

expect&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;extend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; toMatchImageSnapshot &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'React App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Browser&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Page&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;beforeAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    browser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;launch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    page &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;afterAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'renders learn react link'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;App&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; linkElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; screen&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;learn react&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;linkElement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeInTheDocument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'renders correctly'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; image &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;screenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMatchImageSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the test, you will have to open two terminal sessions:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# the first one will run the React development server&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run start

&lt;span class=&quot;token comment&quot;&gt;# and the second one our screenshot test&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# which will run against the React development server&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run &lt;span class=&quot;token builtin class-name&quot;&gt;test&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jest image snapshot takes screenshots against our running development server at &lt;strong&gt;&lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt;&lt;/strong&gt;. The snapshots themselves are stored within &lt;code&gt;src/__image_snapshots__/&lt;/code&gt;. An additional folder is shown when the test fails. That folder will contain a visual representation of the diff between the reference and the new screenshot.&lt;/p&gt;
&lt;p&gt;The reference screenshots can be regenerated by removing the image snapshots folder. However, as this tests against our development server, you can encounter issues while capturing the reference screenshots due to timeouts or incorrect rendering. So the whole process is somewhat flaky. We will solve this later on.&lt;/p&gt;
&lt;p&gt;You can also emulate other devices by extending the page. Underneath a short example emulating an iPhone:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'renders correctly on iPhone 13'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;emulate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;devices&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'iPhone 13'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; image &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;screenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMatchImageSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A full list of supported devices can be found in puppeteer’s &lt;a href=&quot;https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.knowndevices.md&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;handling-animations&quot;&gt;Handling animations&lt;/h3&gt;
&lt;p&gt;You might have noticed that the React welcome screen has a rotating logo. This animation can be an issue when taking screenshots, as a couple of milliseconds of delays can move the logo by a pixel or two, creating inconsistent results. To avoid this, we’ll use a little trick. Add the following code snippet at the end of the &lt;code&gt;src/App.css&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.no-animations *&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* disable all animations */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;animation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* hide the cursor caret when in inputs */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;caret-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; transparent &lt;span class=&quot;token important&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, edit &lt;code&gt;src/App.test.tsx&lt;/code&gt; and add the class right after navigating to the page:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'renders correctly'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;goto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'http://localhost:3000'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// make sure to disable animations&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;classList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'no-animations'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; image &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;screenshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMatchImageSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This change will add the &lt;strong&gt;no-animations&lt;/strong&gt; class to the document’s body, which prevents all animations and hides any cursor that might be placed within an input element. It has to happen after &lt;code&gt;page.goto()&lt;/code&gt;, as the navigation would otherwise throw the class away again.&lt;/p&gt;
&lt;p&gt;However, a development server is not ideal for taking screenshots due to moving parts. On top of that, it is harder to achieve certain states before taking screenshots. For that reason, let’s introduce Storybook!&lt;/p&gt;
&lt;h3 id=&quot;screenshots-using-storybook&quot;&gt;Screenshots using Storybook&lt;/h3&gt;
&lt;p&gt;Storybook is a fantastic tool for documenting and testing our components and views. To install it, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;npx storybook init
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run storybook&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The latter will open up Storybook with some story examples. To add our React view, create &lt;code&gt;src/App.stories.tsx&lt;/code&gt; and place the following code inside:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; App &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./index.css'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Views/App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; App&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Overview &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;App&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Storybook will pick it up automatically and display the story under the “Views” category. You can find additional stories from Storybook inside the &lt;code&gt;src/stories/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;Next, we want to take snapshots from all of the components and views inside Storybook. A couple of preparations are required to achieve this. At this point, we do not need the Storybook development server. Shut it down and run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# remove puppeteer due to peer conflicts&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; remove puppeteer puppeteer-core &lt;span class=&quot;token parameter variable&quot;&gt;--save&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# install the storybook addon&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @storybook/addon-storyshots-puppeteer &lt;span class=&quot;token parameter variable&quot;&gt;--save&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# clear our testing cache&lt;/span&gt;
npx jest &lt;span class=&quot;token parameter variable&quot;&gt;--clearCache&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/storybookjs/storybook/tree/v6.5.16/addons/storyshots/storyshots-puppeteer&quot;&gt;addon-storyshots-puppeteer&lt;/a&gt; package will take care of the correct puppeteer version. Otherwise, we get peer dependency conflicts.&lt;/p&gt;
&lt;p&gt;We have learned that testing against a development server can raise issues. Luckily, Storybook offers a static build of itself without running a server at all. To get this working, we run &lt;code&gt;npm run build-storybook&lt;/code&gt; before executing our tests and then chain it with &lt;code&gt;npm run test&lt;/code&gt;. Our testing setup reads the static build straight off the local file system through &lt;code&gt;file://&lt;/code&gt;, which doesn’t require any server to run.&lt;/p&gt;
&lt;p&gt;To get started, replace the content of our &lt;code&gt;src/App.test.tsx&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'path'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; initStoryshots &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@storybook/addon-storyshots'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; imageSnapshot &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@storybook/addon-storyshots-puppeteer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;initStoryshots&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'storyshots'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;imageSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;storybookUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;file://&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__dirname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../storybook-static'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You could also create a separate file like &lt;code&gt;src/screenshots.test.tsx&lt;/code&gt; for this process. You may want to delete &lt;code&gt;src/__image_snapshots__/&lt;/code&gt; before moving onward.&lt;/p&gt;
&lt;p&gt;To execute the tests, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run build-storybook
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run &lt;span class=&quot;token builtin class-name&quot;&gt;test&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This procedure will add the generated screenshots again into &lt;code&gt;src/__image_snapshots__/&lt;/code&gt; with the same behaviour as before. Instead of just one screenshot, we end up with additional ones from all the components inside Storybook. It is beneficial when testing variants of the same view.&lt;/p&gt;
&lt;h3 id=&quot;glueing-it-together-with-docker&quot;&gt;Glueing it together with Docker&lt;/h3&gt;
&lt;p&gt;The above examples already work when you’re the only person contributing to the project and only run the tests on your system. This setup might break as soon as you need to run screenshot tests on different operating systems due to how fonts are rendered. One solution to this problem is to use Docker to run our tests so that the testing environment is always consistent regardless of where you run them, locally on your Mac, Linux, or in the cloud.&lt;/p&gt;
&lt;p&gt;To get started, create a &lt;code&gt;Dockerfile&lt;/code&gt; in the root of the project and populate it with following content:&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:16.17-alpine&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENV&lt;/span&gt; PATH /app/node_modules/.bin:&lt;span class=&quot;token variable&quot;&gt;$PATH&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package*.json ./&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm install --silent&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file in the root directory containing:&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'3.9'&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;screenshots&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npm run test
    &lt;span class=&quot;token key atrule&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; chrome
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'.:/app:rw'&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'storybook:/app/storybook-static'&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;chrome&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'browserless/chrome:1.53-chrome-stable'&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;CONNECTION_TIMEOUT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3600000&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'3000'&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'.:/app:rw'&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'storybook:/app/storybook-static'&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  storybook&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker will create two containers, a &lt;strong&gt;chrome&lt;/strong&gt; container our tests can connect to and a &lt;strong&gt;screenshots&lt;/strong&gt; container based on our Dockerfile that runs the tests encapsulated from our system.&lt;/p&gt;
&lt;p&gt;Next, open up your test file and replace it with the following code:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'path'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; puppeteer &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'puppeteer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; initStoryshots &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@storybook/addon-storyshots'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; imageSnapshot &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'@storybook/addon-storyshots-puppeteer'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Browser&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getCustomBrowser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Promise&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;puppeteer.Browser&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  browser &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;browserURL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'http://chrome:3000/'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;beforeScreenshot&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token literal-property property&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; puppeteer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;classList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'no-animations'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;initStoryshots&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'storyshots'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;imageSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    getCustomBrowser&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    beforeScreenshot&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;storybookUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;file://&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__dirname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'../storybook-static'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;afterAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; browser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To finally have consistent and reliable tests, delete the &lt;code&gt;src/__image_snapshots__/&lt;/code&gt; folder and run the following sequence:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# we still need to build storybook first&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run build-storybook
&lt;span class=&quot;token comment&quot;&gt;# build the containers&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker-compose&lt;/span&gt; build
&lt;span class=&quot;token comment&quot;&gt;# and then run the tests inside docker&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker-compose&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; screenshots&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another great advantage is that such a setup can easily be integrated into a CI such as &lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub Actions&lt;/a&gt; or &lt;a href=&quot;https://docs.gitlab.com/ee/ci/pipelines/&quot;&gt;Gitlab Pipelines&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Unit testing is a very efficient and relatively cheap process to perform. Integration tests, such as automated screenshot testing, can be expensive and time-consuming.&lt;/p&gt;
&lt;p&gt;At Divio, we have hundreds of screenshot tests with various components and views in both light and dark modes. This tutorial is just a mere start to screenshot testing, and the hole goes deep. Have fun exploring 😊.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Working with CSS Custom Properties]]></title><description><![CDATA[How CSS custom properties work: where to declare them, how they cascade, fallback values, reading and writing them in JavaScript, and combining them with Sass.]]></description><link>https://angelo.dini.dev/blog/css-custom-properties/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/css-custom-properties/</guid><pubDate>Tue, 27 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;say-hi-to-css-variables&quot;&gt;Say hi to CSS variables&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;CSS custom properties, sometimes called CSS variables, are properties you can define in your CSS to reuse variables across your stylesheets.&lt;/p&gt;&lt;p&gt;Initially introduced in 2012, it took roughly five years to reach wide browser adoption, around late 2017 once Edge had caught up. What Less and Sass have offered since the very beginning is finally supported in native CSS too.&lt;/p&gt;
&lt;h3 id=&quot;how-to-use-it&quot;&gt;How to use it&lt;/h3&gt;
&lt;p&gt;A custom CSS property always needs to be defined &lt;strong&gt;within a selector&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.box&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--color&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin-bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--spacing&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* not allowed */&lt;/span&gt;
&lt;span class=&quot;token property&quot;&gt;--font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Helvetica Now&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Arial&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the example above, we define the properties in the box selector. Properties cascade (including pseudo-elements), so if you have an element inside the box, they will inherit the variables from their parent. Let’s have a look at this:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.box&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.box__inner&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--spacing&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As long as &lt;code&gt;box__inner&lt;/code&gt; is within the &lt;code&gt;box&lt;/code&gt; in the HTML document, &lt;code&gt;box__inner&lt;/code&gt; will have access to &lt;code&gt;--spacing&lt;/code&gt;. What if that is not the case? Well, we can make use of default values if a variable is not defined:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.box&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.box__inner&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--color&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* or use another var as backup */&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--custom-spacing&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--spacing&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can use this to create global styles. What would be the uppermost element in a document? Right, it is &lt;strong&gt;html&lt;/strong&gt; or the &lt;strong&gt;:root&lt;/strong&gt; pseudo-element:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Helvetica Now&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Arial&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can further override variables down the cascading tree. We can use this mechanic to, for example, introduce a dark mode:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #fff&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.dark-mode&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #fff&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* or change it with media queries */&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; screen &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;prefers-color-scheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dark&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;--color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #fff&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;--background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #000&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attach the &lt;code&gt;dark-mode&lt;/code&gt; class to the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, and our global values are overridden from there down, so every element inside inherits the dark set instead. Alternatively, we can let the media query do it for us, neat, right?&lt;/p&gt;
&lt;p&gt;All other operations, such as &lt;strong&gt;calc&lt;/strong&gt;, multiplication or addition, are also supported. Custom properties behave in that regard similarly to normal CSS property definitions.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 20px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing-sm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--spacing&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; / 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--spacing-xl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--spacing&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; * 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;using-javascript&quot;&gt;Using JavaScript&lt;/h3&gt;
&lt;p&gt;There is another advantage compared to Less or Sass. You can read and change variables from within JavaScript natively, at runtime.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// get a reference to the element&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;:root&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// set a variable&lt;/span&gt;
root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setProperty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--primary&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#f00&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// read it back, but only if it was set inline&lt;/span&gt;
root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPropertyValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--primary&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// read the value the browser actually resolved,&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// including the ones declared in a stylesheet&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;getComputedStyle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPropertyValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--primary&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That distinction bites people. &lt;code&gt;element.style.getPropertyValue()&lt;/code&gt; only ever sees inline declarations, so a variable you defined in your stylesheet comes back as an empty string. Use &lt;code&gt;getComputedStyle()&lt;/code&gt; when you want the value that is actually in effect.&lt;/p&gt;
&lt;p&gt;With that knowledge, we can do some interesting manipulations:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--x&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; green&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mousemove&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setProperty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'--x'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clientX &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setProperty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'--y'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clientY &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;px&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Courtesy of &lt;em&gt;CSS Tricks&lt;/em&gt;, have a look at the &lt;a href=&quot;https://codepen.io/chriscoyier/pen/yxVQJG&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;sass-and-css-variables&quot;&gt;Sass and CSS variables&lt;/h3&gt;
&lt;p&gt;You can also combine CSS custom properties with Sass, but be aware of the syntax:&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$link-color&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #333&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;:root &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--link-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;#{$link-color}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--link-color-hover&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; #&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;darken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$link-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 25%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.link &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--link-color&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token parent important&quot;&gt;&amp;amp;&lt;/span&gt;:hover &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--link-color-hover&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is good to combine different technologies to get the best out of them. We just recently updated the &lt;a href=&quot;https://control.divio.com/&quot;&gt;Control Panel&lt;/a&gt; to use CSS custom properties as our go-to for variable declaration, which made the dark mode integration a lot less painful 🥳&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[React 18]]></title><description><![CDATA[What React 18 changes for existing apps: the createRoot upgrade, automatic batching, transitions, Suspense on the server, and the new hooks like useId.]]></description><link>https://angelo.dini.dev/blog/react-18/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-18/</guid><pubDate>Thu, 11 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;welcome-to-react-18&quot;&gt;Welcome to React 18&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;React 18 introduces features powered by their new concurrent renderer. Version 18 provides gradual adoption strategies for existing applications with a simple upgrade path.&lt;/p&gt;&lt;p&gt;Their new &lt;a href=&quot;https://github.com/reactwg/react-18/discussions/46&quot;&gt;concurrency system&lt;/a&gt; is a &lt;a href=&quot;https://reactjs.org/blog/2022/03/29/react-v18.html#what-is-concurrent-react&quot;&gt;new behind-the-scenes mechanism&lt;/a&gt; that enables React to prepare multiple versions of the UI simultaneously, adding more speed and efficiency. Naturally, we want to profit from that and upgrade our existing applications to React 18.&lt;/p&gt;
&lt;h3 id=&quot;how-to-upgrade-to-react-18&quot;&gt;How to Upgrade to React 18&lt;/h3&gt;
&lt;p&gt;The first warning you will see after upgrading to React 18 is that &lt;code&gt;ReactDOM.render&lt;/code&gt; is no longer supported. You have to upgrade to &lt;code&gt;ReactDOM.createRoot&lt;/code&gt;;&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before React 18&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOM &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; App &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'root'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ReactDOM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;App&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; root&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After React 18&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOM &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; App &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./App'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ReactDOM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'root'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;App&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same applies to &lt;code&gt;ReactDOM.hydrate&lt;/code&gt;, which we covered in the &lt;a href=&quot;/blog/react-hydration/&quot;&gt;previous article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are some smaller changes to &lt;code&gt;unmountComponentAtNode&lt;/code&gt; as well:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before React 18&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;unmountComponentAtNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;container&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After React 18&lt;/span&gt;
root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unmount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can find more information in the &lt;a href=&quot;https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html&quot;&gt;official blog post&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;automatic-batching&quot;&gt;Automatic Batching&lt;/h3&gt;
&lt;p&gt;In React, we are used to updating states as follows:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;setLoading&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;setData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React is smart and tries to batch those state changes automatically together to avoid unnecessary re-renders. However, the challenge is &lt;strong&gt;when&lt;/strong&gt; React should do this. Before 18, batching only happened inside React event handlers, so &lt;strong&gt;not&lt;/strong&gt; in promises, setTimeouts or native event callbacks.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'/api/'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setLoader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// re-render&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// re-render&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 18 introduces &lt;a href=&quot;https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching&quot;&gt;automatic batching&lt;/a&gt; for promises, setTimeouts and event callbacks. You can opt out by using &lt;code&gt;flushSync&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;flushSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setLoader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;transitions&quot;&gt;Transitions&lt;/h3&gt;
&lt;p&gt;Sometimes you want certain updates to be treated more urgent than others. For example, clicking or pressing are immediate responses that require immediate feedback. You can still load the content after a click, but the UI should indicate this immediately. So at best, we trigger an urgent update and &lt;a href=&quot;https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-transitions&quot;&gt;transition&lt;/a&gt; to a non-urgent one:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; startTransition &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// show the input change immediately&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;setInputValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// fetch the data in a non-urgent manner&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;startTransition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setSearchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Additionally, &lt;code&gt;useTransition&lt;/code&gt; is available as a hook to track the pending state.&lt;/p&gt;
&lt;h3 id=&quot;suspense&quot;&gt;Suspense&lt;/h3&gt;
&lt;p&gt;Wouldn’t it be nice to make the “UI loading state” a first-class declarative concept in the React programming model? This issue is what the &lt;code&gt;&amp;lt;Suspense /&amp;gt;&lt;/code&gt; component solves in React 18. It shows the respective loading component if the corresponding component tree is not yet ready to be displayed:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;AvatarLoading&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Avatar&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Earlier versions of React had a limited understanding of Suspense. The only supported use case was code splitting using &lt;code&gt;React.lazy&lt;/code&gt;, which wasn’t supported when rendering on the server.&lt;/p&gt;
&lt;p&gt;React 18 added support for Suspense on the server while expanding its capabilities using concurrent rendering features.&lt;/p&gt;
&lt;h3 id=&quot;new-client-and-server-rendering-apis&quot;&gt;New Client and Server Rendering APIs&lt;/h3&gt;
&lt;p&gt;Most of the features have been mentioned in the &lt;a href=&quot;/blog/react-hydration/&quot;&gt;previous article&lt;/a&gt;. They include changes to React Hydration and how to stream data efficiently between SSR and CSR.&lt;/p&gt;
&lt;p&gt;Have a closer look at &lt;a href=&quot;https://reactjs.org/blog/2022/03/29/react-v18.html#new-client-and-server-rendering-apis&quot;&gt;React 18’s documentation&lt;/a&gt; if you want to dive further into the details and examples.&lt;/p&gt;
&lt;h3 id=&quot;new-strict-mode-behaviors&quot;&gt;New Strict Mode Behaviors&lt;/h3&gt;
&lt;p&gt;In the future, React will add a feature that allows adding and removing sections in the UI while preserving the state. For example, when a user tabs away from a screen and back, future React will be able to show the previous screen immediately.&lt;/p&gt;
&lt;p&gt;This approach requires React to be resilient to effects being mounted and destroyed multiple times. To help surface these issues, React 18 introduced a new development-only check to Strict Mode. This new check will automatically unmount and remount every component whenever a component mounts for the first time, restoring the previous state on the second mount and looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;* React mounts the component.
  * Layout effects are created.
  * Effects are created.
* React simulates unmounting the component.
  * Layout effects are destroyed.
  * Effects are destroyed.
* React simulates mounting the component with the previous state.
  * Layout effects are created.
  * Effects are created.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See further information about &lt;a href=&quot;https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state&quot;&gt;ensuring a reusable state&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;new-hooks&quot;&gt;New Hooks&lt;/h3&gt;
&lt;p&gt;Besides &lt;code&gt;useTransition&lt;/code&gt; there have been new hooks introduced to make life easier:&lt;/p&gt;
&lt;h4 id=&quot;useid&quot;&gt;useId&lt;/h4&gt;
&lt;p&gt;It generates a unique ID that is stable across the server and the client, which makes it handy for accessibility attributes. It is not intended for generating keys in a list. Keys need to be generated from your data.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;CustomForm&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;htmlFor&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Accept&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;checkbox&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;react&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;usedeferredvalue&quot;&gt;useDeferredValue&lt;/h4&gt;
&lt;p&gt;It accepts a value and returns a new copy of the value that will defer to more urgent updates.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setQuery&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; deferredQuery &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useDeferredValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// we wait for the deferredQuery to be ready&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// to avoid unnecessary re-renders&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; suggestions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useMemo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SearchSuggestions&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;deferredQuery&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;deferredQuery&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// but we still want the query input to be immediate&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SearchInput&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;setQuery&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Loading results...&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;suggestions&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;usesyncexternalstore&quot;&gt;useSyncExternalStore&lt;/h4&gt;
&lt;p&gt;It is a new hook that allows external stores to support concurrent reads by forcing updates to the store to be synchronous. It removes the need for &lt;code&gt;useEffect&lt;/code&gt; when implementing subscriptions to external data sources and is recommended for any library that integrates with state external to React.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// general use case&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; state &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useSyncExternalStore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;subscribe&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getSnapshot
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// or subscribe to a specific field&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; selectedField &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useSyncExternalStore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;subscribe&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; store&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;selectedField&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;useinsertioneffect&quot;&gt;useInsertionEffect&lt;/h4&gt;
&lt;p&gt;This new hook allows CSS-in-JS libraries to address performance issues of injecting styles in the render. You probably won’t use this unless you are building a CSS-in-JS library yourself, so I refer to the &lt;a href=&quot;https://reactjs.org/docs/hooks-reference.html#useinsertioneffect&quot;&gt;documentation instead&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;React 18 introduced a lot of small changes that set the foundation for future releases. At the same time, it keeps a straightforward upgrading path for us developers. Check out &lt;a href=&quot;https://www.youtube.com/watch?v=FZ0cG47msEk&amp;amp;list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&quot;&gt;their keynote&lt;/a&gt; to learn more about React 18 and its future.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[React Hydration]]></title><description><![CDATA[How React hydration pairs server-rendered markup with client-side interactivity, why tree mismatches happen, and which hydration strategies exist to avoid them.]]></description><link>https://angelo.dini.dev/blog/react-hydration/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/react-hydration/</guid><pubDate>Fri, 29 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;understanding-react-hydration&quot;&gt;Understanding React Hydration&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Let's initially load a scaffolding HTML representing our React application, followed by &lt;strong&gt;hydrating&lt;/strong&gt; it with functionality to improve user experience and SEO!&lt;/p&gt;&lt;p&gt;That is the gist behind React Hydration. First, instead of showing an empty page on load, we use &lt;a href=&quot;https://reactjs.org/docs/react-dom-server.html&quot;&gt;ReactDOMServer&lt;/a&gt; to render our components into the DOM during server-side rendering (&lt;strong&gt;SSR&lt;/strong&gt;). Then, during client-side rendering (&lt;strong&gt;CSR&lt;/strong&gt;), React Hydration kicks in and gradually adds interactivity to our components.&lt;/p&gt;
&lt;p&gt;That premise seems simple, but the approach can cause performance concerns and even serious issues. This article will deal with them and provides additional references to strategies.&lt;/p&gt;
&lt;h3 id=&quot;how-does-it-work&quot;&gt;How does it work?&lt;/h3&gt;
&lt;p&gt;Let’s have a look at a basic React app and how it is created:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- index.html --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// DemoApp.js (component)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DemoApp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Hello &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; DemoApp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// index.js (CSR)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOM &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; DemoApp &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./DemoApp'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ReactDOM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'root'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;React.StrictMode&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DemoApp&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;React Hydration&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;React.StrictMode&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this scenario, the website will display an empty page and fill it with our demo application’s “Hello React Hydration” text. This process can be a problem, as the content “flashes” into existence, and crawlers don’t necessarily know about your content.&lt;/p&gt;
&lt;p&gt;To remedy this, we can use SSR to serve our components to the browser without any interactivity:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// server.js (SSR)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOMServer &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react-dom/server&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; DemoApp &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./DemoApp'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'*'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; response&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'./index.html'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'utf-8'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ReactDOMServer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;renderToString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DemoApp&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;React Hydration&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React Hydration will then gradually add that interactivity by using &lt;a href=&quot;https://reactjs.org/docs/react-dom-client.html#hydrateroot&quot;&gt;hydrateRoot&lt;/a&gt; instead of &lt;a href=&quot;https://reactjs.org/docs/react-dom-client.html#createroot&quot;&gt;createRoot&lt;/a&gt; during CSR. In React &amp;lt; 18, it would be &lt;code&gt;render&lt;/code&gt; and &lt;code&gt;hydrate&lt;/code&gt; instead of &lt;code&gt;createRoot&lt;/code&gt; and &lt;code&gt;hydrateRoot&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Watch out for one detail: &lt;code&gt;hydrateRoot&lt;/code&gt; takes the container &lt;strong&gt;and&lt;/strong&gt; the element in one go, so there is no separate &lt;code&gt;render&lt;/code&gt; call afterwards.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// index.js (CSR)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOM &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react-dom/client'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; DemoApp &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'./DemoApp'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

ReactDOM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;hydrateRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'root'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;React.StrictMode&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;DemoApp&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;React Hydration&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;React.StrictMode&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This technique is how React Hydration is applied to an application. Render a pre-build version of your application on the server, and then add interactivity through the hydration process.&lt;/p&gt;
&lt;p&gt;This approach is also used by many static site generators such as &lt;a href=&quot;https://www.gatsbyjs.com/docs/conceptual/react-hydration/&quot;&gt;Gatsby&lt;/a&gt; or &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt;. The sites got pre-generated by SSR and served via their &lt;strong&gt;public/&lt;/strong&gt; folders. The browser can “pick up” where the server left off and add app-like features accordingly.&lt;/p&gt;
&lt;p&gt;However, this approach requires the React tree to be in-sync with the DOM. Otherwise, we end up with issues.&lt;/p&gt;
&lt;h3 id=&quot;issues-with-server-side-rendering-and-hydration&quot;&gt;Issues with Server-Side Rendering and Hydration&lt;/h3&gt;
&lt;p&gt;React Hydration requires our DOM to be in sync with the React tree. Through SSR we pre-fill the DOM with the markup of our application, and React Hydration picks it up from there. What happens if our SSR DemoApp serves different content depending on SSR/CSR generation? Let’s have a look at a typical example:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// DemoApp.js (component)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DemoApp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; greeting &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'undefined'&lt;/span&gt;
   &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Hello '&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;greeting&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; DemoApp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, if the window is defined, our app returns “Hello React Hydration”, and if not, just “React Hydration”. Now we end up with a mismatch, and the following error is thrown:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While rendering your application, there was a difference between the React tree that was pre-rendered (SSR/CSR) and the React tree that was rendered during the first render in the browser.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This issue is widespread and must be dealt with when working on SSR. Not all features are available on a server compared to a browser. For Hydration to work, the tree needs to be identical.&lt;/p&gt;
&lt;p&gt;The above example could be avoided using &lt;code&gt;useEffect&lt;/code&gt;, as the method is only called during the hydration phase and not while pre-rendering the content. For example:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// DemoApp.js (component)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'react'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DemoApp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// be empty during pre-generation&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;greeting&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setGreeting&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// add &quot;Hello&quot; after hydration takes place&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setGreeting&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Hello '&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;greeting&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; DemoApp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This change causes the content to have the “flashing” effect but can be softened through loaders or skeletons.&lt;/p&gt;
&lt;p&gt;There are other issues to consider, for example, when pre-fetching APIs during the SSR process. They need to be appropriately cached or passed onward to avoid mismatches. It is a common source of trouble when working with Gatsby or Next.js.&lt;/p&gt;
&lt;h3 id=&quot;hydration-strategies&quot;&gt;Hydration strategies&lt;/h3&gt;
&lt;p&gt;The idea behind different strategies is to decide which components need to be hydrated. Your website becomes unresponsive during the hydration process, which can block your users from interacting with your product. It might not be a big concern for smaller websites, but when you load huge bundles to add interactivity, it can get frustrating to wait to click on a menu to move onwards.&lt;/p&gt;
&lt;p&gt;Different approaches surfaced in the past years to solve these problems. The one shown in this article is the plainest: render the whole tree with &lt;code&gt;ReactDOMServer.renderToString&lt;/code&gt; and hydrate all of it in one go. &lt;strong&gt;Streaming Server-Side Rendering&lt;/strong&gt; sends the markup down in chunks instead and has been available since React 16 through &lt;code&gt;ReactDOMServer.renderToNodeStream&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Another is &lt;strong&gt;Progressive Hydration&lt;/strong&gt; or &lt;a href=&quot;https://github.com/valcol/react-hydration-on-demand&quot;&gt;Hydration on demand&lt;/a&gt; where you attach specific criteria to your components. These can be on visibility, scrolling, idling, or with a simple delay.&lt;/p&gt;
&lt;p&gt;And the last I want to cover is &lt;strong&gt;Selective Hydration&lt;/strong&gt;, new in React 18, which uses &lt;code&gt;renderToPipeableStream&lt;/code&gt; instead of &lt;code&gt;renderToString&lt;/code&gt;. This method makes it possible to start streaming HTML without waiting for the heavier components to be ready. Meaning that you can lazy-load components when using SSR.&lt;/p&gt;
&lt;p&gt;There is also a great library collection from &lt;a href=&quot;https://github.com/GoogleChromeLabs/progressive-rendering-frameworks-samples&quot;&gt;Google Chrome Labs&lt;/a&gt; on the different approaches if you are interested in code examples.&lt;/p&gt;
&lt;h3 id=&quot;lets-hydrate-some-more&quot;&gt;Let’s hydrate some more 💧&lt;/h3&gt;
&lt;p&gt;React 18 fixes several issues people often encounter when using SSR with React and Hydration. We’ll look closely at React 18’s changes in the following article.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Handling backups]]></title><description><![CDATA[Notes on what actually counts as a backup, why a RAID is not one, how often to test a restore, and why an offline copy beats trusting an online service.]]></description><link>https://angelo.dini.dev/blog/handling-backups/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/handling-backups/</guid><pubDate>Thu, 23 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;restoring-your-private-backups&quot;&gt;Restoring your private backups&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;We've all been told to create regular backups of our data. Be it through your friendly neighbourhood IT guy or ads on services like iCloud or Google Cloud.&lt;/p&gt;&lt;h3 id=&quot;what-is-a-backup&quot;&gt;What is a backup&lt;/h3&gt;
&lt;p&gt;Yeah, I’m starting with the basics here, as I’ve seen things 😱.&lt;/p&gt;
&lt;p&gt;First, what is &lt;strong&gt;not&lt;/strong&gt; a backup; A NAS with a RAID configuration that survives one, two or more disk failures is not a backup. For sure, it is nice to know that your data is not lost once a disk failure occurs, but losing a second disk during the rebuild is common enough, and then all your information is gone anyway.&lt;/p&gt;
&lt;p&gt;Also, a service that promises to keep your data safe may not be the right choice, as the company behind it can go bankrupt, be bought or resells your information…&lt;/p&gt;
&lt;h3 id=&quot;well-what-is-a-backup-then&quot;&gt;Well, what is a backup then?!?&lt;/h3&gt;
&lt;p&gt;So what do I consider a legitimate backup that can save your data and sometimes your neck? For me, the following three principles need to apply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One backup is none; two backups are one.&lt;/li&gt;
&lt;li&gt;A backup doesn’t work if you can’t restore it.&lt;/li&gt;
&lt;li&gt;Offline is better than online.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;one-backup-is-none-two-backups-are-one&quot;&gt;One backup is none; two backups are one&lt;/h3&gt;
&lt;p&gt;I learned early on that having a backup doesn’t guarantee you won’t lose your data. A backup drive wears down as you regularly keep updates and may fail you in an emergency case.&lt;/p&gt;
&lt;p&gt;For this reason, computer scientists speak of “one backup is none, two backups are one”, and so forth. You should ideally back up your regular data on one drive (as you probably already do) and then either back up the backup drive or store a copy on an online service to end up with &lt;strong&gt;one&lt;/strong&gt; valid backup.&lt;/p&gt;
&lt;p&gt;You do not need to create a backup of a backup drive as often as your computer or mobile devices. Once a month or so is a good start, depending on how important the data is to you. You can also easily automate that process 🥇.&lt;/p&gt;
&lt;h3 id=&quot;a-backup-doesnt-work-if-you-cant-restore-it&quot;&gt;A backup doesn’t work if you can’t restore it&lt;/h3&gt;
&lt;p&gt;Creating one or two backups is excellent, but have you ever tried to restore one in a non-emergency case? It doesn’t help if you have a superb backup strategy or disaster recovery plan if you’ve never run a test run.&lt;/p&gt;
&lt;p&gt;Make sure to test that you can restore your backups regularly. I usually do this every half a year or yearly, depending on the backup type.&lt;/p&gt;
&lt;p&gt;For example, I back up my Plex VM about every month or so to a physical drive outside the RAID. The backup itself is not super important to me, as I can manually restore playlists, custom covers and settings, but it’s still a hassle.&lt;/p&gt;
&lt;p&gt;So I do yearly tests where I deactivate the &lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Plex VM&lt;/a&gt; and spin it up again through a backup. After a while, I use the backup as my new replacement and delete the old one to ensure everything works as expected. If not, I will fix it to make sure it won’t happen again in the future.&lt;/p&gt;
&lt;h3 id=&quot;offline-is-better-than-online&quot;&gt;Offline is better than online&lt;/h3&gt;
&lt;p&gt;You may have heard about the many security breaches in the past with various services and backup solutions. So be careful with online backup solutions.&lt;/p&gt;
&lt;p&gt;Apple or Google provide simple solutions for everyone to restore their devices or photos, but you don’t necessarily know what they do with your data. Primarily, Google is known for analysing your images and using the data to improve its services. But we do not see what happens in the background ¯_(ツ)_/¯.&lt;/p&gt;
&lt;p&gt;As such, always ensure your backups are encrypted and enable all the available protection. That also applies to offline backups such as your physical external hard drive. Ensure someone can’t easily steal it and then access your data.&lt;/p&gt;
&lt;p&gt;It is also a good idea to keep backups in different locations. That guarantees a backup is still available even after a physical incident.&lt;/p&gt;
&lt;h3 id=&quot;that-is-it&quot;&gt;That is it&lt;/h3&gt;
&lt;p&gt;Backups are great, keep your data secure and safe, and you’ll spare yourself a lot of hassle. No one wants to lose their precious memories or documents 🤣.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[All about the console]]></title><description><![CDATA[I use console.log more than I'd like to admit, so here is a tour of the other console methods: assert, count, dir, group, table, time, trace and more.]]></description><link>https://angelo.dini.dev/blog/all-the-consoles/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/all-the-consoles/</guid><pubDate>Fri, 13 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;discovering-the-console-methods&quot;&gt;Discovering the console methods&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm using &lt;strong&gt;console.log&lt;/strong&gt; more than I'd like to admit. Though, many more console methods are available to help in certain situations.&lt;/p&gt;&lt;h3 id=&quot;consoleassert&quot;&gt;console.assert&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/assert&quot;&gt;&lt;code&gt;console.assert()&lt;/code&gt;&lt;/a&gt;
method writes an error message to the console if the assertion is false. If the assertion is true, nothing happens.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'hello'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'world'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'hello is not the world'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Assertion failed: hello is not the world&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'hello'&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'string'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'hello is not a string'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// will return &quot;undefined&quot; as the assertion does not fail&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consoleclear&quot;&gt;console.clear&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/clear&quot;&gt;&lt;code&gt;console.clear()&lt;/code&gt;&lt;/a&gt; method clears the console if the environment allows it.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// clears the console entries&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consolecontext&quot;&gt;console.context&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;console.context()&lt;/code&gt; method lists all the available context methods.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// output&lt;/span&gt;
Context &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;dirXml&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;groupEnd&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; …&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;countReset&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;countReset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;dirXml&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;dirXml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;groupCollapsed&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;groupCollapsed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;groupEnd&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;groupEnd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;profileEnd&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;profileEnd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;timeEnd&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;timeEnd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;timeLog&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;timeLog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;timeStamp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;timeStamp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;trace&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;trace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ƒ &lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Prototype&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Object&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consolecount-countreset&quot;&gt;console.count &amp;amp; .countReset&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/count&quot;&gt;&lt;code&gt;console.count()&lt;/code&gt;&lt;/a&gt; method logs the number of times that this particular call to count() has been called.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 1&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 2&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will continue to count when running the loop again until &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/countreset&quot;&gt;&lt;code&gt;console.countReset()&lt;/code&gt;&lt;/a&gt; is called:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 1&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 2&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 3&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 4&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 5&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 6&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;countReset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'increment'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 1&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 2&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// increment: 3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consoledebug&quot;&gt;console.debug&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/debug&quot;&gt;&lt;code&gt;console.debug()&lt;/code&gt;&lt;/a&gt; method outputs a message to the web console at the “debug” log level. The message is only displayed to the user if the console is configured to display debug output. In most cases, the log level is configured within the console UI. This log level might correspond to the &lt;code&gt;Debug&lt;/code&gt; or &lt;code&gt;Verbose&lt;/code&gt; log level.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'world'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'debugging my object'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It behaves similarly to &lt;code&gt;console.log()&lt;/code&gt; and &lt;code&gt;console.info()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;consoledir-dirxml&quot;&gt;console.dir &amp;amp; .dirxml&lt;/h3&gt;
&lt;p&gt;The method &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/dir&quot;&gt;&lt;code&gt;console.dir()&lt;/code&gt;&lt;/a&gt; displays an interactive list of the properties of the specified JavaScript object. The output is presented as a hierarchical listing with disclosure triangles that let you see the contents of child objects.&lt;/p&gt;
&lt;p&gt;In other words, &lt;code&gt;console.dir()&lt;/code&gt; is the way to see all the properties of a specified JavaScript object in console by which the developer can easily get the properties of the object.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;console&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// displays all the methods similar to console.context()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/dirxml&quot;&gt;&lt;code&gt;console.dirxml()&lt;/code&gt;&lt;/a&gt; method displays an interactive tree of the descendant elements of the specified XML/HTML element. If it is impossible to display as an element, the JavaScript Object view is shown instead. The output is presented as a hierarchical listing of expandable nodes that let you see the contents of child nodes.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirxml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// use dirxml to inspect DOM nodes&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consoleerror&quot;&gt;console.error&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/error&quot;&gt;&lt;code&gt;console.error()&lt;/code&gt;&lt;/a&gt; method outputs an error message to the Web console.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is a custom error message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// displays the message in red to the console&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consolegroup-groupcollapsed-groupend&quot;&gt;console.group, .groupCollapsed &amp;amp; .groupEnd&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/group&quot;&gt;&lt;code&gt;console.group()&lt;/code&gt;&lt;/a&gt; method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console.groupEnd() is called.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'A normal log message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// lets group the next logs in an accordion style&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is now one level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is now one level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// even further nesting is possible&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'And another level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/groupend&quot;&gt;&lt;code&gt;console.groupEnd()&lt;/code&gt;&lt;/a&gt; method exits the current inline group in the Web console. See Using groups in the console in the console documentation for details and examples.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'A normal log message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// lets group the next logs in an accordion style&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is now one level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is now one level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// clear the group&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;groupEnd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'A normal log message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/groupcollapsed&quot;&gt;&lt;code&gt;console.groupCollapsed()&lt;/code&gt;&lt;/a&gt; method creates a new inline group in the Web Console. Unlike console.group(), however, the new group is created and collapsed. The user will need to use the disclosure button to expand it, revealing the entries created in the group.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'A normal log message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// creates the group but collapses it&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;groupCollapsed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is now one level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is now one level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// you can still combine it with a regular group call&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// though the parent is still collapsed&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'And another level deeper'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consoleinfo&quot;&gt;console.info&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/info&quot;&gt;&lt;code&gt;console.info()&lt;/code&gt;&lt;/a&gt; method outputs an informational message to the Web console. In Firefox, a small “i” icon is displayed next to these items in the Web console’s log.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is an info message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// well, displays the text :)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consolelog&quot;&gt;console.log&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/log&quot;&gt;&lt;code&gt;console.log()&lt;/code&gt;&lt;/a&gt; method outputs a message to the web console. The message may be a single string (with optional substitution values), or it may be any one or more JavaScript objects.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Some'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'log'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'message'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// you know this one&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consolememory&quot;&gt;console.memory&lt;/h3&gt;
&lt;p&gt;Related to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory&quot;&gt;performance.memory&lt;/a&gt; that displays additional memory information. Be aware that it is not a method call:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;memory&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// displays additional memory information&lt;/span&gt;
MemoryInfo &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;totalJSHeapSize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;usedJSHeapSize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;jsHeapSizeLimit&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3760000000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;jsHeapSizeLimit&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3760000000&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;totalJSHeapSize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000000&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;usedJSHeapSize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000000&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Prototype&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MemoryInfo&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consoleprofile-consoleprofileend&quot;&gt;console.profile &amp;amp; console.profileEnd&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/profile&quot;&gt;&lt;code&gt;console.profile()&lt;/code&gt;&lt;/a&gt; starts recording a performance profile (for example, the Firefox performance tool).&lt;/p&gt;
&lt;p&gt;You can optionally supply an argument to name the profile, enabling you to stop only that profile if multiple profiles are recorded. See &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/profileend&quot;&gt;&lt;code&gt;console.profileEnd()&lt;/code&gt;&lt;/a&gt; to see how this argument is interpreted.&lt;/p&gt;
&lt;p&gt;Be aware that this feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behaviour may change in the future.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;profileTest&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;callback&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; counter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; interval &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;counter &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;interval&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Working...'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      counter&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'profileTest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;profileTest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;profileEnd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'profileTest'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To view the result in Chrome, select the three dots in the Developer Tools and choose &lt;strong&gt;More tools&lt;/strong&gt; &amp;gt; &lt;strong&gt;JavaScript Profiler&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;consoletable&quot;&gt;console.table&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/table&quot;&gt;&lt;code&gt;console.table()&lt;/code&gt;&lt;/a&gt; method displays tabular data as a table.&lt;/p&gt;
&lt;p&gt;This function takes one mandatory argument data, which must be an array or an object, and one additional optional parameter column.&lt;/p&gt;
&lt;p&gt;It logs data as a table. Each element in the array (or enumerable property if data is an object) will be a row in the table.&lt;/p&gt;
&lt;p&gt;The first column in the table will be labelled (index). If data is an array, then its values will be the array indices. If data is an object, then its values will be the property names. Note that (in Firefox) console.table is limited to displaying 1000 rows (first row is the labeled index).&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;firstName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; lastName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;firstName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; firstName&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lastName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; lastName&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Smith'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or with multiple entries using arrays:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; john &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Smith'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; jane &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Jane'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Doe'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; emily &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'Emily'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Jones'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;john&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; jane&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; emily&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consoletime-consoletimeend-consoletimelog-consoletimestamp&quot;&gt;console.time, console.timeEnd, console.timeLog &amp;amp; console.timeStamp&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/time&quot;&gt;&lt;code&gt;console.time()&lt;/code&gt;&lt;/a&gt; method starts a timer you can use to track how long an operation takes. You give each timer a unique name, and may have up to 10,000 timers running on a given page. When you call console.timeEnd() with the same name, the browser will output the time, in milliseconds, that elapsed since the timer was started.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// do something very long&lt;/span&gt;
Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;timeEnd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// wait&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// default: 4767.52490234375 ms&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/timelog&quot;&gt;&lt;code&gt;console.timeLog()&lt;/code&gt;&lt;/a&gt; method logs the current value of a timer previously started by calling console.time() to the console.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/timestamp&quot;&gt;&lt;code&gt;console.timeStamp()&lt;/code&gt;&lt;/a&gt; method adds a single marker to the browser’s Performance or Waterfall tool. This approach lets you correlate a point in your code with the other events recorded in the timeline, such as layout and paint events.&lt;/p&gt;
&lt;h3 id=&quot;consoletrace&quot;&gt;console.trace&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/trace&quot;&gt;&lt;code&gt;console.trace()&lt;/code&gt;&lt;/a&gt; method outputs a stack trace to the Web console.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// traces now by execution:&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// bar&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// foo&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &amp;lt;anonymous&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consolewarn&quot;&gt;console.warn&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/console/warn&quot;&gt;&lt;code&gt;console.warn()&lt;/code&gt;&lt;/a&gt; method outputs a warning message to the Web console.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'This is your last warning'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// shows an orange marked warning message&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;that-is-it&quot;&gt;That is it&lt;/h3&gt;
&lt;p&gt;There you have it, all the console methods in a short overview 😎.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Using Normalizr]]></title><description><![CDATA[Normalization is required when interacting with multiple API endpoints that reference each other. How normalizr stores the responses and links them back up.]]></description><link>https://angelo.dini.dev/blog/normalizr/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/normalizr/</guid><pubDate>Wed, 27 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;normalizing-api-responses&quot;&gt;Normalizing API responses&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Normalization is required when interacting with multiple API endpoints that have references.&lt;/p&gt;&lt;p&gt;We are using &lt;a href=&quot;https://github.com/paularmstrong/normalizr&quot;&gt;normalizr&lt;/a&gt; for that particular job at Divio. Tools such as &lt;a href=&quot;https://tanstack.com/query/latest&quot;&gt;React Query&lt;/a&gt; or &lt;a href=&quot;https://redux-toolkit.js.org/rtk-query/overview&quot;&gt;rtk-query&lt;/a&gt; are great alternatives. Today we would probably use React Query. Yet, the migration effort to switch outweighs the benefits, for now. &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;so-what-is-the-problem&quot;&gt;So, what is the problem?&lt;/h3&gt;
&lt;p&gt;Let’s have a look at the following two API responses:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Categories API response&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; categoriesApi &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'iu6h3wr74vc2bhugglgzmp3hhu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Movie'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'4g35twfl3vhu3njuxgbtardjrm'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'TV Show'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'oiz6zif455h3hctzzhobak5l2y'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Other'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Movies API response&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; moviesApi &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'3l5buhzt7rcbdmhntsrdlxd3h4'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Memento'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'iu6h3wr74vc2bhugglgzmp3hhu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zxevaw7jkfbhdiflgfp5lxwrku'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Pulp Fiction'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'iu6h3wr74vc2bhugglgzmp3hhu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'uexridtrxnb3tls2az7x3eemvu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Spirited Away'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'oiz6zif455h3hctzzhobak5l2y'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'x3eemv2az7x3etsrdlxd3h4dda'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Breaking Bad'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'4g35twfl3vhu3njuxgbtardjrm'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;categoriesApi&lt;/code&gt; provides a list of categories. It uses the &lt;strong&gt;uuid&lt;/strong&gt; to mark each entry with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Universally_unique_identifier&quot;&gt;universally unique identifier&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;moviesApi&lt;/code&gt; follows a similar pattern but holds a reference to the &lt;strong&gt;category uuid&lt;/strong&gt;. This approach allows us to separate the data and avoid repeating unnecessary information, making the API slim and fast.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This response is not what we desire when passing the data to a component. We would want to have the category inlined inside the movie like so:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;x3eemv2az7x3etsrdlxd3h4dda&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Breaking Bad&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;category&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4g35twfl3vhu3njuxgbtardjrm&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;TV Show&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This step is where normalizr comes into play. Normalizr takes the API response and stores them as &lt;strong&gt;entities&lt;/strong&gt; internally, and links them together through a &lt;strong&gt;schema&lt;/strong&gt;. Let’s have a look at how that would work.&lt;/p&gt;
&lt;h3 id=&quot;setting-everything-up&quot;&gt;Setting everything up&lt;/h3&gt;
&lt;p&gt;We will be using our two API responses &lt;code&gt;categoriesApi&lt;/code&gt; and &lt;code&gt;moviesApi&lt;/code&gt;, as references moving forward. Now, create a JavaScript project through &lt;code&gt;npm init&lt;/code&gt; and install normalizr through &lt;code&gt;npm install normalizr --save&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We are going to import &lt;strong&gt;normalize&lt;/strong&gt;, &lt;strong&gt;denormalize&lt;/strong&gt; and &lt;strong&gt;schema&lt;/strong&gt; from normalizr. Add the following line on the top:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; normalize&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; denormalize&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; schema &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'normalizr'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need to let normalizr know about our data structure. This is where our &lt;strong&gt;entities&lt;/strong&gt; will be stored and how normalizr knows what objects are linked. To define our entities, we call &lt;code&gt;schema.Entity&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; category &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;schema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// here, we define the name of the entity&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'categories'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// the idAttribute is used to identify references&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;idAttribute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'uuid'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; movie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;schema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'movies'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// here, we let normalizr know that movies&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// has a reference to another category&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; category&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;idAttribute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'uuid'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These definitions allow us to normalize the data from our API endpoints when calling &lt;code&gt;normalize()&lt;/code&gt;. Normalizr requires a schema that defines what the data should look like:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dataSchema &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;categories&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;category&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;movies&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;movie&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can normalize the data by using &lt;code&gt;normalize()&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; normalized &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;normalize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;categories&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; categoriesApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;movies&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; moviesApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  dataSchema
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output of &lt;code&gt;normalized&lt;/code&gt; should look something like:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;entities&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;categories&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;iu6h3wr74vc2bhugglgzmp3hhu&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;iu6h3wr74vc2bhugglgzmp3hhu&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Movie&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;movies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;x3eemv2az7x3etsrdlxd3h4dda&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;x3eemv2az7x3etsrdlxd3h4dda&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Breaking Bad&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;category&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4g35twfl3vhu3njuxgbtardjrm&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do not have the data in our desired format yet. To achieve this, we need to &lt;strong&gt;denormalize&lt;/strong&gt; the data. This step is where the entities will be linked together in a more usable format. Let’s &lt;code&gt;denormalize()&lt;/code&gt; our data:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; denormalized &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;denormalize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;categories&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; normalized&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;categories&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;movies&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; normalized&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;movies&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  dataSchema&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  normalized&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;entities
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Denormalizing is finally bringing the data together in a format where we can process the data:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;categories&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;iu6h3wr74vc2bhugglgzmp3hhu&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Movie&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;movies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3l5buhzt7rcbdmhntsrdlxd3h4&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Memento&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;category&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;uuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;iu6h3wr74vc2bhugglgzmp3hhu&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Movie&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could also remove &lt;code&gt;categories: [category]&lt;/code&gt; from the &lt;code&gt;dataSchema&lt;/code&gt; if we do not care about them individually. The categories will be inlined inside the movie object itself. However, that depends on how you process your data.&lt;/p&gt;
&lt;p&gt;Finally, this is how everything in the file should look like:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; normalize&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; denormalize&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; schema &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'normalizr'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; categoriesApi &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'iu6h3wr74vc2bhugglgzmp3hhu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Movie'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'4g35twfl3vhu3njuxgbtardjrm'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'TV Show'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'oiz6zif455h3hctzzhobak5l2y'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Other'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; moviesApi &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'3l5buhzt7rcbdmhntsrdlxd3h4'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Memento'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'iu6h3wr74vc2bhugglgzmp3hhu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'zxevaw7jkfbhdiflgfp5lxwrku'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Pulp Fiction'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'iu6h3wr74vc2bhugglgzmp3hhu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'uexridtrxnb3tls2az7x3eemvu'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Spirited Away'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'oiz6zif455h3hctzzhobak5l2y'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'x3eemv2az7x3etsrdlxd3h4dda'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'Breaking Bad'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'4g35twfl3vhu3njuxgbtardjrm'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; category &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;schema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'categories'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;idAttribute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'uuid'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; movie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;schema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;'movies'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; category&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;idAttribute&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;'uuid'&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dataSchema &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;movies&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;movie&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; normalized &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;normalize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;movies&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; moviesApi&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  dataSchema
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'entity store'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; normalized&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; denormalized &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;denormalize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;movies&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; normalized&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;movies&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  dataSchema&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  normalized&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;entities
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;'denormalized'&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; denormalized&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This short tutorial should explain the basics of normalization when using different API endpoints that reference each other. Just be sure to avoid circular references 🤯.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Monitoring with Checkmk]]></title><description><![CDATA[Any good server infrastructure needs monitoring to supervise its services. How I installed Checkmk on an Ubuntu VM and registered the homelab hosts with it.]]></description><link>https://angelo.dini.dev/blog/homelab-monitoring/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-monitoring/</guid><pubDate>Sat, 19 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;identify-issues-or-failures-real-time&quot;&gt;Identify issues or failures real-time&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-monitoring-at-home&quot;&gt;Why monitoring at home&lt;/h3&gt;
&lt;p&gt;It is good practice to monitor your infrastructure. You want to know when, where and why an issue occurred and ideally fix it quickly. At Divio we go even further and automate the process of resolving these issues as much as possible, so little human interaction is needed.&lt;/p&gt;
&lt;p&gt;However, at home, that might be a bit overkill. Nonetheless, I still want to know if something is not working as it should. Most notably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Diskspace&lt;/strong&gt;: react before space runs out and avoid outages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heat&lt;/strong&gt;: ensure the hardware is appropriately cooled for longevity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Usage&lt;/strong&gt;: how much CPU or memory is utilised&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;: is there any packet loss&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;monitoring-using-checkmk&quot;&gt;Monitoring using Checkmk&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-monitoring/assets/checkmk-720.webp 720w, /blog/homelab-monitoring/assets/checkmk.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-monitoring/assets/checkmk.png&quot; alt=&quot;Checkmk interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Checkmk interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;There are several free, open-source monitoring services out there. Most notably &lt;a href=&quot;https://www.nagios.org/&quot;&gt;Nagios&lt;/a&gt;, &lt;a href=&quot;https://www.zabbix.com/&quot;&gt;Zabbix&lt;/a&gt; and &lt;a href=&quot;https://prometheus.io/&quot;&gt;Prometheus&lt;/a&gt;/&lt;a href=&quot;https://grafana.com/grafana/&quot;&gt;Grafana&lt;/a&gt;. However, I settled for another popular one, &lt;strong&gt;Checkmk&lt;/strong&gt;. Checkmk has excellent docs, is easy to install, and the raw edition is entirely open-source. Connecting the infrastructure is also very straightforward by using Checkmk’s agents. To get started, make sure you have a separate Ubuntu VM instance running where we will install Checkmk on top of it.&lt;/p&gt;
&lt;p&gt;The Checkmk documentation has a pretty good &lt;a href=&quot;https://docs.checkmk.com/latest/en/intro_welcome.html&quot;&gt;Beginner’s guide&lt;/a&gt;, so I recommend reading through it to understand the basic principles. I will focus on the installation and configuration only in this article.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;To start the installation, download the correct package for your system from the &lt;a href=&quot;https://checkmk.com/download?edition=cfe&amp;amp;version=stable&amp;amp;dist=ubuntu&amp;amp;os=focal&quot;&gt;product selection&lt;/a&gt;. I will use the &lt;strong&gt;Checkmk Free Edition&lt;/strong&gt; to get &lt;a href=&quot;https://checkmk.com/product/editions&quot;&gt;additional features&lt;/a&gt; to play with that the raw edition doesn’t support. The free edition is limited to one instance and a maximum of 25 hosts, which is not a problem for me. However, if you want to monitor more hosts, progress by installing the raw edition.&lt;/p&gt;
&lt;p&gt;Download the package and install it on your server by moving it via &lt;code&gt;scp&lt;/code&gt; or directly:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; https://download.checkmk.com/checkmk/2.0.0p21/check-mk-free-2.0.0p21_0.focal_amd64.deb

&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; openssh-server
&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; dpkg-sig
&lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; https://download.checkmk.com/checkmk/Check_MK-pubkey.gpg
gpg &lt;span class=&quot;token parameter variable&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org --recv-keys 434DAC48C4503261
gpg &lt;span class=&quot;token parameter variable&quot;&gt;--armor&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--export&lt;/span&gt; 434DAC48C4503261 &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; Check_MK-pubkey.gpg
gpg &lt;span class=&quot;token parameter variable&quot;&gt;--import&lt;/span&gt; Check_MK-pubkey.gpg

dpkg-sig &lt;span class=&quot;token parameter variable&quot;&gt;--verify&lt;/span&gt; check-mk-free-2.0.0p21_0.focal_amd64.deb

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ./check-mk-free-2.0.0p21_0.focal_amd64.deb&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can test if the installation was successful by running &lt;code&gt;omd version&lt;/code&gt;. It should show something like &lt;code&gt;OMD - Open Monitoring Distribution Version 2.0.0p21.cre&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, you want to create a site. Follow the subsequent step-by-step instruction to &lt;a href=&quot;https://docs.checkmk.com/latest/en/intro_setup.html#create_site&quot;&gt;set up a site&lt;/a&gt;. In short:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;omd create homelab
omd status&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mind the instructions shown to you, and then start or stop your site by running &lt;code&gt;omd start&lt;/code&gt; or &lt;code&gt;omd stop&lt;/code&gt;, respectively.&lt;/p&gt;
&lt;p&gt;Finally, open the web interface &lt;a href=&quot;http://x.x.x.x/homelab&quot;&gt;http://x.x.x.x/homelab&lt;/a&gt; and login with the username &lt;code&gt;cmkadmin&lt;/code&gt; and the password you set up through the site creation process.&lt;/p&gt;
&lt;h3 id=&quot;agents&quot;&gt;Agents&lt;/h3&gt;
&lt;p&gt;Now, we need to add hosts to monitor. To do this, head to &lt;strong&gt;Setup &amp;gt; Agents (Windows, Linux, Solaris, AIX)&lt;/strong&gt;. The easiest is always to register the monitoring server itself. This step is also recommended by &lt;a href=&quot;https://docs.checkmk.com/latest/en/intro_setup_monitor.html&quot;&gt;Checkmk’s documentation&lt;/a&gt;. In our case, we download the “Linux / Deb” package. Rename that file on your computer to “agent.deb” and copy it over via &lt;code&gt;scp&lt;/code&gt;. Then install it:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;scp&lt;/span&gt; ~/Downloads/agent.deb user@x.x.x.x:/home/organizr/agent.deb
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; dpkg &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; ./agent.deb&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is all you have to do on any system you want to add. Some systems do not require an agent if they have built-in SNMP support. Also, have a look at the available agents. You can register ESXi and vSphere by using &lt;strong&gt;VMware ESX via vSphere&lt;/strong&gt; and track the VMs altogether.&lt;/p&gt;
&lt;h3 id=&quot;configuration&quot;&gt;Configuration&lt;/h3&gt;
&lt;p&gt;To add a new host, head to &lt;strong&gt;Setup &amp;gt; Hosts&lt;/strong&gt; and click on &lt;strong&gt;Add host&lt;/strong&gt;. The only option to configure is the IPv4 address. Alternatively, you can also set up a hostname and use it instead. Click on &lt;strong&gt;Save &amp;amp; go to service configuration&lt;/strong&gt; and add all discovered services by clicking on &lt;strong&gt;Fix all&lt;/strong&gt;. Next, on the top right, click on &lt;strong&gt;x changes&lt;/strong&gt; followed by &lt;strong&gt;Activate on selected sites&lt;/strong&gt;. This action will activate your first monitored host.&lt;/p&gt;
&lt;p&gt;Continue this with all other hosts until all of them are added. And then start figuring out what goes wrong and why by googling 🤣.&lt;/p&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;Well, that’s it 🤩. This is the last article I intended to add to this series. Though, there might be independent articles related to my homelab in the future.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Configuring Calibre, Organizr and more]]></title><description><![CDATA[Additional services to complement your homelab, such as Calibre, Ubooquity, Tautulli, Ombi and Organizr, to manage books, comics, statistics and requests.]]></description><link>https://angelo.dini.dev/blog/homelab-additional/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-additional/</guid><pubDate>Sat, 26 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;additional-services-to-complement-any-homelab&quot;&gt;Additional services to complement any homelab&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;more-services-for-our-toolbox&quot;&gt;More services for our toolbox&lt;/h3&gt;
&lt;p&gt;With Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar we cover a wide area of DVR systems to manage our media. However, many additional tools are available to make your life even more convenient. This section will cover some I find very helpful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/janeczku/calibre-web&quot;&gt;Calibre Web&lt;/a&gt; and &lt;a href=&quot;https://vaemendis.net/ubooquity/&quot;&gt;Ubooquity&lt;/a&gt; to manage eBooks and PDFs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tautulli.com/&quot;&gt;Tautulli&lt;/a&gt; and &lt;a href=&quot;https://ombi.io/&quot;&gt;Ombi&lt;/a&gt; to gather stats and manage requests&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://organizr.app/&quot;&gt;Organizr&lt;/a&gt; to have a friendly landing page for all the services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Remember that we are still using a Ubuntu VM, as with &lt;a href=&quot;/blog/homelab-services/&quot;&gt;the previous services&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;managing-ebooks-and-pdf&quot;&gt;Managing eBooks and PDF&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-additional/assets/calibre-web-720.webp 720w, /blog/homelab-additional/assets/calibre-web.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-additional/assets/calibre-web.png&quot; alt=&quot;Calibre web interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Calibre web interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The state of eBook and PDF management isn’t there yet. It would be great if Plex takes the “cord-cutting” to the next level by adding support, as it has been one of the &lt;a href=&quot;https://forums.plex.tv/t/plexreader-comics-books-pdfs/26684&quot;&gt;most requested features&lt;/a&gt; on the platform.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://calibre-ebook.com/&quot;&gt;Calibre&lt;/a&gt; is still the best option to go to until Readarr reaches maturity. The app itself works on any platform. Install it and designate a folder on your NAS to store all the eBooks or PDFs. Calibre creates a &lt;code&gt;metadata.db&lt;/code&gt; and &lt;code&gt;metadata_pre_restore.db&lt;/code&gt; file that hosts all the data necessary for your library. This process makes it easy to use the same library on different systems. It also has excellent plugin support to extend its functionality to, for example, also manage your Manga/Comic library.&lt;/p&gt;
&lt;p&gt;In addition to the app itself you can also install &lt;strong&gt;Calibre Web&lt;/strong&gt;. It provides an online interface where you can manage your library from anywhere you want. &lt;a href=&quot;https://github.com/janeczku/calibre-web&quot;&gt;The installation&lt;/a&gt; is also straightforward:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; python3.7 python3-setuptools
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/janeczku/calibre-web.git /opt/CalibreWeb
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /opt/CalibreWeb
python3 cps.py&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, open up &lt;a href=&quot;http://x.x.x.x:8083/config&quot;&gt;http://x.x.x.x:8083/config&lt;/a&gt; to check that everything is working. Complete the installation instructions and follow up by ensuring that the service starts automatically. Run &lt;code&gt;sudo vim /etc/systemd/system/calibreweb.service&lt;/code&gt; and add the following content:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Unit&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Calibre-Web

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Service&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;simple
&lt;span class=&quot;token assign-left variable&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl
&lt;span class=&quot;token assign-left variable&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/usr/bin/python3 /opt/CalibreWeb/cps.py
&lt;span class=&quot;token assign-left variable&quot;&gt;WorkingDirectory&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/opt/CalibreWeb

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Install&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adapt the &lt;code&gt;User&lt;/code&gt; entry to your system’s user. Finally, enable the service to start after a restart:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl daemon-reload
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; calibreweb
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start calibreweb&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;streaming-ebooks-and-pdf&quot;&gt;Streaming eBooks and PDF&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-additional/assets/ubooquity-720.webp 720w, /blog/homelab-additional/assets/ubooquity.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-additional/assets/ubooquity.png&quot; alt=&quot;Ubooquity interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Ubooquity interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Audiobooks technically work on Plex, but better tools are around, such as &lt;a href=&quot;https://github.com/advplyr/audiobookshelf&quot;&gt;Audiobookshelf&lt;/a&gt; or &lt;a href=&quot;https://github.com/popeen/Booksonic-Air&quot;&gt;Booksonic&lt;/a&gt; for streaming. I use Plex to browse and load them manually into Apple’s books app for synchronisation.&lt;/p&gt;
&lt;p&gt;Calibre isn’t the best choice either to stream Comics, eBooks and PDFs. Here is where &lt;strong&gt;Ubooquity&lt;/strong&gt; shines. It allows establishing OPDS feeds to stream its content to apps such as &lt;a href=&quot;https://apps.apple.com/us/app/chunky-comic-reader/id663567628&quot;&gt;Chunky&lt;/a&gt; or &lt;a href=&quot;https://apps.apple.com/us/app/tireader-ebook-reader/id1276508285&quot;&gt;tiReader&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The installation is more involving, as you need to have Java installed on your VM. The setup process looks something like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; default-jre
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; /opt/Ubooquity
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /opt/Ubooquity
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; http://vaemendis.net/ubooquity/service/download.php
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; download.php Ubooquity-2.1.2.zip
&lt;span class=&quot;token function&quot;&gt;unzip&lt;/span&gt; Ubooquity-2.1.2.zip&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, configure the service using &lt;code&gt;sudo vim /etc/systemd/system/ubooquity.service&lt;/code&gt; and add the following content:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Unit&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Ubooquity Daemon
&lt;span class=&quot;token assign-left variable&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;network.target

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Service&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl
&lt;span class=&quot;token assign-left variable&quot;&gt;Group&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl

&lt;span class=&quot;token assign-left variable&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;simple
&lt;span class=&quot;token assign-left variable&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/usr/bin/java &lt;span class=&quot;token parameter variable&quot;&gt;-jar&lt;/span&gt; /opt/Ubooquity/Ubooquity.jar &lt;span class=&quot;token parameter variable&quot;&gt;--workdir&lt;/span&gt; /opt/Ubooquity &lt;span class=&quot;token parameter variable&quot;&gt;--libraryport&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8097&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--adminport&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8098&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--remoteadmin&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--headless&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;TimeoutStopSec&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;KillMode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;process
&lt;span class=&quot;token assign-left variable&quot;&gt;Restart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;on-failure

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Install&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again ensure that you use the correct &lt;code&gt;User&lt;/code&gt; and &lt;code&gt;Group&lt;/code&gt;, and optionally change the &lt;strong&gt;libraryport&lt;/strong&gt; and &lt;strong&gt;adminport&lt;/strong&gt; to your liking. Then fix the permissions and enable the service:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;770&lt;/span&gt; /opt/Ubooquity &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; ddl:ddl /opt/Ubooquity &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl daemon-reload
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; ubooquity
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start ubooquity&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The admin is simple to understand and setup. Make sure to “Enable OPDS feed” from the advanced section.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bonus&lt;/strong&gt;: I’ve also created a nice Plex based theme that you can install by running &lt;code&gt;sudo git clone https://github.com/FinalAngel/plextheme.git -b master /opt/Ubooquity/themes/plextheme&lt;/code&gt;. Once installed, go to the admin and select it from the “Generals” tab.&lt;/p&gt;
&lt;h3 id=&quot;gather-statistics-and-manage-requests&quot;&gt;Gather statistics and manage requests&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-additional/assets/ombi-720.webp 720w, /blog/homelab-additional/assets/ombi.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-additional/assets/ombi.png&quot; alt=&quot;Ombi interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Ombi interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Plex added its Dashboard showing basic statistics about usage, play history, or top played media a while ago. However, &lt;strong&gt;Tautulli&lt;/strong&gt; offers deeper analytics and more graphs and filters. You can install it by executing:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; python3.7 python3-setuptools
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /opt
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/Tautulli/Tautulli.git
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; addgroup tautulli
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; adduser &lt;span class=&quot;token parameter variable&quot;&gt;--system&lt;/span&gt; --no-create-home tautulli &lt;span class=&quot;token parameter variable&quot;&gt;--ingroup&lt;/span&gt; tautulli
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; tautulli:tautulli /opt/Tautulli
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; /opt/Tautulli/init-scripts/init.systemd /lib/systemd/system/tautulli.service
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl daemon-reload
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; tautulli.service
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start tautulli.service&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, open up &lt;a href=&quot;http://x.x.x.x:8181&quot;&gt;http://x.x.x.x:8181&lt;/a&gt; and configure Tautulli to connect to your Plex server. The Tautulli login will synchronise with your Plex login.&lt;/p&gt;
&lt;p&gt;Finally, if you want to manage requests from friends or family, use WhatsApp or talk to them 🤣. Alternatively, install &lt;strong&gt;Ombi&lt;/strong&gt; and manage all requests centrally in one place. It can also be easily installed by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deb https://apt.ombi.app/master jessie main&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/ombi.list
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-sSL&lt;/span&gt; https://apt.ombi.app/pub.key &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; apt-key &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; -
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ombi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ombi has been installed successfully and will automatically start during boot if no errors are shown. It should be reachable by visiting &lt;a href=&quot;http://x.x.x.x:5000&quot;&gt;http://x.x.x.x:5000&lt;/a&gt;. Open the page and connect it to your Plex server as well. Finally, begin inviting users to allow them to make requests.&lt;/p&gt;
&lt;h3 id=&quot;managing-your-bookmarks&quot;&gt;Managing your bookmarks&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-additional/assets/organizr-720.webp 720w, /blog/homelab-additional/assets/organizr.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-additional/assets/organizr.png&quot; alt=&quot;Organizr interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Organizr interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Last but not least, we need a place to reach all our tools and services easily. &lt;strong&gt;Organizr&lt;/strong&gt; is great for this. It adds a nice interface to reach all of them. Host it on a separate Ubuntu VM instance; it interfered too much with my existing VMs. To install, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/elmerfdz/OrganizrInstaller /opt/OrganizrInstaller
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /opt/OrganizrInstaller/ubuntu/oui
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt; ou_installer.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once installed, open &lt;a href=&quot;http://x.x.x.x&quot;&gt;http://x.x.x.x&lt;/a&gt; using the IP from your VM and start adding your services 🍺.&lt;/p&gt;
&lt;h3 id=&quot;honourable-mentions&quot;&gt;Honourable mentions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://hakuneko.download/&quot;&gt;HakuNeko&lt;/a&gt; is fun to manage mangas&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Prowlarr/Prowlarr&quot;&gt;Prowlarr&lt;/a&gt; to manage indexers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.servarr.com/&quot;&gt;The Servarr wiki&lt;/a&gt; for a lot more services&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;We’re ready for the final chapter. We will set up monitoring for all of our “critical infrastructures” there.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Looking back at 2021]]></title><description><![CDATA[A short summary of what happened in 2021 in my life. It was eventful and stressful, dealing with school, work and private life all at once, as bullet points.]]></description><link>https://angelo.dini.dev/blog/looking-back-at-2021/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/looking-back-at-2021/</guid><pubDate>Thu, 20 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;looking-back-at-2021&quot;&gt;Looking back at 2021&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;2021 hasn't been short on events either. We continued to improve the user experience at work and added new functionality. Studies progressed into the subsequent semesters, and life is treating me well.&lt;/p&gt;&lt;p&gt;I continued journaling my experiences at work, school and personal life. As already in &lt;a href=&quot;/blog/looking-back-at-2020/&quot;&gt;2020&lt;/a&gt;, for my sanity and everyone else’s; as bullet points:&lt;/p&gt;
&lt;h3 id=&quot;work&quot;&gt;Work&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;We made several improvements with our documentation presentation and organisation.&lt;/li&gt;
&lt;li&gt;The continued effort moving away from Django templates to React delivers remarkable speed, efficiency and user experience.&lt;/li&gt;
&lt;li&gt;We are doing more and more pair coding and pair reviews, which is excellent for the product’s quality and knowledge exchange.&lt;/li&gt;
&lt;li&gt;We also had the chance to clean up many loose ends and improve on the overall build and quality of life stuff™.&lt;/li&gt;
&lt;li&gt;Several improvements to the marketing sites have been made, which are lovely.&lt;/li&gt;
&lt;li&gt;There are also new features available, like shortcuts on the control panel; check it out.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;study&quot;&gt;Study&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Completed two semesters and continued with the third.&lt;/li&gt;
&lt;li&gt;Improved on my Anki and Notion learning methods, adapted where needed. The Notion app is fantastic.&lt;/li&gt;
&lt;li&gt;Though I’m overall not very happy about the study/school itself, more on that probably in 2022.&lt;/li&gt;
&lt;li&gt;Again taught Modular Frontend Development at the Ostschweizer Fachhochschule (OST) 🥳&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;personal&quot;&gt;Personal&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I read about 50 books again this year, though several have been non-fiction for school 😅.&lt;/li&gt;
&lt;li&gt;I started playing Gloomhaven (Boardgame), which is very enjoyable.&lt;/li&gt;
&lt;li&gt;I also played some computer games again, including Cyberpunk 2077, Final Fantasy VII Remake and FFXIV.&lt;/li&gt;
&lt;li&gt;I gambled way too much with GME and the sorts with little gains 🤪.&lt;/li&gt;
&lt;li&gt;I continued playing the guitar, though a bit less due to studies.&lt;/li&gt;
&lt;li&gt;I installed a complete home lab in my cellar and started &lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;writing articles about it&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;looking-forward&quot;&gt;Looking forward&lt;/h3&gt;
&lt;p&gt;School consumed most of my time this year, and as such several other projects have been delayed. However, I’m sure 2022 will bring more changes, surprises and roadblocks as I go along.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Configuring Sonarr, Radarr and more]]></title><description><![CDATA[Setting up Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar. These services will allow us to monitor TV Shows, Movies, Music, Books, Comics and Mangas.]]></description><link>https://angelo.dini.dev/blog/homelab-services/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-services/</guid><pubDate>Wed, 29 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;dvr-services-to-manage-your-digital-content&quot;&gt;DVR services to manage your digital content&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;before-we-begin&quot;&gt;Before we begin&lt;/h3&gt;
&lt;p&gt;Continuing from &lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;, we’ll focus on setting up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sonarr.tv&quot;&gt;Sonarr&lt;/a&gt; to manage tv-shows&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://radarr.video/&quot;&gt;Radarr&lt;/a&gt; to manage movies&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lidarr.audio/&quot;&gt;Lidarr&lt;/a&gt; to manage music&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://readarr.com/&quot;&gt;Readarr&lt;/a&gt; to manage books&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bazarr.media/&quot;&gt;Bazarr&lt;/a&gt; to manage subtitles&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mylar3/mylar3/&quot;&gt;Mylar&lt;/a&gt; to manage comics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Certain services will use additional tools to organise the content. For example, with Radarr, we will manage movies through IMDB watchlists. Radarr will pull and synchronise the list and automatically download the requested movies in this scenario.&lt;/p&gt;
&lt;p&gt;I recommend creating one Ubuntu VM with about 30 GB of disk space to host the services below. The downloads will be stored and managed on the NAS, so the VM doesn’t need much space.&lt;/p&gt;
&lt;p&gt;There is also a separate section about &lt;a href=&quot;#migrating-from-an-existing-instance&quot;&gt;Migrating from an existing instance&lt;/a&gt; to assist in these cases.&lt;/p&gt;
&lt;h3 id=&quot;installing-sonarr&quot;&gt;Installing Sonarr&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-services/assets/sonarr-720.webp 720w, /blog/homelab-services/assets/sonarr.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-services/assets/sonarr.png&quot; alt=&quot;Sonarr interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Sonarr interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Let’s get started by installing Sonarr first. Go to the &lt;a href=&quot;https://sonarr.tv/#downloads-v3-linux-ubuntu&quot;&gt;Sonarr website&lt;/a&gt; and follow the instructions for Linux. As of this writing, you need to run the following sequence of commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; apt-key adv &lt;span class=&quot;token parameter variable&quot;&gt;--keyserver&lt;/span&gt; hkp://keyserver.ubuntu.com:80 --recv-keys 2009837CBFFD68F45BC180471F4F90DE2A9B4BF8
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deb https://apt.sonarr.tv/ubuntu focal main&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/sonarr.list
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; sonarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may be asked to provide a username during the installation process. If so, choose the same as you have for the VM.&lt;/p&gt;
&lt;p&gt;Once the installation finishes, check the status by running &lt;code&gt;sudo systemctl status sonarr&lt;/code&gt;. If there are no errors, open a browser and enter &lt;code&gt;http://x.x.x.x:8989&lt;/code&gt; to begin the initial configuration. Replace the &lt;strong&gt;x.x.x.x&lt;/strong&gt; with the IP address of your VM. If you are unsure, just run &lt;code&gt;ip a&lt;/code&gt; to get the current IP address. If the service doesn’t start automatically, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; sonarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start sonarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to clean up downloaded files from your server after the installation (usually inside &lt;strong&gt;~/&lt;/strong&gt;). This process will apply to all the services.&lt;/p&gt;
&lt;p&gt;See the bonus section about &lt;a href=&quot;#configuring-the-services&quot;&gt;Configuring the services&lt;/a&gt;, for further configuration details, as the configuration steps are very similar.&lt;/p&gt;
&lt;div class=&quot;breakline&quot;&gt;&lt;/div&gt;&lt;p&gt;I do not use any additional tools to manage tv-shows. There are some available such as &lt;a href=&quot;https://trakt.tv/&quot;&gt;trakt.tv&lt;/a&gt;. However, I found it easy to manage shows via the Sonarr user interface.&lt;/p&gt;
&lt;h3 id=&quot;installing-radarr&quot;&gt;Installing Radarr&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-services/assets/radarr-720.webp 720w, /blog/homelab-services/assets/radarr.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-services/assets/radarr.png&quot; alt=&quot;Radarr interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Radarr interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Next up is Radarr. Go to the &lt;a href=&quot;https://radarr.video/#downloads-v3-linux-debianubuntu&quot;&gt;Radarr website&lt;/a&gt; and follow the instructions for Linux. As of this writing, you need to run the following sequence of commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; mediainfo

&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-O&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://api.github.com/repos/Radarr/Radarr/releases &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; linux.tar.gz &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; browser_download_url &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;&quot; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-xvzf&lt;/span&gt; Radarr.master.*.linux.tar.gz
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; Radarr /opt

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;770&lt;/span&gt; /opt/Radarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt; /opt/Radarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, have a look at the community &lt;a href=&quot;https://wiki.servarr.com/radarr/installation#linux&quot;&gt;installation instructions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You may need to adapt the &lt;code&gt;User=&lt;/code&gt; and &lt;code&gt;Group=&lt;/code&gt; entries inside &lt;code&gt;/etc/systemd/system/radarr.service&lt;/code&gt; to match your VM’s username and group.&lt;/p&gt;
&lt;p&gt;Finally, run the following commands to start Radarr:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; radarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start radarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last line should inform you that Radarr has been started and is running on &lt;code&gt;http://x.x.x.x:7878&lt;/code&gt;. Replace &lt;strong&gt;x.x.x.x&lt;/strong&gt; with the IP of your VM and open it up in a browser.&lt;/p&gt;
&lt;div class=&quot;breakline&quot;&gt;&lt;/div&gt;&lt;p&gt;Managing movies through the Radarr user interface is also very simple. However, if you use IMDB often, you may be interested in the IMDB watchlist integration.&lt;/p&gt;
&lt;p&gt;Whenever you see a trailer or an exciting movie, add it to the watchlist and Radarr will automatically add and download it.&lt;/p&gt;
&lt;h3 id=&quot;installing-lidarr&quot;&gt;Installing Lidarr&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-services/assets/lidarr-720.webp 720w, /blog/homelab-services/assets/lidarr.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-services/assets/lidarr.png&quot; alt=&quot;Lidarr interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Lidarr interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Let’s continue by installing Lidarr next. Go to the &lt;a href=&quot;https://lidarr.audio/#downloads-v1-linux-debianubuntu&quot;&gt;Lidarr website&lt;/a&gt; and follow the instructions for Linux. As of this writing, you need to run the following sequence of commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; mediainfo

&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-O&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; https://api.github.com/repos/Lidarr/Lidarr/releases &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; linux.tar.gz &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; browser_download_url &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;&quot; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-xvzf&lt;/span&gt; Lidarr.master.*.linux.tar.gz
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; Lidarr /opt

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;770&lt;/span&gt; /opt/Lidarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt; /opt/Lidarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, have a look at the community &lt;a href=&quot;https://wiki.servarr.com/lidarr/installation#Debian.2FUbuntu&quot;&gt;installation instructions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You may need to adapt the &lt;code&gt;User=&lt;/code&gt; and &lt;code&gt;Group=&lt;/code&gt; entries inside &lt;code&gt;/etc/systemd/system/lidarr.service&lt;/code&gt; to match your VM’s username and group.&lt;/p&gt;
&lt;p&gt;Finally, run the following commands to start Lidarr:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; lidarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start lidarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last line should inform you that Lidarr has been started and is running on &lt;code&gt;http://x.x.x.x:8686&lt;/code&gt;. Replace &lt;strong&gt;x.x.x.x&lt;/strong&gt; with the IP of your VM and open it up in a browser.&lt;/p&gt;
&lt;div class=&quot;breakline&quot;&gt;&lt;/div&gt;&lt;p&gt;Lidarr can import existing artists and albums very easily. The initial setup requires some work, though. I do not use any additional services to manage my library. I buy the album and then load it into Lidarr.&lt;/p&gt;
&lt;h3 id=&quot;installing-readarr&quot;&gt;Installing Readarr&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-services/assets/readarr-720.webp 720w, /blog/homelab-services/assets/readarr.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-services/assets/readarr.png&quot; alt=&quot;Readarr interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Readarr interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Up next is Readarr. There is no packaged install, so I followed &lt;a href=&quot;https://www.reddit.com/r/DataHoarder/comments/is6spx/how_to_set_up_readarr_without_docker_on/&quot;&gt;this write-up on r/DataHoarder&lt;/a&gt; and built it from source.&lt;/p&gt;
&lt;p&gt;Be warned, though, Readarr is still in its early stages, and it is more difficult to install than any of the other services. So here’s what you need to do:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-O&lt;/span&gt; https://github.com/Readarr/Readarr/archive/develop.tar.gz
&lt;span class=&quot;token function&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-xvzf&lt;/span&gt; develop.tar.gz
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; Readarr-develop
&lt;span class=&quot;token comment&quot;&gt;# install https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# install https://classic.yarnpkg.com/en/docs/install/#mac-stable&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt; build.sh
&lt;span class=&quot;token function&quot;&gt;yarn&lt;/span&gt; build

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; /opt/Readarr
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; ~/Readarr-develop/_output/net462/linux-x64
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-av&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--progress&lt;/span&gt; * /opt/Readarr/

&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; ~/Readarr-develop/_output
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-av&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--progress&lt;/span&gt; UI /opt/Readarr/

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;770&lt;/span&gt; /opt/Readarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt; /opt/Readarr &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once all commands have run successfully, start the service by running &lt;code&gt;sudo /usr/bin/mono --debug /opt/Readarr/Readarr.exe -nobrowser&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This process should start Readarr, and you should be able to open the web interface through &lt;code&gt;http://x.x.x.x:8787&lt;/code&gt;. Replace &lt;strong&gt;x.x.x.x&lt;/strong&gt; with the IP of your VM and open it up in a browser.&lt;/p&gt;
&lt;p&gt;To create a startup file run &lt;code&gt;sudo vim /etc/systemd/system/readarr.service&lt;/code&gt;, and add the following contents to the file:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Unit&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Readarr Daemon
&lt;span class=&quot;token assign-left variable&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;network.target

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Service&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl
&lt;span class=&quot;token assign-left variable&quot;&gt;Group&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl

&lt;span class=&quot;token assign-left variable&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;simple
&lt;span class=&quot;token assign-left variable&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/usr/bin/mono &lt;span class=&quot;token parameter variable&quot;&gt;--debug&lt;/span&gt; /opt/Readarr/Readarr.exe &lt;span class=&quot;token parameter variable&quot;&gt;-nobrowser&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;TimeoutStopSec&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;KillMode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;process
&lt;span class=&quot;token assign-left variable&quot;&gt;Restart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;on-failure

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Install&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to adapt the &lt;code&gt;User=&lt;/code&gt; and &lt;code&gt;Group=&lt;/code&gt; entries accordingly.&lt;/p&gt;
&lt;p&gt;Alternatively, have a look at the community &lt;a href=&quot;https://wiki.servarr.com/readarr/installation&quot;&gt;installation instructions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, run the following commands to start Readarr:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; readarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start readarr&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;breakline&quot;&gt;&lt;/div&gt;&lt;p&gt;I manage my books through &lt;a href=&quot;https://calibre-ebook.com/&quot;&gt;Calibre&lt;/a&gt; as Readarr is still in its infancy. So for now, let’s sit back and watch its development 😅.&lt;/p&gt;
&lt;h3 id=&quot;installing-bazarr&quot;&gt;Installing Bazarr&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-services/assets/bazarr-720.webp 720w, /blog/homelab-services/assets/bazarr.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-services/assets/bazarr.png&quot; alt=&quot;Bazarr interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Bazarr interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Let’s tackle Bazarr now! Go to the &lt;a href=&quot;https://wiki.bazarr.media/Getting-Started/Installation/Linux/linux/&quot;&gt;Bazarr wiki&lt;/a&gt; and follow the instructions for Linux. As of this writing, you need to run the following sequence of commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; python3-dev python3-pip python3-distutils

&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-O&lt;/span&gt; https://github.com/morpheus65535/bazarr/releases/latest/download/bazarr.zip
&lt;span class=&quot;token function&quot;&gt;unzip&lt;/span&gt; bazarr.zip &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; Bazarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; Bazarr /opt

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;770&lt;/span&gt; /opt/Bazarr
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt; /opt/Bazarr

pip3 &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; /opt/Bazarr/requirements.txt
python3 bazarr.py&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last line should inform you that Bazarr has been started and is running on &lt;code&gt;http://x.x.x.x:6767&lt;/code&gt;. Replace &lt;strong&gt;x.x.x.x&lt;/strong&gt; with the IP of your VM and open it up in a browser.&lt;/p&gt;
&lt;div class=&quot;breakline&quot;&gt;&lt;/div&gt;&lt;p&gt;There’s not much to manage here. Bazarr automatically downloads the requested subtitles, and it just works.&lt;/p&gt;
&lt;h3 id=&quot;installing-mylar&quot;&gt;Installing Mylar&lt;/h3&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-services/assets/mylar-720.webp 720w, /blog/homelab-services/assets/mylar.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-services/assets/mylar.png&quot; alt=&quot;Mylar interface&quot; width=&quot;1024&quot; height=&quot;580&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Mylar interface&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Let’s wrap this up by installing &lt;a href=&quot;https://github.com/mylar3/mylar3&quot;&gt;Mylar&lt;/a&gt;! The installation process is also a bit more involving but still manageable. Run the following commands to get started:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/mylar3/mylar3 &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; master /opt/Mylar

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;770&lt;/span&gt; /opt/Mylar &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt; /opt/Mylar &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt;

pip3 &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; /opt/Mylar/requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we want to create the systemd process, do this by running &lt;code&gt;sudo cp /opt/Mylar/init-scripts/systemd/mylar.service /etc/systemd/system/mylar.service&lt;/code&gt; followed by &lt;code&gt;sudo vim /etc/systemd/system/mylar.service&lt;/code&gt;. Make sure the content of the file looks something like this:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Unit&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;Description&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;Mylar3 Daemon
&lt;span class=&quot;token assign-left variable&quot;&gt;After&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;network-online.target

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Service&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl
&lt;span class=&quot;token assign-left variable&quot;&gt;Group&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;ddl

&lt;span class=&quot;token assign-left variable&quot;&gt;ExecStart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/usr/bin/python3 /opt/Mylar/Mylar.py &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--daemon&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--nolaunch&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--quiet&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;GuessMainPID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;no
&lt;span class=&quot;token assign-left variable&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;forking
&lt;span class=&quot;token assign-left variable&quot;&gt;Restart&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;on-failure

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Install&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;WantedBy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to adapt the &lt;code&gt;User=&lt;/code&gt; and &lt;code&gt;Group=&lt;/code&gt; entries accordingly.&lt;/p&gt;
&lt;p&gt;Finally, run the following commands to start Mylar:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; mylar
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start mylar&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last line should inform you that Mylar has been started and is running on &lt;code&gt;http://x.x.x.x:8090&lt;/code&gt;. Replace &lt;strong&gt;x.x.x.x&lt;/strong&gt; with the IP of your VM and open it up in a browser.&lt;/p&gt;
&lt;div class=&quot;breakline&quot;&gt;&lt;/div&gt;&lt;p&gt;Mylar does a decent job to manage your favourite comics or manga. I use the &lt;a href=&quot;https://apps.apple.com/us/app/chunky-comic-reader/id663567628&quot;&gt;Chunky Comic Reader&lt;/a&gt; and &lt;a href=&quot;https://vaemendis.net/ubooquity/&quot;&gt;Ubooquity&lt;/a&gt; to read them on my devices. We’ll get into those extra services in the next blog post.&lt;/p&gt;
&lt;p&gt;&lt;span id=&quot;configuring-the-services&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;bonus-configuring-the-services&quot;&gt;Bonus: Configuring the services&lt;/h3&gt;
&lt;p&gt;Changing the default port for each service once the setup is completed to avoid clashes is recommended. Choose a port that suits you and increment each time, e.g. &lt;strong&gt;8091&lt;/strong&gt;, &lt;strong&gt;8092&lt;/strong&gt; and so on.&lt;/p&gt;
&lt;p&gt;Additionally, choose a username and password to protect access to the services.&lt;/p&gt;
&lt;p&gt;Finally, the most critical part is configuring the connection to SABnzbd under &lt;strong&gt;Download Clients&lt;/strong&gt;. You should have no trouble adding all the necessary information. Make sure to add the appropriate &lt;strong&gt;Category&lt;/strong&gt; that we defined in the &lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;previous article&lt;/a&gt;. Use the &lt;strong&gt;Test&lt;/strong&gt; button to check if everything is correctly configured.&lt;/p&gt;
&lt;p&gt;You also need to add your &lt;strong&gt;Indexers&lt;/strong&gt;. There are plenty of presets to choose from. Pick the one that fits your indexers and adapt them accordingly or have a look at &lt;a href=&quot;https://github.com/Prowlarr/Prowlarr&quot;&gt;Prowlarr&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Other than that, I tend to leave the settings at their default values. You may want to choose different naming schemes, better qualities and profiles. I advise you to play around with the settings and then manually sync them across the services.&lt;/p&gt;
&lt;p&gt;&lt;span id=&quot;migrating-from-an-existing-instance&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;bonus-migrating-from-an-existing-instance&quot;&gt;Bonus: Migrating from an existing instance&lt;/h3&gt;
&lt;p&gt;If you have an existing installation backup the &lt;code&gt;&amp;lt;service&amp;gt;.db&lt;/code&gt; and copy it over to the new installation directory. The default directory is normally &lt;code&gt;/var/lib/&amp;lt;service&amp;gt;/&lt;/code&gt; or &lt;code&gt;/opt/&amp;lt;service&amp;gt;/&lt;/code&gt;. My migration commands looked something like this with Sonarr:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# do this on both instances&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl stop sonarr
&lt;span class=&quot;token comment&quot;&gt;# copy over the database&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tar&lt;/span&gt; cvfz sonarr.tar.gz nzbdrone/
&lt;span class=&quot;token function&quot;&gt;scp&lt;/span&gt; sonarr.tar.gz username@x.x.x.x:~/
&lt;span class=&quot;token function&quot;&gt;tar&lt;/span&gt; xvf sonarr.tar.gz
&lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; sonarr.db /var/lib/sonarr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simply repeat the process for the individual services.&lt;/p&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;We’re ready for the next chapter with the services and download managers set up. We will set up additional services that can help manage our ever-growing digital library even more.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Setting up a VPN server using WireGuard]]></title><description><![CDATA[Step-by-step instructions for running a WireGuard VPN server at home on Ubuntu, with a macOS client, so you can reach local resources without open ports.]]></description><link>https://angelo.dini.dev/blog/vpn-setup/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/vpn-setup/</guid><pubDate>Sat, 27 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;understanding-the-vpn-options&quot;&gt;Understanding the VPN options&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Accessing network resources through various open ports on a firewall isn't the most secure option. It's safer to create a VPN tunnel and block other traffic from your network.&lt;/p&gt;&lt;p&gt;Setting up a VPN server isn’t always that easy. If your router doesn’t support VPN out of the box, you may need to use third-party firmware or host a VPN server yourself.&lt;/p&gt;
&lt;p&gt;Creating a VPN server, however, has its own set of problems. Both Windows and Apple offer VPN services by themselves. These aren’t the most secure options around and difficult to get working right.&lt;/p&gt;
&lt;p&gt;OpenVPN, ProtonVPN and the likes offer you friendly services to get started, but I wanted to host my own server to have complete control without external influence.&lt;/p&gt;
&lt;h3 id=&quot;hello-wireguard&quot;&gt;Hello WireGuard&lt;/h3&gt;
&lt;p&gt;This is where WireGuard comes in. It works on Windows, macOS, Linux, iOS and Android, covering most use cases.&lt;/p&gt;
&lt;p&gt;It is built on modern cryptography (Curve25519, ChaCha20, Poly1305), speaks UDP, and it is small enough that you can read the whole thing. It is also incredibly fast, which is specifically not the case with some of the alternatives.&lt;/p&gt;
&lt;p&gt;WireGuard knows the concept of servers and clients. The &lt;strong&gt;server&lt;/strong&gt; is where you set up your VPN to connect to, that forwards the traffic to your internal network. &lt;strong&gt;Clients&lt;/strong&gt; connect to the server. For this to work, the server needs to know the public keys and IPs of the &lt;strong&gt;peers&lt;/strong&gt; (clients) that want to connect, and each client needs the server’s location and public key. This approach allows for a very secure connection.&lt;/p&gt;
&lt;p&gt;The following sections guide you through a step-by-step guide on setting up the server and a client example using macOS.&lt;/p&gt;
&lt;h3 id=&quot;configuring-the-server&quot;&gt;Configuring the server&lt;/h3&gt;
&lt;p&gt;Consult the &lt;a href=&quot;https://www.wireguard.com/install/&quot;&gt;official installation&lt;/a&gt; instructions for in-depth information and configuration options.&lt;/p&gt;
&lt;p&gt;I set up my VPN &lt;strong&gt;server&lt;/strong&gt; using a virtualised Ubuntu server 20.04 instance. Any other OS will do; just consult the setup instructions for your preferred OS.&lt;/p&gt;
&lt;p&gt;After the initial installation and security updates, run the following commands to install WireGuard:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; wireguard&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check that there are no error messages or warnings. If everything ran smoothly, generate a public and private key by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /etc/wireguard/
&lt;span class=&quot;token builtin class-name&quot;&gt;umask&lt;/span&gt; 077&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; wg genkey &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; privatekey &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; wg pubkey &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; publickey
&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; publickey
&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; privatekey&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last two commands will output the public and private keys (in that order). You will need these for future references.&lt;/p&gt;
&lt;p&gt;Next, create a WireGuard configuration file using &lt;code&gt;sudo vim /etc/wireguard/wg0.conf&lt;/code&gt;. “wg0” will be used as a network interface name to establish the connection.&lt;/p&gt;
&lt;p&gt;That’s the intelligent thing about WireGuard. It presents itself as an ordinary network interface, so all the usual routing and firewall tools keep working.&lt;/p&gt;
&lt;p&gt;Add the following content to that file:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# VPN Server&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Interface&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
PrivateKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;the private key shown earlier&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
Address &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.x.1/24
ListenPort &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;51820&lt;/span&gt;
PostUp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; iptables &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; FORWARD &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; wg0 &lt;span class=&quot;token parameter variable&quot;&gt;-j&lt;/span&gt; ACCEPT&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; iptables &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; nat &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; POSTROUTING &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; ens160 &lt;span class=&quot;token parameter variable&quot;&gt;-j&lt;/span&gt; MASQUERADE
PostDown &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; iptables &lt;span class=&quot;token parameter variable&quot;&gt;-D&lt;/span&gt; FORWARD &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; wg0 &lt;span class=&quot;token parameter variable&quot;&gt;-j&lt;/span&gt; ACCEPT&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; iptables &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; nat &lt;span class=&quot;token parameter variable&quot;&gt;-D&lt;/span&gt; POSTROUTING &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; ens160 &lt;span class=&quot;token parameter variable&quot;&gt;-j&lt;/span&gt; MASQUERADE

&lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Peer&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
PublicKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;to be added later&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
AllowedIPs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;to be added later&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; replace the following entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;192.168.x.1/24&lt;/code&gt; with your desired IP. That should be a different IP subnet from your internal network. It will be used to connect to and redirect traffic. E.g. if you use 10.0.0.0 for your internal network, choose something like 10.0.10.1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ens160&lt;/code&gt; replace this with your primary network interface. It is the interface that’s connected to your internal network.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;51820&lt;/code&gt; you can choose another port. When doing so, adapt it to other places as well. I will use the standard port in this example.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;PostUp&lt;/code&gt; ensures that the VPN server automatically forwards the traffic through NAT into your network once the service is started. &lt;code&gt;PostDown&lt;/code&gt; accordingly resets the configuration.&lt;/p&gt;
&lt;p&gt;Next, if you have a firewall setup, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt;/tcp
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw allow &lt;span class=&quot;token number&quot;&gt;51820&lt;/span&gt;/udp
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ufw status&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s see if everything worked correctly, start the service and check that everything is working as expected by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; wg-quick@wg0
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start wg-quick@wg0
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl status wg-quick@wg0
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; wg
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ip&lt;/span&gt; a show wg0&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;configuring-a-client&quot;&gt;Configuring a client&lt;/h3&gt;
&lt;p&gt;Once the server is set up, we need clients that connect to it. Clients have their WireGuard service configured with private and public keys. They can only link to the server if the server is aware that they exist. As such, we need to create a client and add the information to the server.&lt;/p&gt;
&lt;p&gt;In our example, I’ll use a macOS computer to connect to my VPN server. Go to the &lt;a href=&quot;https://www.wireguard.com/install/&quot;&gt;WireGuard installation&lt;/a&gt; section and choose the software for your OS.&lt;/p&gt;
&lt;p&gt;Open the application and click on the “+” to “Add an empty tunnel”. Choose a name and create the following configuration:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Interface&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
PrivateKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;should already be there&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
Address &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.x.2/32
DNS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.1.1

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Peer&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
PublicKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;public key from the server&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
AllowedIPs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;.0.0/0
Endpoint &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; server-ip-address:51820
PersistentKeepalive &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;25&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; replace the following entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;192.168.x.2/32&lt;/code&gt; use here the same IP as configured on the server. Just increment the host part 1. Further clients will keep increasing the IP host, e.g. 192.168.x.3/32.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DNS = &lt;/code&gt; make sure you add there the default DNS address of your router. This step ensures that the internet will continue to work when establishing a VPN connection.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server-ip-address&lt;/code&gt; replace this with the public IP address of your VPN server. DynDNS entries work here as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that configuration is done, go back to the servers configuration file and adapt the following information:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Peer&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
PublicKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;public key from the client&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
AllowedIPs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.x.2/32&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The allowed IP will be the same configured on the client. Also, copy and paste the client public key into the configuration.&lt;/p&gt;
&lt;p&gt;Once done, configure port forwarding and activate the connection on the client.&lt;/p&gt;
&lt;h3 id=&quot;configure-port-forwarding&quot;&gt;Configure port forwarding&lt;/h3&gt;
&lt;p&gt;Check the manual for your router and enable port forwarding for &lt;strong&gt;51820&lt;/strong&gt;. This configuration will allow connections from the internet to your VPN server.&lt;/p&gt;
&lt;p&gt;Save all configuration files and restart WireGuard on the server with &lt;code&gt;sudo systemctl restart wg-quick@wg0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finally, go to your client and &lt;strong&gt;activate&lt;/strong&gt; the connection. Congratulations, you have now access to your network through a subscription-free service you own yourself ;)&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Configuring SABnzbd and Transmission]]></title><description><![CDATA[Installing and configuring SABnzbd and Transmission on an Ubuntu VM, plus the NFS mounts to the NAS, before the Sonarr and Radarr setup in the next chapter.]]></description><link>https://angelo.dini.dev/blog/homelab-downloads/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-downloads/</guid><pubDate>Sun, 17 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;manage-your-downloads&quot;&gt;Manage your downloads&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-we-need&quot;&gt;What we need&lt;/h3&gt;
&lt;p&gt;Continuing from &lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;, we’ll focus on setting up &lt;a href=&quot;https://sabnzbd.org/&quot;&gt;SABnzbd&lt;/a&gt; and &lt;a href=&quot;https://transmissionbt.com/&quot;&gt;Transmission&lt;/a&gt; for our download management.&lt;/p&gt;
&lt;p&gt;This step is needed before we install and configure Sonarr, Radarr, Lidarr and the likes.&lt;/p&gt;
&lt;h3 id=&quot;installing-sabnzbd&quot;&gt;Installing SABnzbd&lt;/h3&gt;
&lt;p&gt;Create an Ubuntu VM with about 30 GB of disk space. Our primary download directory will be on the NAS, so the VM doesn’t need much space.&lt;/p&gt;
&lt;p&gt;A section is available that demonstrates how to &lt;a href=&quot;#extra-setting-up-mounts&quot;&gt;set up the mounts&lt;/a&gt; on Ubuntu to access the NAS volumes.&lt;/p&gt;
&lt;p&gt;Let’s get started! Inside your VM run the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; sabnzbdplus&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, check the status running &lt;code&gt;sudo systemctl status sabnzbdplus&lt;/code&gt;. If everything appears fine shut the service down by running &lt;code&gt;sudo systemctl stop sabnzbdplus&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then, edit the configuration file by executing &lt;code&gt;sudo vim /etc/default/sabnzbdplus&lt;/code&gt; and change the content to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;USER={user}&lt;/strong&gt; (replace {user} with the Ubuntu user)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HOST=0.0.0.0&lt;/strong&gt; (important to allow external access)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PORT=8089&lt;/strong&gt; (or to any port you like)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save the file and restart the service by running &lt;code&gt;sudo systemctl start sabnzbdplus&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Finally, open SABnzbd using &lt;a href=&quot;http://x.x.x.x:PORT&quot;&gt;http://x.x.x.x:PORT&lt;/a&gt;. Replace x.x.x.x with your IP and PORT with the configured port.&lt;/p&gt;
&lt;h3 id=&quot;configuring-sabnzbd&quot;&gt;Configuring SABnzbd&lt;/h3&gt;
&lt;p&gt;Once you open the above URL you’ll be welcomed by the SABnzbd configuration wizard.&lt;/p&gt;
&lt;p&gt;Choose a language for your system and then add the server details for your Usenet provider. I use &lt;a href=&quot;https://www.supernews.com/?i=YwoUGuga6G8=&quot;&gt;Supernews&lt;/a&gt;, which is pretty good and cheap. Enable SSL if your provider supports it.&lt;/p&gt;
&lt;p&gt;Server configuration can also be done later in the &lt;strong&gt;Servers&lt;/strong&gt; section by choosing to &lt;strong&gt;Add Server&lt;/strong&gt;. Make sure to test the connection.&lt;/p&gt;
&lt;p&gt;Continue with the completed and temporary folders for now. These will be created in &lt;strong&gt;~/downloads&lt;/strong&gt;. We will clean this up later when configuring the remaining settings.&lt;/p&gt;
&lt;p&gt;Head over to general and adapt the following settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Activate &lt;strong&gt;Advanced Settings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;SABnzbd Username: Choose a username&lt;/li&gt;
&lt;li&gt;SABnzbd Password: Choose a password&lt;/li&gt;
&lt;li&gt;External internet access: &lt;strong&gt;Full Web interface - Only external access requires login&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hit &lt;strong&gt;Save Changes&lt;/strong&gt;. Next, head over to User Folders. Create a &lt;code&gt;downloads/&lt;/code&gt; folder on your NAS and then set:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporary Download Folder: &lt;code&gt;/path-to-nas/downloads/incomplete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Completed Download Folder: &lt;code&gt;/path-to-nas/downloads/complete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Watched Folder: &lt;code&gt;/path-to-nas/downloads/watching&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Permissions for completed downloads: set to 777&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course, you can also choose a different name instead of &lt;code&gt;downloads&lt;/code&gt;; adapt the paths accordingly. However, make sure that the corresponding folder has full access set:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chmod 777 /path-to-nas/downloads/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It may also help to create the three folders (&lt;strong&gt;incomplete, complete, watching&lt;/strong&gt;) in the &lt;code&gt;downloads/&lt;/code&gt; directory and to provide the same access rights. Leave the rest and hit &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Next, browse to &lt;strong&gt;Categories&lt;/strong&gt;. In categories, you want to add or modify the following rows:&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/homelab-downloads/assets/sabnzbd-categories-720.webp 720w, /blog/homelab-downloads/assets/sabnzbd-categories.webp 1400w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/homelab-downloads/assets/sabnzbd-categories.png&quot; alt=&quot;The SABnzbd Categories settings with the rows to add or modify&quot; width=&quot;2286&quot; height=&quot;1132&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;The SABnzbd Categories settings with the rows to add or modify&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Finally, head to the switches section and change the following settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Direct Unpack&lt;/strong&gt;: enable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace dots in folder name&lt;/strong&gt;: enable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable Indexer Integration&lt;/strong&gt;: enable and add API key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s it, SABnzbd is configured 😊&lt;/p&gt;
&lt;h3 id=&quot;installing-transmission&quot;&gt;Installing Transmission&lt;/h3&gt;
&lt;p&gt;Besides Usenet, you can also set up a torrent downloader of your choice. This section focuses on Transmission.&lt;/p&gt;
&lt;p&gt;I recommend following the installation from &lt;a href=&quot;https://help.ubuntu.com/community/TransmissionHowTo&quot;&gt;the community&lt;/a&gt;. In summary:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; add-apt-repository ppa:transmissionbt/ppa
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; transmission-cli transmission-common transmission-daemon
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt; transmission-daemon stop&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, follow the configuration steps for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/var/lib/transmission-daemon/info/settings.json&lt;/code&gt; and&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/home/{USER}/.config/transmission-daemon/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Similar to SABnzbd, you can create a &lt;strong&gt;torrents&lt;/strong&gt; folder under downloads and set some access rights:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;usermod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-G&lt;/span&gt; debian-transmission &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;YOUR_USER&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chgrp&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; debian-transmission /media/nas/downloads/torrents/
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;777&lt;/span&gt; /media/nas/downloads/torrents/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Additional permissions may need to be adapted:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;usermod&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-G&lt;/span&gt; debian-transmission &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token environment constant&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl stop transmission-daemon
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token environment constant&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;:&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GROUP&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; /etc/transmission-daemon/
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token environment constant&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;:&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GROUP&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; /var/lib/transmission-daemon/
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start transmission-daemon

&lt;span class=&quot;token function&quot;&gt;netstat&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-an&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; LISTEN&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it, transmission should be up and running on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://x.x.x.x:PORT/transmission/web/&quot;&gt;http://x.x.x.x:PORT/transmission/web/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;extra-setting-up-mounts&quot;&gt;Extra: Setting up mounts&lt;/h3&gt;
&lt;p&gt;QNAP supports several protocols though I chose to use NFS as it worked best for my use case.&lt;/p&gt;
&lt;p&gt;First, folders need to be created to mount the individual folders too. In my case, I used &lt;code&gt;/media/&lt;/code&gt; and added the subsequent sub-folders inside it.&lt;/p&gt;
&lt;p&gt;Next, install the requirements into Ubuntu:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; network-manager
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; nfs-common&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can check if a connection can be established like:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mount&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; nfs x.x.x.x:/server/folder /media/photos&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and replace x.x.x.x with the IP address of the storage server. If all works prepare the &lt;code&gt;/etc/fstab&lt;/code&gt; file as follows:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;x.x.x.x:/server/folder1 /media/folder1 nfs _netdev,auto,x-systemd.automount &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
x.x.x.x:/server/folder2 /media/folder2 nfs _netdev,auto,x-systemd.automount &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
x.x.x.x:/server/folder3 /media/folder3 nfs _netdev,auto,x-systemd.automount &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace x.x.x.x again with the IP address of the storage server and map the respective folders to their counterpart on Ubuntu. The additional options ensure that the folders get automatically mounted after a restart and wait for the server to be available.&lt;/p&gt;
&lt;h3 id=&quot;whats-next&quot;&gt;What’s next&lt;/h3&gt;
&lt;p&gt;We have our download managers set up. Next, we will configure the DVR recorders 🥳&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[An alcohol-free weekend]]></title><description><![CDATA[I love beer, and alcohol-free beers are on the rise. A tasting weekend with a friend, seventeen bottles from Swiss shops, prices, notes and star ratings.]]></description><link>https://angelo.dini.dev/blog/alcohol-free/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/alcohol-free/</guid><pubDate>Thu, 30 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-review-of-different-brands&quot;&gt;A review of different brands&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I've been moving away from alcohol-based beverages to alcohol-free variants for a while now. The main reason wasn't based on health. I just had it with the after-effects from drinking. 🤣&lt;/p&gt;&lt;p&gt;You can enjoy drinking at a party without alcohol. This epiphany took me a while to realise. The cultural environment and peer pressure I lived in forces almost any young adult into a sip of beer, wine or stronger shi🤬. Of course, you can refuse and be abstinent, but that’s difficult when you are sixteen years old (the legal drinking age in Switzerland) and want to join the fray.&lt;/p&gt;
&lt;p&gt;Sadly I started drinking at a much younger age, as it is often so in Wallis (a province known in Switzerland for its accent and drinking). I was around six years old and mistook a bottle of red wine with syrup. You can’t qualify this as a career starter, though. I had my first hangover when I was around thirteen and enjoyed my youth with plenty of binge drinking.&lt;/p&gt;
&lt;h3 id=&quot;discovering-alternatives&quot;&gt;Discovering alternatives&lt;/h3&gt;
&lt;p&gt;That being said, I started to move away from alcoholic beverages to alcohol-free variants at around thirty. Back then, the market was still tiny, and there were only a handful of alcohol-free beers or wine available. The most famous of them was “Bilz”, which I loved to drink even when I was younger.&lt;/p&gt;
&lt;p&gt;Today there are more alternatives available than ever before, and the market is growing fast. Supermarkets are picking up the pace and extend the choices even further, which is nice!&lt;/p&gt;
&lt;p&gt;Luckily, I’m not alone with the notion of finding alternatives. This summer, I sat down with a friend and ordered almost every alcohol-free beer from Coop, a supermarket chain in Switzerland. The idea was to test and compare them during a beer tasting weekend 🍺.&lt;/p&gt;
&lt;p&gt;It was pretty fun! Below you find a list of all beers we tried with the brand name, price and a small review:&lt;/p&gt;
&lt;h3 id=&quot;the-review&quot;&gt;The review&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: The review&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brand&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/leermond.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/leermond.png&quot; alt=&quot;A bottle of Appenzeller Leermond&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Appenzeller Leermond&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;3.79/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.brack.ch/appenzeller-bier-bier-leermond-0-0-alkoholfrei-6-x-0-33-l-1230105&quot;&gt;Brack&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Fruity and smooth taste. A great replacement for a typical lager beer.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/sonnwendig.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/sonnwendig.png&quot; alt=&quot;A bottle of Appenzeller Sonnwendlig&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Appenzeller Sonnwendlig&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;3.79/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/beer-multipacks/appenzeller-alcohol-free-sonnwendlig-beer-6x33cl/p/4949444&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Has a watery, slightly fruity taste. Typical lager without any special surprises.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/weissbier.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/weissbier.png&quot; alt=&quot;A bottle of Appenzeller Wheat&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Appenzeller Wheat&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;3.90/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/wheat/bottles/appenzeller-alcohol-free-wheat-beer-4x50cl/p/6387411&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Very nice and authentic wheat taste. Recommended as wheat beer alternative.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/bilz.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/bilz.png&quot; alt=&quot;A bottle of Bilz Panaché&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Bilz Panaché&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;4.20/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/bilz-panache-00-alcohol-free-mixed-beer-10x33cl/p/4951070&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;It has to be here cause of childhood memories. In general, very sweet.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/brewdog-punk-af.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/brewdog-punk-af.png&quot; alt=&quot;A bottle of Brewdog Punk AF&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Brewdog Punk AF&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Scotland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;9.70/100cl&lt;/mark&gt; – 0.5% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/brewdog-alcohol-free-punk-beer-4x33cl/p/6460739&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Fruity and malty taste with a slight bitterness. Gets close to a typical IPA feel.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/chopfab.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/chopfab.png&quot; alt=&quot;A bottle of Chopfab Bleifrei&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Chopfab Bleifrei&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;5.91/100cl&lt;/mark&gt; – 0.5% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/chopfab-alcohol-free-wheat-free-beer-6x33cl/p/6097192&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Slight fruity and mild taste. A very balanced lager and recommendable.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/easy-rider-bulldog.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/easy-rider-bulldog.png&quot; alt=&quot;A bottle of Easy Rider Bulldog&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Easy Rider Bulldog&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Sweden&lt;/kbd&gt; &amp;nbsp; ⭐⭐ &lt;br /&gt;&lt;mark&gt;10.91/100cl&lt;/mark&gt; – 0.4% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/gotlands-alcohol-free-easy-rider-beer/p/6323076&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Has a velvety, watery and bitter taste. A mild taste for an IPA.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/feldschloesschen.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/feldschloesschen.png&quot; alt=&quot;A bottle of Feldschlösschen&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Feldschlösschen&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;3.48/100cl&lt;/mark&gt; – 0.5% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/beer-multipacks/feldschloesschen-alcohol-free-lager-beer-6x50cl/p/3507288&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Watery and neutral taste, good to drink very cold. Decent lager beer.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/gran-alpin-senza.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/gran-alpin-senza.png&quot; alt=&quot;A bottle of Gran Alpin Senza&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gran Alpin Senza&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;5.03/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/naturaplan-gran-alpin-senza-6x33cl/p/6699008&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Fruity and sweet taste with a hint of citrus. Has a slight bitter aftertaste.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/grimbergen.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/grimbergen.png&quot; alt=&quot;A bottle of Grimbergen&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Grimbergen&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;France&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;7.67/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/grimbergen-00-alcohol-free-beer-6x25cl/p/6764048&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Very sweet taste with variants of spices, and no bitter aftertaste.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/heineken-00.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/heineken-00.png&quot; alt=&quot;A bottle of Heineken 0.0&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Heineken 0.0&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Netherlands&lt;/kbd&gt; &amp;nbsp; ⭐ &lt;br /&gt;&lt;mark&gt;4.49/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/heineken-alcohol-free-00-beer-6x33cl/p/6100096&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Watery taste, misses any sign of flavour like the original.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/lola.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/lola.png&quot; alt=&quot;A bottle of Lola&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lola&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Germany&lt;/kbd&gt; &amp;nbsp; ⭐⭐ &lt;br /&gt;&lt;mark&gt;11.52/100cl&lt;/mark&gt; – 0.5% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/lola-alcohol-free-beer/p/6103029&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;A very dry but slightly fruity IPA. A mild taste for an IPA.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/lola-wit.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/lola-wit.png&quot; alt=&quot;A bottle of Lola Wit&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lola Wit&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Germany&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;11.52/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/lola-wit-alcohol-free-beer/p/6461428&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Very wheaty and soft beer. A good alternative for a good wheat beer.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/lowlander-ipa.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/lowlander-ipa.png&quot; alt=&quot;A bottle of Lowlander IPA&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lowlander IPA&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Netherlands&lt;/kbd&gt; &amp;nbsp; ⭐⭐⭐ &lt;br /&gt;&lt;mark&gt;9.60/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://urbndrnk.ch/en/products/lowlander-ipa-alkoholfrei-12x33cl&quot;&gt;UrbnDrnk&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Has a refreshing, spicy note. Very sparkly with a taste of cinnamon and orange.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/lowlander-wit.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/lowlander-wit.png&quot; alt=&quot;A bottle of Lowlander Wit&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lowlander Wit&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Netherlands&lt;/kbd&gt; &amp;nbsp; ⭐⭐ &lt;br /&gt;&lt;mark&gt;9.60/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://urbndrnk.ch/en/products/lowlander-0-00-alkoholfrei-12x33cl&quot;&gt;UrbnDrnk&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Very sweet and fruity taste with a hint of citrus.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/riegele-ipa.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/riegele-ipa.png&quot; alt=&quot;A bottle of Riegele IPA Liberis&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Riegele IPA Liberis&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Germany&lt;/kbd&gt; &amp;nbsp; ⭐⭐ &lt;br /&gt;&lt;mark&gt;10.30/100cl&lt;/mark&gt; – 0.4% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/ale-amber/bottles/riegele-alcohol-free-ipa-liberis-beer/p/6324322&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Very strong bitter and malty taste, stronger than a usual IPA.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/alcohol-free/assets/schuetzengarten.webp&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/alcohol-free/assets/schuetzengarten.png&quot; alt=&quot;A bottle of Schützengarten IPA&quot; width=&quot;200&quot; height=&quot;300&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Schützengarten IPA&lt;/strong&gt; &amp;nbsp;&lt;kbd&gt;Switzerland&lt;/kbd&gt; &amp;nbsp; ⭐⭐ &lt;br /&gt;&lt;mark&gt;5.05/100cl&lt;/mark&gt; – 0.0% – &lt;a href=&quot;https://www.coop.ch/en/food/drinks/beer/alcohol-free-beer/brewery-schuetzengarten-alcohol-free-ip-beer-6x33cl/p/6639258&quot;&gt;Coop&lt;/a&gt;&lt;br /&gt;&lt;small&gt;Very bitter taste with a whole lot of acidity, a bit stronger than usual IPAs.&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;small class=&quot;text-muted&quot;&gt;All prices in CHF, based on a ⭐⭐⭐⭐⭐ star rating.&lt;/small&gt;&lt;/p&gt;&lt;p&gt;Thanks for a fantastic beer testing weekend V 😊.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Summer break]]></title><description><![CDATA[A short note before the August break. No new article this month, so instead here are five earlier posts on my homelab, dotfiles, GitHub profile and apps.]]></description><link>https://angelo.dini.dev/blog/summer-break/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/summer-break/</guid><pubDate>Tue, 31 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;summer-break&quot;&gt;Summer break&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;It's been a busy year so far. Work and school continue from home as the Coronavirus ravages, and there is no end in sight.&lt;/p&gt;&lt;p&gt;So I’ve decided to use August to relax, go on some holidays and write another article again in September. In the meantime, have a look at the following articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/homeoffice-games/&quot;&gt;HomeOffice Games&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;My Homelab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/dotfiles/&quot;&gt;Automation using dotfiles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/github-readme/&quot;&gt;Improve your profile on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/necessary-apps/&quot;&gt;Apps I can’t live without&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Configuring Plex]]></title><description><![CDATA[Installing Plex on an Ubuntu server VM with a static IP, unattended updates, GPU passthrough for transcoding, NFS mounts and a migration from an old instance.]]></description><link>https://angelo.dini.dev/blog/homelab-plex/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-plex/</guid><pubDate>Sat, 17 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;plex-marks-the-spot-for-free-streaming&quot;&gt;“Plex marks the spot for free streaming”&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;before-we-begin&quot;&gt;Before we begin&lt;/h3&gt;
&lt;p&gt;The following chapters will be focusing on tutorials with instructions on how to set up the respective services.&lt;/p&gt;
&lt;p&gt;All services run on Virtual Machines using &lt;strong&gt;Ubuntu server 20.04 LTS&lt;/strong&gt;. A section featuring the specific setup instructions is available &lt;a href=&quot;#extra-setting-up-ubuntu&quot;&gt;at the end&lt;/a&gt; of this article.&lt;/p&gt;
&lt;h3 id=&quot;installing-plex&quot;&gt;Installing Plex&lt;/h3&gt;
&lt;p&gt;With your Ubuntu server ready, head over to the Plex website and choose the correct installation package from the &lt;a href=&quot;https://www.plex.tv/media-server-downloads/&quot;&gt;downloads page&lt;/a&gt;. Copy the link and replace it with the &lt;code&gt;{download-link}&lt;/code&gt; placeholder and run the commands in sequence:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;download-link&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; beignet-opencl-icd
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ocl-icd-libopencl1
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; dpkg &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;download-package&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; dpkg &lt;span class=&quot;token parameter variable&quot;&gt;-L&lt;/span&gt; plexmediaserver
&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;download-package&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to replace &lt;code&gt;{download-package}&lt;/code&gt; with the respective package name 😎.&lt;/p&gt;
&lt;p&gt;Monitor the prompt output for additional warnings or errors after running the commands. There may be subsequent packages you need to install.&lt;/p&gt;
&lt;p&gt;Next, check the status of the Plex server and stop it for now by executing:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl status plexmediaserver
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl stop plexmediaserver
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl disable plexmediaserver&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Following, enable the Plex repository by uncommenting the content in &lt;code&gt;/etc/apt/sources.list.d/plexmediaserver.list&lt;/code&gt;. Save the file and run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-q&lt;/span&gt; https://downloads.plex.tv/plex-keys/PlexSign.key &lt;span class=&quot;token parameter variable&quot;&gt;-O&lt;/span&gt; - &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; apt-key &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; -
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; upgrade
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start plexmediaserver&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the server, and you are ready to start configuring Plex. The administration interface is reachable through &lt;strong&gt;x.x.x.x:32400/web&lt;/strong&gt;. You can get the full address by running &lt;code&gt;netstat -lnpt | grep Plex&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;configuring-plex&quot;&gt;Configuring Plex&lt;/h3&gt;
&lt;p&gt;If you want to migrate an existing Plex instance, stop here and read &lt;a href=&quot;#extra-migrating-an-existing-plex-instance&quot;&gt;Migrating an existing Plex instance&lt;/a&gt; first. If this is not the case, continue with the configuration.&lt;/p&gt;
&lt;p&gt;The configuration itself is pretty straightforward. Login to your account, give your server a name and add your shares. There is also a section available that demonstrates how to &lt;a href=&quot;#extra-setting-up-mounts&quot;&gt;set up the mounts&lt;/a&gt; on Ubuntu.&lt;/p&gt;
&lt;h4 id=&quot;using-a-static-ip-address&quot;&gt;Using a static IP address&lt;/h4&gt;
&lt;p&gt;This step is optional, but I prefer to set a static IP address for my VMs. Alternatively, add your instance hostnames to your DNS configuration to reach them locally.&lt;/p&gt;
&lt;p&gt;To get started, figure out the network identifier that connects to your network by running &lt;code&gt;ip a&lt;/code&gt;. In my case, it is &lt;em&gt;ens224&lt;/em&gt;. Then create &lt;code&gt;/etc/netplan/01-netcfg.yaml&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ethernets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;ens224&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;dhcp4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; no
      &lt;span class=&quot;token key atrule&quot;&gt;addresses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 192.168.1.200/24
      &lt;span class=&quot;token key atrule&quot;&gt;gateway4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 192.168.1.1
      &lt;span class=&quot;token key atrule&quot;&gt;nameservers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;addresses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;192.168.1.1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 8.8.8.8&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1.1.1.1&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may need to replace &lt;strong&gt;ens224&lt;/strong&gt; with your network identifier and &lt;strong&gt;192.168.1.200/24&lt;/strong&gt; with the desired IP address. You may also have a different gateway and nameservers. To apply the settings, run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; netplan apply
&lt;span class=&quot;token function&quot;&gt;ip&lt;/span&gt; addr show dev ens224&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;enabling-auto-updates-for-ubuntu&quot;&gt;Enabling auto-updates for Ubuntu&lt;/h4&gt;
&lt;p&gt;I prefer Ubuntu to be updated at all times. If something breaks, I’d rather revert to a snapshot than sit on security issues. To get started, run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; unattended-upgrades
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;vim&lt;/span&gt; /etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the editor uncomment the following lines:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${distro_id}&lt;/span&gt;:&lt;span class=&quot;token variable&quot;&gt;${distro_codename}&lt;/span&gt;-updates&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
Unattended-Upgrade::Remove-Unused-Kernel-Packages &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
Unattended-Upgrade::Remove-New-Unused-Dependencies &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
Unattended-Upgrade::Remove-Unused-Dependencies &lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next edit &lt;code&gt;/etc/apt/apt.conf.d/20auto-upgrades&lt;/code&gt; and replace the content with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;APT::Periodic::Update-Package-Lists &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
APT::Periodic::Unattended-Upgrade &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
APT::Periodic::Download-Upgradeable-Packages &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
APT::Periodic::AutocleanInterval &lt;span class=&quot;token string&quot;&gt;&quot;7&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can test the changes by running &lt;code&gt;sudo unattended-upgrades --dry-run --debug&lt;/code&gt; and checking the logs in &lt;code&gt;cat /var/log/unattended-upgrades/unattended-upgrades.log&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;enabling-auto-updates-for-plex&quot;&gt;Enabling auto-updates for Plex&lt;/h4&gt;
&lt;p&gt;Plex needs some special care to update automatically. Luckily there is a great script available on &lt;a href=&quot;https://github.com/mrworf/plexupdate&quot;&gt;github.com/mrworf/plexupdate&lt;/a&gt;. Follow the instructions in the &lt;code&gt;README.md&lt;/code&gt;, and the subsequent terminal prompts.&lt;/p&gt;
&lt;h4 id=&quot;installing-nvidia-drivers&quot;&gt;Installing NVIDIA drivers&lt;/h4&gt;
&lt;p&gt;GPU passthrough is a bit of a hassle. Using a consumer GeForce GTX 1050 Ti doesn’t make it easier. However, I got it to work on my setup by using the following steps:&lt;/p&gt;
&lt;p&gt;Make sure ESXi supports passthrough by adding:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;hypervisor.cpuid.v0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; FALSE
pciPassthru0.msiEnabled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; FALSE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to the VM configuration. Next, installing the drivers:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ubuntu-drivers-common
ubuntu-drivers devices
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ubuntu-drivers autoinstall&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need to confirm that &lt;strong&gt;hevc_nvenc&lt;/strong&gt; is working by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; remove ffmpeg
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; snap &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ffmpeg
ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-hwaccels&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the last command should output something like:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Hardware acceleration methods:
vdpau
cuda     &amp;lt;- this is important
vaapi
drm
opencl&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, we need to make sure that the system doesn’t boot in UI mode by running:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl set-default multi-user.target
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;shutdown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; now&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a name=&quot;extra-migrating-an-existing-plex-instance&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;extra-migrating-an-existing-plex-instance&quot;&gt;Extra: Migrating an existing Plex instance&lt;/h3&gt;
&lt;p&gt;I had an existing Plex instance using a Mac Mini and needed to migrate it to the new Ubuntu server. This section is based on &lt;a href=&quot;https://support.plex.tv/articles/201370363-move-an-install-to-another-system/&quot;&gt;Moving an Install to Another System&lt;/a&gt;. In short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the old Plex instance, disable the &lt;strong&gt;Empty trash automatically after every scan&lt;/strong&gt; preference for the server.&lt;/li&gt;
&lt;li&gt;Do not configure anything on the new Plex instance yet. To be safe, sign out and stop the server by running &lt;code&gt;sudo systemctl stop plexmediaserver&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ideally, do the same for the old server. I had a running live system that I didn’t want to shut down during the process.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that everything is prepared copy the old instance’s data to the new server by using &lt;strong&gt;rsync&lt;/strong&gt;. Rsync allows you to run the command multiple times even if there are network issues or running the old instance side-by-side.&lt;/p&gt;
&lt;p&gt;Make sure both servers have rsync installed by executing &lt;code&gt;sudo apt install rsync&lt;/code&gt;. Add the following script to the server of the &lt;strong&gt;new Plex instance&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rsync -avz --delete --exclude 'Cache' user@x.x.x.x:/var/lib/plexmediaserver/ /var/lib/plexmediaserver/
sudo chown -R plex:plex /var/lib/plexmediaserver
sudo chmod -R 755 /var/lib/plexmediaserver&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; ~/plexmigrate.sh
&lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; +x ~/plexmigrate.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;strong&gt;&lt;a href=&quot;mailto:user@x.x.x.x&quot;&gt;user@x.x.x.x&lt;/a&gt;&lt;/strong&gt; with the user/IP of your &lt;strong&gt;old Plex instance&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Finally, you can run the command using &lt;code&gt;sudo ~/plexmigrate.sh&lt;/code&gt; and start the migration. Once finished, make sure to turn off the old Plex instance permanently and start the new one:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# on the old server&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl stop plexmediaserver
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl disable plexmediaserver

&lt;span class=&quot;token comment&quot;&gt;# on the new server&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl start plexmediaserver
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; plexmediaserver&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a name=&quot;extra-setting-up-mounts&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;extra-setting-up-mounts&quot;&gt;Extra: Setting up mounts&lt;/h3&gt;
&lt;p&gt;QNAP supports several protocols though I chose to use NFS as it worked best for my use case.&lt;/p&gt;
&lt;p&gt;First, local folders need to be created to mount the remote ones into. In my case, I used &lt;code&gt;/media/&lt;/code&gt; and added the subsequent sub-folders inside it.&lt;/p&gt;
&lt;p&gt;Next, install the requirements into Ubuntu:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; network-manager
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; nfs-common&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can check if a connection can be established like:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mount&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; nfs x.x.x.x:/server/folder /media/photos&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and replace x.x.x.x with the IP address of the storage server. If all works prepare the &lt;code&gt;/etc/fstab&lt;/code&gt; file as follows:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;x.x.x.x:/server/folder1 /media/folder1 nfs _netdev,auto,x-systemd.automount &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
x.x.x.x:/server/folder2 /media/folder2 nfs _netdev,auto,x-systemd.automount &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
x.x.x.x:/server/folder3 /media/folder3 nfs _netdev,auto,x-systemd.automount &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace x.x.x.x again with the IP address of the storage server and map the respective folders to their counterpart on Ubuntu. The additional options ensure that the folders get automatically mounted after a restart and wait for the server to be available.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;extra-setting-up-ubuntu&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;extra-setting-up-ubuntu&quot;&gt;Extra: Setting up Ubuntu&lt;/h3&gt;
&lt;p&gt;Get the latest version of &lt;strong&gt;Ubuntu server&lt;/strong&gt; and follow the general installation steps. You may want to get the current IP through &lt;code&gt;ip a&lt;/code&gt; and ssh into your Ubuntu instance. After that, run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; upgrade
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; full-upgrade
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; net-tools&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply these steps for any other service you want to install.&lt;/p&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;We now have instructions for setting up Ubuntu in general and our first VM running Plex. In the next chapter, I’ll be setting up the DVR to manage content.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Networking with Ubiquiti]]></title><description><![CDATA[Replacing my Turris Omnia with Ubiquiti hardware, wiring a 10-gigabit link between the storage and virtualisation servers, and measuring what it delivers.]]></description><link>https://angelo.dini.dev/blog/homelab-networking/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-networking/</guid><pubDate>Tue, 29 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;upgrading-to-a-10gbe-network&quot;&gt;Upgrading to a 10GbE Network&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;choosing-the-hardware&quot;&gt;Choosing the hardware&lt;/h3&gt;
&lt;p&gt;Who hasn’t struggled with their network setup at some point? From failed routing to dropped WiFi connections, networking done right is complex. I hope I finally learn how to set up everything correctly with proper monitoring to debug where the 🤬 packet drops come from at school…&lt;/p&gt;
&lt;p&gt;I started with the typical vendor-specific routers with an included WiFi module. The standard setup everyone gets when ordering an internet product from UPC Cablecom, Swisscom and the sorts.&lt;/p&gt;
&lt;p&gt;However, once fibre became available at my place, I immediately switched to Fiber7. It was not just cheaper but offered better uptime at higher bandwidth and excellent support. Though not everything was a 🌈. At the same time, my hate-love relationship started with the new &lt;strong&gt;Turris Omnia&lt;/strong&gt; router. Open hardware is incredible, but that piece of 💩 had one of the worst WiFi modules installed I’ve ever come across. It just didn’t work most of the time. It did everything else flawlessly.&lt;/p&gt;
&lt;p&gt;That was the moment where I decided to look for an alternative setup. I looked at AmpliFi, Linksys, TP-Link and the sorts, settling on &lt;strong&gt;Ubiquiti&lt;/strong&gt;. Why? Cause it’s simple, affordable, provides a unified experience, an extendable mesh network, and it just works.&lt;/p&gt;
&lt;h3 id=&quot;the-stack&quot;&gt;The stack&lt;/h3&gt;
&lt;p&gt;The first components I bought were a UniFi nanoHD together with a Cloud Key. The UniFi nanoHD and Cloud Key directly connected to my Turris Omnia. I also purchased a Unifi PoE 8-Port Switch to allow all my other LAN capable devices to connect to my router (as I ran out of ports). So far, I was pleasantly surprised by how smoothly everything worked.&lt;/p&gt;
&lt;p&gt;I still got packet loss here and there due to my Turris Omnia. I decided to replace the remaining infrastructure entirely with Ubiquiti hardware once I started my server setup, ending up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1x UniFi nanoHD&lt;/li&gt;
&lt;li&gt;1x UniFi Switch PoE 8 (150W)&lt;/li&gt;
&lt;li&gt;1x UniFi Switch 48&lt;/li&gt;
&lt;li&gt;1x UniFi Switch XG 16 (10-gigabit networking)&lt;/li&gt;
&lt;li&gt;1x UniFi Security Gateway Pro 4 (new router)&lt;/li&gt;
&lt;li&gt;1x UniFi Cloud Key Gen2 with Rack Mount (upgrade)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The nanoHD and 8-Port PoE switch were the only components I bought brand new. I acquired the remaining devices either through Tutti or Ricardo.&lt;/p&gt;
&lt;p&gt;The 48-port switch is slightly overkilled but ended up cheaper than the 24-port candidates &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The setup&lt;/h3&gt;
&lt;p&gt;Now my fibre connection goes directly via SFP into the Security Gateway Pro. The second SFP connection is used as an uplink to the XG 16, which in turn has another SFP connection to the 48-port switch.&lt;/p&gt;
&lt;p&gt;The XG 16 has two 10-gigabit connections to the storage server and another two 10-gigabit connections to the virtualisation server. That connectivity allows both to communicate super fast with each other. The additional NICs connect each via a 1-gigabit connection to the 48 port switch. The different links are mainly for custom VLAN configuration through VMware ESXi for some of the virtual machines.&lt;/p&gt;
&lt;p&gt;I also plan to play around with custom VLANs from the Ubiquiti Administration Interface further to separate my internal LAN from the WAN where necessary.&lt;/p&gt;
&lt;p&gt;There are also additional cables connected to the 48-port switch for general management, monitoring, power failure, etc.&lt;/p&gt;
&lt;h3 id=&quot;the-bottlenecks&quot;&gt;The bottlenecks&lt;/h3&gt;
&lt;p&gt;One significant drawback in my whole setup is the connection from my server in the cellar to the network in the apartment.&lt;/p&gt;
&lt;p&gt;I use two &lt;strong&gt;Zyxel PLA5456&lt;/strong&gt; Powerline connectors as there is no direct LAN cable. Even though they advertise as 1800MBit/s, the reality is around 1000MBit/s. Which is fine for my needs. Though I’m looking forward to proper cabling in the future 🥳&lt;/p&gt;
&lt;h3 id=&quot;checking-the-speeds&quot;&gt;Checking the speeds&lt;/h3&gt;
&lt;p&gt;Performance tests were next in line. I tested the following scenarios, the “Client” being a computer inside the apartment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storage to virtualisation server and vice-versa&lt;/li&gt;
&lt;li&gt;Client to the storage/virtualisation server and vice-versa&lt;/li&gt;
&lt;li&gt;Client to another Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I did that using iperf3 and speedtest.net. Here a rough outline of the procedure:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; iperf3
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; speedtest-cli&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start iperf3 on the target server and run the speed-tests on your source device:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# target server (e.g. 192.168.1.10)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; iperf3
iperf3 &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8088&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# source device&lt;/span&gt;
speedtest-cli
iperf3 &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;192.168&lt;/span&gt;.1.10 &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8088&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;With the network ideally set up, I’m ready to set up the software for managing my digital assets. We’ll cover this in the next section.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[HomeOffice games]]></title><description><![CDATA[Browser games to play with your co-workers while working from home. Drawing games, online escape rooms, social deduction titles and cooperative word games.]]></description><link>https://angelo.dini.dev/blog/homeoffice-games/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homeoffice-games/</guid><pubDate>Mon, 31 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;lets-play&quot;&gt;Let’s Play!&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;HomeOffice wasn't that uncommon in the IT industry before but became more apparent during the Corona pandemic. Distributed teams are growing, and so the lack of personal contact.&lt;/p&gt;&lt;p&gt;It has become difficult to grab a 🍺 with your co-workers. We are a distributed team at Divio spread across the world. Team events are costly and rare; getting everyone together in one place is difficult.&lt;/p&gt;
&lt;p&gt;Of course, several team-building solutions sprang up in the last couple of months to provide a creative solution. The following consists of activities that have been tested through either Divio or with friends. All games are played inside a browser without installing any additional software; thus, it is convenient to start the game.&lt;/p&gt;
&lt;h3 id=&quot;drawing-games&quot;&gt;Drawing games&lt;/h3&gt;
&lt;p&gt;In the classical sense, they are charade-inspired word-guessing games. The objective is to draw a word that someone receives while the other participants try to guess it. There are multiple variations and arrangements.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Skribbl&lt;/strong&gt;, a group meets inside a drawing game where someone receives a word, draws it, and the others try to figure it out. The guess is submitted in a chat window, and points are distributed by how fast the drawing is guessed. The other participants won’t see the solution till the end of the round. The chat window shows if someone figured it out without displaying the solution.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://skribbl.io/&quot;&gt;skribbl.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sketchful&lt;/strong&gt; and &lt;strong&gt;Gartic&lt;/strong&gt; are similar but add some customisations, different lobbies and leaderboards.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sketchful.io/&quot;&gt;sketchful.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gartic.io/&quot;&gt;gartic.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Gartic Phone&lt;/strong&gt; has a different but fun concept. It follows the idea of “telephone”, where a group of players form a line or circle. The first player comes up with a message and whispers it to the next person’s ear. That process continues until the word reaches the end of the line. The term is then compared to its original form.&lt;/p&gt;
&lt;p&gt;In Gartic Phone everyone chooses a word and writes it down. That word is then passed to the next in line. The twist is that the word has to be drawn next. The drawing is then given to the next person, who has to guess the initial phrase. This process repeats until the initially defined word has reached the end of the cycle. Afterwards, the history is displayed in a chat-like tale.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://garticphone.com/&quot;&gt;garticphone.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;escape-rooms&quot;&gt;Escape rooms&lt;/h3&gt;
&lt;p&gt;An escape room is a game in which a team of players is locked in, searches for clues, solves puzzles, and accomplishes tasks to escape from the room. There are multiple versions available that follow the same flow but online:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bern.adventurerooms.ch/&quot;&gt;AdventureRooms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.escapelive.co.uk/online-escape-rooms/&quot;&gt;EscapeLive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://thepanicroom.net/online-escape-rooms&quot;&gt;The Panic Room&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a unique variation I would like to mention: &lt;strong&gt;CodinGame&lt;/strong&gt;. It advertises itself as the very first escape game for coders and non-coders and is pretty fun for development teams and companies.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://escape.codingame.com/&quot;&gt;CodinGame&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another notable variation is &lt;strong&gt;Keep Talking and Nobody Explodes&lt;/strong&gt;. Everyone meets somewhere online, and one starts the bomb. The others don’t see the bomb but have a manual to diffuse it. The one with the bomb has to describe the randomly generated components, while the others have to find the proper process to disarm them. If the time runs out, the team loses 💥.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://keeptalkinggame.com/&quot;&gt;Keep Talking and Nobody Explodes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;social-deduction-games&quot;&gt;Social deduction games&lt;/h3&gt;
&lt;p&gt;These are games in which players attempt to uncover each other’s hidden role or allegiances. Commonly, they are played in teams with larger groups, while one side is considered “good” and the other “bad”.&lt;/p&gt;
&lt;p&gt;One of those is &lt;strong&gt;One Night Ultimate Werewolf&lt;/strong&gt;, the browser version of the tabletop classic. It is played with 3 to 10 people and, as the name promises, it lasts a single night. Each player gets a role with an agenda. The roles entail various effects, but in essence, there are two categories. As a villager, you try to find the werewolves among you, and as a werewolf, you hunt down the villagers while convincing the others that you are innocent. The concept is simple: everyone closes their eyes, the roles act in turn, and in the morning the table votes on who to accuse. Nobody gets eliminated, so a round takes about ten minutes and the whole team argues for all of them. That last part is the actual game.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://netgames.io/games/onu-werewolf&quot;&gt;Ultimate Werewolf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another worthy mention is &lt;strong&gt;Among Us&lt;/strong&gt;. An exception to my initially defined rule, you have to install an app. However, it is straightforward, available on all platforms and super easy to get started with a novice team. You are either a regular “crew member” doing tasks in a spaceship or the “impostor” who kills the crew members off one by one. The crew aims to figure out who the impostor is before they kill the entire team. The communication happens either after finding a dead body on the floor or by pressing an emergency button.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://innersloth.com/gameAmongUs.php&quot;&gt;Among Us&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cooperative-games&quot;&gt;Cooperative games&lt;/h3&gt;
&lt;p&gt;Cooperative games are designed to test the teams’ ability with memorisation/language or to explore their properties. They are typically designed to work together or in separate groups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Codenames&lt;/strong&gt; and &lt;strong&gt;Codewords&lt;/strong&gt; are great examples of word guessing cooperative games. Rival Codebreakers race to identify which of the 25 Codewords/Codenames are their own. They do this by listening to their Codemasters, who take turns giving one-word clues. The Codebreakers try to guess which words their Codemaster meant, one at a time. If they guess correctly, they may continue guessing until they either run out of ideas for the given clue or get a Codeword wrong. Then it is the other team’s turn to give a hint and guess. The first team to reveal all their Codewords wins the game.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://codenames.game/&quot;&gt;Codenames&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://netgames.io/games/codewords/&quot;&gt;Codewords&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another fantastic cooperative game is &lt;strong&gt;Hanabi&lt;/strong&gt;. Instead of guessing words, team members work together to identify their cards. Like “Indian Poker”, players can see each other’s cards but cannot see their own. Information or fuse tokens provide information to the players on what colours or numbers they are holding. The goal is to create a “hanabi” (firework) by placing all cards in order and by colour on the table.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.boardgamearena.com/gamepanel?game=hanabi&quot;&gt;Hanabi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;other-worthy-mentions&quot;&gt;Other worthy mentions&lt;/h3&gt;
&lt;p&gt;There are other games on my list to try out I would like to share:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mousewars.io/&quot;&gt;mousewars.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://teambuilding.com/activity/originals/war-of-the-wizards&quot;&gt;War of the Wizards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://netgames.io/games/&quot;&gt;netgames.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jeopardylabs.com/&quot;&gt;jeopardylabs.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I hope you have a fantastic time playing some of these games with your co-workers. If you have any other recommendations, let me know.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Storage - QNAP vs. Synology]]></title><description><![CDATA[Adding storage to my homelab. This is critical to host and organise my media. I also extend the hardware to make reads and writes even faster with caching.]]></description><link>https://angelo.dini.dev/blog/homelab-storage/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-storage/</guid><pubDate>Fri, 30 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;getting-over-70-tb-of-storage&quot;&gt;Getting over 70 TB of storage&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;choice-of-storage&quot;&gt;Choice of storage&lt;/h3&gt;
&lt;p&gt;I was pleased with my Synology DS1815+ as my primary &lt;strong&gt;storage solution&lt;/strong&gt; for years. It handled the automatic download, categorisation and storage of all my digital assets. It was reliable, and I didn’t have any hardware or disk failure so far. So why did I switch to a QNAP?&lt;/p&gt;
&lt;p&gt;The goal was to get a 2U 12-Bay NAS for under 1’000 CHF, including all upgrades, without the storage drives. Thus, it didn’t need to be the newest model. Additionally, I wanted upgradability for additional bays, expandable SSD cache, more memory or better CPUs for the future. Sadly, most vendors are restricted by one or the other technology. However, Synology lagged significantly behind when comparing their models to QNAP as the performance to price ratio seemed to be much better with QNAP’s hardware.&lt;/p&gt;
&lt;p&gt;After some further research, I found a QNAP TVS-1271U-RP for a relatively affordable price. It had a stock Dual-core Intel® Core™ i3-4150 CPU with 8 GB memory. I promptly ordered a Quad-core Intel® Core™ i7-4790S CPU and upgraded the RAM to 32 GB. I also ordered two cheap GM512GB mSATA drives that will provide the SSD cache and an M.2 NVMe SSD Express card to add my spare 1TB Crucial SSD as an additional resource for virtual machines. On top of that, I also added a 10 Gbps NIC 😁, ending up with the following specs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel® Core™ i7-4790S (4 Cores up to 4.00 GHz)&lt;/li&gt;
&lt;li&gt;32 GB DDR3L 1600 MHz Memory (4x 8GB modules)&lt;/li&gt;
&lt;li&gt;12 bay hard drive slots&lt;/li&gt;
&lt;li&gt;2x 512 GB mSATA SSD&lt;/li&gt;
&lt;li&gt;1x M.2 NVMe SSD express card with 1TB Crucial SSD&lt;/li&gt;
&lt;li&gt;1 Gbps integrated quad-port NIC&lt;/li&gt;
&lt;li&gt;10 Gbps X540 dual-port PCIe NIC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All that together kept me under the 1k range, by like a beer or two. Thanks to some cheap offerings from Tutti and the fantastic prices from AliExpress.&lt;/p&gt;
&lt;h3 id=&quot;choosing-the-drives&quot;&gt;Choosing the drives&lt;/h3&gt;
&lt;p&gt;I used Western Digital Red for years, until &lt;a href=&quot;https://blog.westerndigital.com/wd-red-nas-drives/&quot;&gt;the 2020 scandal&lt;/a&gt; around WD shipping SMR drives without labelling them as such. I got two 6 TB SMR drives to fill up my Synology from 6 to 8 drives just a couple of years ago, and ever since, the RAID consistency checks take forever to complete. Digitec support did a better job offering me replacements than the WD support, even if they didn’t have to. Kudos to their support! 🥳&lt;/p&gt;
&lt;p&gt;These issues led me to switch to 12 x 8 TB Seagate IronWolf (CMR) NAS drives. I didn’t want to save costs there as the data was essential to me. So I waited for a reasonable offer to come along and bought them new at around 180 bucks (the original price was 260 bucks a piece back then).&lt;/p&gt;
&lt;h3 id=&quot;setting-up-the-nas&quot;&gt;Setting up the NAS&lt;/h3&gt;
&lt;p&gt;The QNAP interface was similar to Synology’s “DiskStation Manager” (DSM). QNAP calls theirs “Turbo NAS Operating System” (QTS). Both operating systems offer similar features. The notable difference is that DSM tries to simplify processes and provide fewer options, while QTS allows for basically any modification leading to a cluttered UI. If you are an IT specialist, QTS is for you. Otherwise, go with DSM (and Synology, for that matter).&lt;/p&gt;
&lt;p&gt;The setup wasn’t that hard though, choose a user, tweak some settings and be done with it.&lt;/p&gt;
&lt;h3 id=&quot;resolving-some-issues&quot;&gt;Resolving some issues&lt;/h3&gt;
&lt;p&gt;Though sadly, there was an issue. Every now and then, I had to restart the NAS multiple times to recognise the discs. Sometimes only 8 LEDs were displayed instead of all 12, meaning that four brand new drives went missing and the RAID failed.&lt;/p&gt;
&lt;p&gt;QNAP’s support was excellent and responsive. They quickly discovered that I was running on a BIOS version with a disk problem known to the TVS-1271U-RP.&lt;/p&gt;
&lt;p&gt;So I followed the provided information to upgrade the BIOS version to the latest, and everything was back in order.&lt;/p&gt;
&lt;p&gt;Finally, I updated the firmware and operating system to the latest versions as well.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-the-storage&quot;&gt;Setting up the storage&lt;/h3&gt;
&lt;p&gt;Now I had a working NAS without any issues and upgraded everything to the latest and greatest. Next was to set up the 12 drives, the cache, and the additional M.2 storage.&lt;/p&gt;
&lt;p&gt;My storage pool consists of 12 NAS drives. I chose to go with RAID 6 and its two-disk failure tolerance. That gives me ample space and some breathing room. I have two volumes set up, one that holds my data and another one for surveillance recordings.&lt;/p&gt;
&lt;p&gt;I also set up cache acceleration using the two 512 GB mSATA SSDs. It speeds up data transfer for the most requested files, which is nice. I never used cache acceleration, so I wanted to play around with it and see if it delivers real benefits. So far, it didn’t disappoint.&lt;/p&gt;
&lt;p&gt;The M.2 drive is set up as an additional storage pool to host VMs or VM backups. I don’t use it yet, but it’s reserved for projects in a later stage.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-backups&quot;&gt;Setting up backups&lt;/h3&gt;
&lt;p&gt;As most know, RAID is not a backup solution. If more than two disks fail, all data is gone. Even if just one disk fails and gets replaced, the rebuild can cause a cascading failure. To remedy that, I added two 4TB USB-Drives to the NAS for “not so important data but still important enough to be saved somewhere else” and a remote backup solution using Backblaze for my most treasured data.&lt;/p&gt;
&lt;p&gt;QNAP offers an extensive interface to set up backups using “HBS 3 Hybrid Backup Sync”.&lt;/p&gt;
&lt;h3 id=&quot;accessing-the-volumes&quot;&gt;Accessing the volumes&lt;/h3&gt;
&lt;p&gt;After all that, the only thing left was tweaking some settings and figuring out how to connect to the drives.&lt;/p&gt;
&lt;p&gt;Also, that was pretty simple. I headed over to “Control Panel” &amp;gt; “Network &amp;amp; File Services” and enabled “Apple Networking” and “NFS Services”. The latter will be used later for the VMs.&lt;/p&gt;
&lt;p&gt;After that, I can connect to the drives through finder with specific users set up for network access.&lt;/p&gt;
&lt;p&gt;I transferred the data on my Synology NAS via &lt;code&gt;rsync&lt;/code&gt; to a designated folder and then rearranged everything to match my taste.&lt;/p&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;Now, I have a running QNAP TVS-1271U-RP with the latest firmware and OS. All my data is migrated to the new NAS, and everything was working, fast and reliable.&lt;/p&gt;
&lt;p&gt;The following section will deal with my networking setup using Ubiquiti hardware to connect all the data. And yeah, there are some bottlenecks 😭.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Virtualisation with ESXi]]></title><description><![CDATA[Adding virtualisation to my homelab. Picking a Dell PowerEdge R820, updating its firmware, sorting out the RAID and getting VMware ESXi and vSphere running.]]></description><link>https://angelo.dini.dev/blog/homelab-virtualisation/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-virtualisation/</guid><pubDate>Sat, 27 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;configure-and-setup-esxi-and-vsphere&quot;&gt;Configure and setup ESXi and vSphere&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;choice-of-server&quot;&gt;Choice of server&lt;/h3&gt;
&lt;p&gt;I used a Mac mini as my &lt;strong&gt;media server&lt;/strong&gt; and a Synology DS1815+ as my &lt;strong&gt;media manager&lt;/strong&gt; previously. The goal was to consolidate both systems into a mighty server using virtualisation to host each service.&lt;/p&gt;
&lt;p&gt;At first, I wanted to build the server myself using an Intertech 2U 20255 chassis. The components would have cost me over 1200.- CHF for a decent system that could host about 4-5 demanding virtual machines.&lt;/p&gt;
&lt;p&gt;Taking that as a baseline, I scoured Ricardo and Tutti for some Dell PowerEdge or HP ProLiant systems, as they appeared to have a good price/performance ratio overall.&lt;/p&gt;
&lt;p&gt;I ultimately settled on a baseline PowerEdge R820 for about 900 bucks and upgraded it through AliExpress for an additional 500.- CHF, ending up with the following specs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4x Intel® Xeon® Processor E5-4650 (8 Cores up to 3.30 GHz)&lt;/li&gt;
&lt;li&gt;768 GB DDR3 1333 MHz ECC Memory (32 GB modules)&lt;/li&gt;
&lt;li&gt;4x 300 GB SATA and 4x 900 GB SATA drives&lt;/li&gt;
&lt;li&gt;1 TB M.2 Kingston A2000 SSD with PCIe adapter card&lt;/li&gt;
&lt;li&gt;PERC H710 raid card&lt;/li&gt;
&lt;li&gt;1 Gbps integrated quad-port NIC&lt;/li&gt;
&lt;li&gt;1 Gbps I350-T4V2 quad-port PCIe NIC&lt;/li&gt;
&lt;li&gt;10 Gbps X540 dual-port PCIe NIC&lt;/li&gt;
&lt;li&gt;2x Platinum+ 1100 Watt power supplies&lt;/li&gt;
&lt;li&gt;MSI GeForce GTX 1050 Ti OC&lt;/li&gt;
&lt;li&gt;Internal Dual SD Module (2x Sandisk Ultra U1 16GB)&lt;/li&gt;
&lt;li&gt;iDRAC Enterprise&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not too bad for a total of around 1’500 bucks 🤪&lt;/p&gt;
&lt;h3 id=&quot;discovering-the-basics&quot;&gt;Discovering the basics&lt;/h3&gt;
&lt;p&gt;I had no experience with server hardware before. So getting access to the management interface appeared to be challenging already. The server turned on when pressing the power button, which was good, yes? After getting a VGA cable, mouse, keyboard and monitor, I was ready to dig in.&lt;/p&gt;
&lt;p&gt;The server took some time to boot up, finally showing a colourful VMware ESXi “ready” screen. “Nice, whatever this is”, were my first thoughts. There was a yellow-black screen with an IP shown; let’s open it! Ah! That’s a virtualisation UI, excellent.&lt;/p&gt;
&lt;p&gt;The first hours of experimenting led me to some insights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The booting sequence takes some time, as the server needs to “rediscover” all the hardware and regenerate its “Inventory”.&lt;/li&gt;
&lt;li&gt;On boot, I could start the “Dell Lifecycle Controller” which is “an advanced embedded systems management technology… blah”. In short, it’s an advanced UEFI/BIOS interface that allows configuring or “deployment” of the server. We’ll get back to that one later.&lt;/li&gt;
&lt;li&gt;VMware ESXi is an excellent virtualisation tool to manage virtual machines. The web interface looked decent, and naturally, I broke it after some time 😅&lt;/li&gt;
&lt;li&gt;There is a dedicated iDRAC (Integrated Dell Remote Access Controller), which provides access to the server’s admin interface through the browser. The IP address is static and was visible after the boot. There was even a Virtual Console so I could ditch the extra monitor and continue from my laptop.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;resolving-some-issues&quot;&gt;Resolving some issues&lt;/h3&gt;
&lt;p&gt;The server showed two issues when booting on the front display. The first indicated that the second power cable wasn’t connected, easy to solve.&lt;/p&gt;
&lt;p&gt;The latter was a bit more tricky. A message about “The storage BP1 SAS A cable is not connected” indicated that the raid controller did not correctly connect to the hard drives, but they were, and even the hard drives showed up. I first tried disconnecting both SAS cables and reconnecting them again, with no success. I finally solved it by changing the order of the wires (wtf).&lt;/p&gt;
&lt;p&gt;Always check that all connections are firm and the cables are in the proper order 🤓.&lt;/p&gt;
&lt;h3 id=&quot;updating-the-firmware&quot;&gt;Updating the firmware&lt;/h3&gt;
&lt;p&gt;As the iDRAC provided access to almost any functionality the server provided, I booted into the Dell Lifecycle Controller and upgraded the firmware versions for its hardware and components. There were some issues, and I had to manually download some of the drivers and install them by hand. The web interface helped as it provided a simple upload form with good error feedback.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-the-storage&quot;&gt;Setting up the storage&lt;/h3&gt;
&lt;p&gt;I used the iDRAC to recreate the RAID setup. I used raid six as the double-parity was more important to me than total space. I created two raid groups, “datastore_300” (name in ESXi) with the four 300 GB SAS drives and “datastore_900” with the four 900 GB SAS drives, plus “datastore_nvme” for the 1 TB M.2 SSD.&lt;/p&gt;
&lt;p&gt;The purpose is to use “datastore_300” to use for ISO storage or when moving VMs around, “datastore_900” as an archive/backup drive and “datastore_nvme” as the primary host for the VMs.&lt;/p&gt;
&lt;h3 id=&quot;installing-the-host-os&quot;&gt;Installing the Host OS&lt;/h3&gt;
&lt;p&gt;I looked at other virtualisation software such as &lt;a href=&quot;https://www.proxmox.com/en/proxmox-ve&quot;&gt;Proxmox VE&lt;/a&gt;, &lt;a href=&quot;https://www.microsoft.com/en-us/evalcenter/evaluate-hyper-v-server-2019&quot;&gt;Hyper-V&lt;/a&gt; or &lt;a href=&quot;https://www.citrix.com/products/citrix-hypervisor/&quot;&gt;Citrix Hypervisor&lt;/a&gt;, amongst many others. Though they all seemed fine, the combination of VMware ESXi and vSphere seemed pretty good to me from what I read. Plus, I also got a free enterprise licence while at school 🥳.&lt;/p&gt;
&lt;p&gt;It took me a while to figure out where the existing ESXi instance got installed. Dell provides a Dual SD Module where you can install the host system. Of course, you can also use the SAS drives, but &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;, seemed to be good enough. I set up the system to mirror both SD cards. That way, it would be easy to restore a failure, plus I back up the contents regularly.&lt;/p&gt;
&lt;p&gt;First, I installed a plain VMware ESXi, which showed some irregularities with missing drivers and weird behaviour. I later discovered that I need a &lt;a href=&quot;https://www.dell.com/support/kbdoc/en-th/000176963/dell-emc-customized-image-of-vmware-esxi-availability-and-download-instructions&quot;&gt;DELL EMC Customized Image of VMware ESXi&lt;/a&gt;. After that was installed, everything seemed fine.&lt;/p&gt;
&lt;h3 id=&quot;virtualisation-using-esxi&quot;&gt;Virtualisation using ESXi&lt;/h3&gt;
&lt;p&gt;The default installation served me already pretty well. I tinkered a bit with the Networking defaults and promptly set up my datastores afterwards. Everything seemed fine. Creating VMs worked, but I wasn’t able to move VMs around freely, which was odd.&lt;/p&gt;
&lt;p&gt;I discovered that I needed VMware vSphere for “advanced VM management”. The problem was that it was necessary to be installed as a separate VM on the ESXi host, which would later take over the ESXi host to manage it, weird or? 🤪&lt;/p&gt;
&lt;p&gt;Well, I downloaded vSphere, got some licences via my school account and set it up. The installation procedure is pretty straightforward. vSphere creates a separate VM on the ESXi host to provide an improved administrative interface. Once vSphere completed the setup, it took over the host’s control, and I was able to move VMs around. vSphere even provided updated functionalities. However, I didn’t understand its inner working and broke the installation several times. I settled to use it for creating, editing and moving only for now.&lt;/p&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;Now, I have set up my Dell PowerEdge R820 with the latest firmware, ESXi as Host OS with vSphere as managing director, proper storage and resolved all significant issues. My virtualisation beast was ready, and I moved to the next step, setting up the storage monster to hold all the data. We’ll cover this in the next section.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[My Homelab]]></title><description><![CDATA[I finally got around to revising my homelab setup. This time I went big with a server rack, a Dell PowerEdge R820, a QNAP NAS and Ubiquiti networking gear.]]></description><link>https://angelo.dini.dev/blog/homelab-introduction/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/homelab-introduction/</guid><pubDate>Sun, 28 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;I'm a data hoarder; that's nothing new to people who know me. This year around, I drove my passion a little bit further and streamlined my setup. The following articles will be a multi-part series introducing various areas such as:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-introduction/&quot;&gt;Introduction&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-virtualisation/&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt; using a PowerEdge R820&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-storage/&quot;&gt;Storage&lt;/a&gt;&lt;/strong&gt; with a QNAP TVS-1271U-RP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-networking/&quot;&gt;Networking&lt;/a&gt;&lt;/strong&gt; with Ubiquiti&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-plex/&quot;&gt;Media streaming&lt;/a&gt;&lt;/strong&gt; with Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-downloads/&quot;&gt;Download management&lt;/a&gt;&lt;/strong&gt; with SABnzbd and Transmission&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-services/&quot;&gt;Service management&lt;/a&gt;&lt;/strong&gt; with Sonarr, Radarr, Lidarr, Readarr, Bazarr and Mylar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-additional/&quot;&gt;Additional tools&lt;/a&gt;&lt;/strong&gt; like Calibre, Ubooquity, Ombi or Organizr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;/blog/homelab-monitoring/&quot;&gt;Monitoring&lt;/a&gt;&lt;/strong&gt; with Checkmk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-brief-history&quot;&gt;A brief history&lt;/h3&gt;
&lt;p&gt;I started off storing data on CDs and later DVDs back in the days (you know, before the Y2K problems 🤦🏻‍♂️). Network Attached Storage (NAS) solutions were still in their early stages, and hard drives expensive. At some point, I switched to external USB drives and created mini raids for the most critical data like digital documents or photos. Though I still held onto the mountains of DVDs piling up.&lt;/p&gt;
&lt;p&gt;About ten years ago, I decided to switch to a NAS setup and choose to build my own. It had about 1 TB of total storage, which was ample to hold all my data. I extended my hardware with a Mac mini in 2012 to create my first Homelab using Plex, Couch Potato, Sick Beard, Headphones and SABnzbd to manage my continuously growing library.&lt;/p&gt;
&lt;p&gt;In 2015 my NAS and Mac mini failed me due to a power surge, and I decided to migrate all of my data to a Synology DS1815+ with 12 TB of total storage. I switched from Couch Potato to Sonarr and later from Sick Beard to Radarr. Headphones were replaced by Lidarr sometime later.&lt;/p&gt;
&lt;h3 id=&quot;all-new-homelab&quot;&gt;All new homelab&lt;/h3&gt;
&lt;p&gt;I’m continuously looking for challenges as a developer and decided, at the end of 2020, to overhaul my setup completely this year. I started by looking for a powerful server to host virtual environments for my services, an expandable NAS solution for my data and a good rack to store all my hardware.&lt;/p&gt;
&lt;p&gt;I settled buying a &lt;strong&gt;Dell PowerEdge R820&lt;/strong&gt; with 4 Intel Xeon E5-4650 CPUs (32 cores, 64 threads), 768 GB DDR3 ECC Memory and a total of 3 TB SAS HDD space. Furthermore, I extended it with a 1TB NVMe and an
NVIDIA GeForce GTX 1050 Ti. The VMs will be hosted on the NVMe drive, while the SATA drives serve as a backup. The graphics card will provide hardware-accelerated streaming for Plex.&lt;/p&gt;
&lt;p&gt;I ended up buying a &lt;strong&gt;QNAP TVS-1271U-RP&lt;/strong&gt; with 12 x 8 TB Seagate HDDs as my NAS solution. It was a good fit for the setup and extendable. I upgraded the hardware to include another 2TB of NVMe storage for cache acceleration and switched the shipped Intel® Core™ i3-4150 for an Intel® Core™ i7-4790S and upgraded the RAM from 8GB to 32GB. The upgrade provides me with ample power to manage QNAPs services and use it as an alternative for my VMs as a failsafe.&lt;/p&gt;
&lt;p&gt;Both servers include a 4-port gigabit NIC as well as a 2-port 10 gigabit NIC each. All of that gets connected to my existing Ubiquiti network hardware (Unifi Security Gateway Pro, Unifi 48-port switch, CloudKey Gen 2). The hardware will be mounted in an APC NetShelter SX 24U that I got for nine bucks 💎.&lt;/p&gt;
&lt;p&gt;The last piece in my setup is a &lt;strong&gt;Powerwalker VI 2200&lt;/strong&gt; for power failure management. The USV can handle up to 1320 watts (overkill), which is ample to secure the hardware. I still have a smaller APC left that provides up to 400 watts additionally.&lt;/p&gt;
&lt;p&gt;I got most of the hardware or upgrades either through Ricardo, Tutti or AliExpress. It costs me less than 5000 CHF in total, which is a bargain (including everything, network hardware, cables etc.).&lt;/p&gt;
&lt;h3 id=&quot;so-whats-next&quot;&gt;So what’s next&lt;/h3&gt;
&lt;p&gt;Everything is installed and running so far as expected. Next, I’d like to write up on how I installed and configured everything, providing tutorials on setting up ESXi, VSphere, QNAP, Plex and all the other services I use to make my digital life run on autopilot.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Python 3.9 - What's new]]></title><description><![CDATA[A short introduction to the new functionalities that Python 3.9 offers. It adds a lot of minor quality of life improvements as well as new functionality.]]></description><link>https://angelo.dini.dev/blog/python_3_9/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/python_3_9/</guid><pubDate>Thu, 14 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;whats-new-in-python-39x&quot;&gt;What’s new in Python 3.9.x&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Python 3.9.0 has been released on the 5th of October, and Python 3.9.1 followed shortly after on the 7th of December.&lt;/p&gt;&lt;p&gt;They introduce a bunch of new functionality to Python’s syntax, build-in features, the standard library and the interpreter. For my learning objective, I’d like to go over some fundamental changes and provide examples and tips on using some of the new features.&lt;/p&gt;
&lt;p&gt;Let’s start with how the language is maintained first, before moving onwards to the fancy part.&lt;/p&gt;
&lt;h3 id=&quot;moving-towards-a-yearly-release-cycle&quot;&gt;Moving towards a yearly release cycle&lt;/h3&gt;
&lt;p&gt;Previously, Python has been released on an eighteen-month cycle. 3.9 will change this to an &lt;strong&gt;annual release cycle&lt;/strong&gt;. This change comes from the increasing popularity of the language in various areas such as data science or machine learning.&lt;/p&gt;
&lt;p&gt;Naturally, releases will become smaller to land features and bug fixes sooner into the users’ hands. A shorter release cycle also decreases the rush before betas and creates further predictable planning for everyone involved.&lt;/p&gt;
&lt;p&gt;Related Python Enhancement Proposals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0602/&quot;&gt;PEP 602&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;performance-improvements&quot;&gt;Performance improvements&lt;/h3&gt;
&lt;p&gt;Python has continuously improved its performance on every past release. However, 3.9 introduced two changes that don’t require any code adaptions at all. Without getting into details, the new PEG parser is more efficient, and many builtins now use the &lt;code&gt;vectorcall&lt;/code&gt; convention to make their execution faster.&lt;/p&gt;
&lt;p&gt;Related Python Enhancement Proposals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0590/&quot;&gt;PEP 590&lt;/a&gt; – Vectorcall: a fast calling protocol for CPython&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0617/&quot;&gt;PEP 617&lt;/a&gt; – New PEG parser for CPython&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;internal-changes&quot;&gt;Internal changes&lt;/h3&gt;
&lt;p&gt;Cleaning up code, including improving and modernising the codebase, is an ongoing development task for every developer. This approach applies to Python’s core developers as well. Besides the removal of deprecated compatibility code, there are some noteworthy internal changes.&lt;/p&gt;
&lt;p&gt;These include; module state access is now possible from C extension methods, garbage collection doesn’t block on resurrected objects, and several additional modules use multi-phase initialization and/or the stable ABI.&lt;/p&gt;
&lt;p&gt;Related Python Enhancement Proposals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0384/&quot;&gt;PEP 384&lt;/a&gt; – Defining a Stable ABI&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0489/&quot;&gt;PEP 489&lt;/a&gt; – Multi-phase extension module initialization&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0573&quot;&gt;PEP 573&lt;/a&gt; – Module State Access from C Extension Methods&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;new-features&quot;&gt;New features&lt;/h2&gt;
&lt;p&gt;There are three significant features I’d like to dig deeper into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how prefixes and suffixes work in string operations&lt;/li&gt;
&lt;li&gt;what the union operator allows us to do in dictionaries&lt;/li&gt;
&lt;li&gt;the relaxation on decorators to improve maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, all changes are listed after them for completeness.&lt;/p&gt;
&lt;h3 id=&quot;prefixes-and-suffixes-string-operations&quot;&gt;Prefixes and suffixes string operations&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0616/&quot;&gt;PEP 616&lt;/a&gt; introduces string methods to remove prefixes and suffixes. Let’s have a look at an example:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; test_func_name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;startswith&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;test_func_name&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;test_func_name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There we check if &lt;code&gt;test_func_name&lt;/code&gt; starts with “test_” (prefix). If this matches, we remove “test_” and return the new value or return &lt;code&gt;test_func_name&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This can now be simplified by using &lt;code&gt;removeprefix&lt;/code&gt; or &lt;code&gt;removesuffix&lt;/code&gt; respectively:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# for the prefix case&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;test_func_name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;removeprefix&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# for the suffix case&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;test_func_name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;removesuffix&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;add-union-operators-to-dictionaries&quot;&gt;Add union operators to dictionaries&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0584/&quot;&gt;PEP 584&lt;/a&gt; introduces a union operator to dictionaries. That operator dramatically simplifies how two dictionaries can be merged. Let’s have a look at an example:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;d1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;spam&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
d2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;eggs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;**&lt;/span&gt;d1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;**&lt;/span&gt;d2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may also want to have a look at &lt;a href=&quot;https://www.python.org/dev/peps/pep-0584/#id13&quot;&gt;dict.update&lt;/a&gt; or &lt;a href=&quot;https://www.python.org/dev/peps/pep-0584/#collections-chainmap&quot;&gt;collections.ChainMap&lt;/a&gt;, as these offer similar functionalities. Though the union operator is more readable and avoids some pitfalls when merging objects:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;d1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;spam&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
d2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;eggs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;d1 &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; d2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;more-flexible-decorators&quot;&gt;More flexible decorators&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0614/&quot;&gt;PEP 614&lt;/a&gt; relaxes the grammar restrictions on decorators. They were initially limited as a preference rather than a technical requirement. To demonstrate the benefits, let’s have a look at a code example:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;button_0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; buttons&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
button_1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; buttons&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@button_0&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clicked&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connect&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;spam&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@button_1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clicked&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connect&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;eggs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can now simplify and make it more readable, idiomatic and maintainable:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@buttons&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clicked&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connect
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;spam&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@buttons&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clicked&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;connect
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;eggs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;more-type-hinting&quot;&gt;More type hinting&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0585/&quot;&gt;PEP 585&lt;/a&gt; adds more type hinting capabilities which were first introduced in Python 3.5. More specifically, Python now enables support for the generics syntax in all standard collections currently available in the &lt;code&gt;typing&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0593/&quot;&gt;PEP 593&lt;/a&gt; adds a mechanism to extend the type annotations from &lt;a href=&quot;https://www.python.org/dev/peps/pep-0484/&quot;&gt;PEP 484&lt;/a&gt; with arbitrary metadata.&lt;/p&gt;
&lt;h3 id=&quot;iana-time-zone-support&quot;&gt;IANA time zone support&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.python.org/dev/peps/pep-0615/&quot;&gt;PEP 615&lt;/a&gt; introduces support for the IANA time zone database in the standard library. This feature is a bit more abstract (and I don’t understand it well enough), so I recommend to dig into the PEP specification to learn more about it.&lt;/p&gt;
&lt;p&gt;You can find more information about the IANA time zone database on &lt;a href=&quot;https://www.iana.org/time-zones&quot;&gt;their website&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;other-changes&quot;&gt;Other changes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;os.pidfd_open()&lt;/code&gt; has been added that allows process management without races and signals.&lt;/li&gt;
&lt;li&gt;An implementation of a topological sort of a graph is now provided in the new graphlib module.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Python 3.9 adds a lot of excellent new features to improve the quality of life for any developer. A faster release cycle will allow us to upgrade faster with fewer compatibility issues and the new features are a welcome addition to the language.&lt;/p&gt;
&lt;p&gt;We expect Python 3.10 to be released around the &lt;strong&gt;4th of October&lt;/strong&gt;. But don’t worry, all releases are supported for five years, so 3.9 will be active until 2025.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Looking back at 2020]]></title><description><![CDATA[A short review of my 2020 in bullet points: work at Divio and django CMS, starting my computer science studies at FHNW, and what Corona changed along the way.]]></description><link>https://angelo.dini.dev/blog/looking-back-at-2020/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/looking-back-at-2020/</guid><pubDate>Thu, 31 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;looking-back-at-2020&quot;&gt;Looking back at 2020&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;2020 hasn't been short on events. We added multiple environment support at Divio, with a bunch of fantastic UI improvements. My studies at FHNW finally started after a year of preparation, and of course, Corona disturbed all our lives.&lt;/p&gt;&lt;p&gt;To finish the year, I summarise what I’ve been doing at work, school and otherwise in this article. For my sanity and everyone else’s; as bullet points 😅.&lt;/p&gt;
&lt;h3 id=&quot;working-hard&quot;&gt;Working hard&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;We added multiple environments, customisable services, new views, better logs, changelog, a lot of automation, and many more UI improvements to Divio Cloud.&lt;/li&gt;
&lt;li&gt;We upgraded django CMS to support Django Version 3 and above plus released django CMS 3.8.0 along the way.&lt;/li&gt;
&lt;li&gt;The django CMS Association was founded with support from many other companies to lead the future for django CMS and its ecosystem.&lt;/li&gt;
&lt;li&gt;Corona didn’t spare our company, so we had to say goodbye to some great team members and friends.&lt;/li&gt;
&lt;li&gt;I have been working at home for about two days a week in the past. Corona changed this to full-time home-office. It sounds great on paper, but I like to be at the office at least once or twice to get out of the house.&lt;/li&gt;
&lt;li&gt;I also teach at HSR (now OST) and recreated most of the material for the lectures that year with success.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;learning-again&quot;&gt;Learning again&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I completed the preparation course to join FHNW’s bachelor programme successfully.&lt;/li&gt;
&lt;li&gt;Now studying Computer Science and almost finished my first semester without any significant issues.&lt;/li&gt;
&lt;li&gt;Passed the initial evaluation tests to join the English electives (highest grades) and got additional free ECTS points 😎.&lt;/li&gt;
&lt;li&gt;Also, Corona isn’t much fun here. Even though we started the first days on campus, the latter half was remote.&lt;/li&gt;
&lt;li&gt;Played Badminton at school at first, but again, Corona 🤮.&lt;/li&gt;
&lt;li&gt;Grateful to all other students and new won friends, making it through this difficult time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;personal-achievements&quot;&gt;Personal achievements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I published my website and a blog post every month!&lt;/li&gt;
&lt;li&gt;Started to record a journal about my days to create this summary ultimately 👍.&lt;/li&gt;
&lt;li&gt;Began with new morning and evening routines that help me to be a bit more efficient.&lt;/li&gt;
&lt;li&gt;A tropical sickness plagued me for weeks, that wasn’t fun.&lt;/li&gt;
&lt;li&gt;Had some great financial success investing in companies, might start writing about that in the future.&lt;/li&gt;
&lt;li&gt;Was able to read again about 50 books this year! Looking at less though in 2021 as studies will consume more time.&lt;/li&gt;
&lt;li&gt;Neglected Anki and Udemy this year to focus on other things, let’s see if I can get that back on track 2021.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-big-thank-you&quot;&gt;A big thank you&lt;/h3&gt;
&lt;p&gt;I want to thank everyone who helped me at work, at school or privately to have a good 2020. Especially Jasmin, my better half, who got me through some rough times. 2021 looks good so far, and I might have a ton of new updates by the end of it, looking forward to some big changes!&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Host a Gatsby project on Divio Cloud]]></title><description><![CDATA[A walk through hosting a Gatsby site on Divio Cloud: creating the project, adding the git remote, writing the Dockerfile and deploying to a test server.]]></description><link>https://angelo.dini.dev/blog/hosting-on-divio-cloud/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/hosting-on-divio-cloud/</guid><pubDate>Sat, 28 Nov 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;how-i-setup-my-website-on-divio-cloud&quot;&gt;How I setup my website on Divio Cloud&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Preparing a Gatsby project to be hosted on Divio Cloud is relatively simple. I'll guide you through the process of creating a Gatsby starter project, adding the Divio Cloud remote repository and deploying the project.&lt;/p&gt;&lt;p&gt;There have been a lot of improvements on Divio Cloud since I launched my website. These changes made the hosting setup even more straightforward. I decided to record my steps while upgrading my website. Start by heading over to &lt;a href=&quot;https://control.divio.com/&quot;&gt;control.divio.com&lt;/a&gt; and create an account.&lt;/p&gt;
&lt;p&gt;First, add a new project and enter an appropriate name. Choose “&lt;strong&gt;Build your own&lt;/strong&gt;” from the options, as we want to configure everything by ourselves. Hit “&lt;strong&gt;Continue&lt;/strong&gt;” to proceed.&lt;/p&gt;
&lt;p&gt;Select the developer subscription to get started. Make sure to upgrade your subscription as soon as you go live; otherwise, you may lack resources to handle the incoming traffic. Also, consider getting in touch with Divio’s excellent support team to assist in choosing the right subscription to your needs.&lt;/p&gt;
&lt;p&gt;Next, create a project folder somewhere on your system and create a simple Gatsby project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;run &lt;code&gt;mkdir my-gatsby&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;cd my-gatsby&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;npm install -g gatsby-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;gatsby new . https://github.com/gatsbyjs/gatsby-starter-hello-world&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can also use any other &lt;a href=&quot;https://www.gatsbyjs.com/starters/&quot;&gt;Gatsby starter&lt;/a&gt; with Divio Cloud, just make sure to set up the project in the root of the folder.&lt;/p&gt;
&lt;p&gt;Next, connect the project to Divio Cloud:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;run &lt;code&gt;git init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;git remote add origin git@git.divio.com:gatsby-on-divio-cloud.git&lt;/code&gt;
(make sure to use your git remote)&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;git push origin master --force&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your local setup should now be in sync with Divio Cloud. Let’s configure your project to deploy it.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; node:14.15.1-alpine&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
  make g++ &amp;amp;&amp;amp; &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
  apk add vips-dev fftw-dev --update-cache &lt;span class=&quot;token operator&quot;&gt;\&lt;/span&gt;
  &amp;amp;&amp;amp; rm -fR /var/cache/apk/*&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# the cli is required to build the site&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm install -g gatsby-cli&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# switch to app dir to install node_packages into app/node_packages&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# this allows them to be available on your host system through docker-compose&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# copy the package files first for them to get cached&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; package*.json /app/&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm install&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . /app&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# build the project and serve it&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; npm run build&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 80&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CMD&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;npm&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;run&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;deploy&quot;&lt;/span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a &lt;code&gt;.dockerignore&lt;/code&gt; file with the following content:&lt;/p&gt;
&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;.cache/
node_modules/
public/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To finalise the setup, add the deploy script to the &lt;code&gt;package.json&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ...
  &lt;span class=&quot;token property&quot;&gt;&quot;deploy&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gatsby serve --port 80 --host 0.0.0.0&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Commit all your changes and push them to Divio Cloud. Go back to the web interface and hit the “&lt;strong&gt;Deploy&lt;/strong&gt;” button on the test server. Wait a bit, and you are ready to go! Open up the test server by clicking on the “env url”. Make sure to check out the “Logs” section if you are experiencing any errors along the way.&lt;/p&gt;
&lt;p&gt;Enjoy your new Gatsby playground.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Apps I can't live without]]></title><description><![CDATA[The ten macOS apps I open almost every day, from Notion and 2Do to Habitica and Plex, with the workflow behind each one and the alternatives I considered.]]></description><link>https://angelo.dini.dev/blog/necessary-apps/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/necessary-apps/</guid><pubDate>Sat, 31 Oct 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;top-10-tools-i-use-every-day&quot;&gt;Top 10 tools I use every day&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Everyone has their specific arsenal of tools to optimise workflows or to make life easier. As the saying goes: pick the right tool for the job.&lt;/p&gt;&lt;p&gt;This article focuses on ten or more tools I’m using almost every single day. These help me to stay organised so that I can store and retrieve knowledge on short notice and keep my information architecture structured.&lt;/p&gt;
&lt;p&gt;Tools change over time; this list represents a snapshot of my current tool-chain. An overview of all the tools I’m using can be found at the end of the article.&lt;/p&gt;
&lt;p&gt;Not every tool is indispensable, and some are used more often than others. The following tools are the most important to me in order of usage:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#notion&quot;&gt;Notion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#unclutter&quot;&gt;Unclutter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2do&quot;&gt;2Do&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#habitica&quot;&gt;Habitica&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#airmail&quot;&gt;Airmail&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#calendar&quot;&gt;Calendar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#in-your-face&quot;&gt;In Your Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#vscode-iterm&quot;&gt;VSCode &amp;amp; iTerm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tower&quot;&gt;Tower&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#plex&quot;&gt;Plex&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;&lt;p&gt;&lt;span id=&quot;notion&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-notion&quot;&gt;1. Notion&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 1. Notion&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.notion.so/&quot;&gt;https://www.notion.so/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Wiki, Organisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.getoutline.com/&quot;&gt;Outline&lt;/a&gt; (self-hosted)&lt;br /&gt;&lt;a href=&quot;https://www.atlassian.com/software/confluence&quot;&gt;Confluence&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Notion serves as my &lt;strong&gt;second brain&lt;/strong&gt; 🧠. It’s where I capture, organise and reclaim ideas, notes, summaries and any other long-term information that should be permanently stored.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easily create pages, add content, link and personalise them.&lt;/li&gt;
&lt;li&gt;Create powerful databases with different views to manage lists, notes, meetings, projects and much more.&lt;/li&gt;
&lt;li&gt;Linked databases allow creating different views from existing data sets.&lt;/li&gt;
&lt;li&gt;Available as an app for macOS, iOS, iPadOS and online with offline mode support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: All short and long-term information is stored in Notion. The below image shows an outline of the structures I’ve created so far. Each category has further information and databases.&lt;/p&gt;
&lt;p&gt;For example, I created an overview of all available modules at school and filter them by active modules (the ones I’m currently registered for). Each module has further entries with linked databases to store notes, due exams or other relevant information.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/notion-720.webp 720w, /blog/necessary-apps/assets/notion.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/notion.png&quot; alt=&quot;Example Notion landing page&quot; width=&quot;1024&quot; height=&quot;634&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example Notion landing page&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;unclutter&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-unclutter&quot;&gt;2. Unclutter&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 2. Unclutter&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://unclutterapp.com/&quot;&gt;https://unclutterapp.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Notes, Todos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bear.app/&quot;&gt;Bear&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://evernote.com/&quot;&gt;Evernote&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;I’ve been using Bear in the past to structure my information. The experience was never satisfying to me. Either some records got lost in the endless abyss of data storage, or I didn’t find what I needed. So I adapted my process when I switched from Bear to Notion.&lt;/p&gt;
&lt;p&gt;This change introduced Unclutter as a new tool. It helps create quick notes and review them later on, to decide, if the information should go into long-term storage.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Superfast access via the scroll-wheel on the macOS menu bar.&lt;/li&gt;
&lt;li&gt;The flyout shows the clipboard history, most used files (or folders) and notes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: Notes or todo candidates are written directly into the flyout. The main criteria are that it is super fast accessible in any context, independent of the window that is currently active. The clipboard and files are just a bonus.&lt;/p&gt;
&lt;p&gt;However, this only works on my computer. I do not have a reliable solution yet for iOS or iPadOS. There I use the standard Notes app alternately.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/unclutter-720.webp 720w, /blog/necessary-apps/assets/unclutter.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/unclutter.png&quot; alt=&quot;Example Unclutter panel&quot; width=&quot;1024&quot; height=&quot;180&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example Unclutter panel&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;2do&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-2do&quot;&gt;3. 2Do&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 3. 2Do&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.2doapp.com/&quot;&gt;https://www.2doapp.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Todos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://todoist.com/&quot;&gt;Todoist&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://culturedcode.com/things/&quot;&gt;Things&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Even though Notion offers some todo functionality, it is far from usable. Instead, I use 2Do to manage my todos. Everyone should have a todo app/system in place to manage their tasks. Which one you use is mostly personal preference.&lt;/p&gt;
&lt;p&gt;I choose 2Do because it had no subscription model compared to other tools available with free syncing to iCloud and no hidden costs.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add, schedule and mark todos and categorise them.&lt;/li&gt;
&lt;li&gt;Capable of adding recurring todos and checklists.&lt;/li&gt;
&lt;li&gt;Synchronisation across all my devices.&lt;/li&gt;
&lt;li&gt;Simple UI and fast creation of todos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: I either open the app to create a todo or use the allocated shortcut, depending on what kind of todo that needs to be made. I try to keep the list as short as possible and time every todo. This process ensures that the todos will be done.&lt;/p&gt;
&lt;p&gt;2Do is a holy place for me. They are promises to myself about my daily, weekly or monthly responsibilities and goals. As all of them are timed, having todos past their due date is a sin 😇. Anything else is just bad planning or evaluation from my end.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/2do-720.webp 720w, /blog/necessary-apps/assets/2do.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/2do.png&quot; alt=&quot;Example 2Do window&quot; width=&quot;1024&quot; height=&quot;472&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example 2Do window&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;habitica&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;4-habitica&quot;&gt;4. Habitica&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 4. Habitica&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://habitica.com/&quot;&gt;https://habitica.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Habits, Todos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.habitify.me/&quot;&gt;Habitify&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.stridesapp.com/&quot;&gt;Strides&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Habitica is fun. I have been working on improving my workflow and habits for a while now and came across this gem. It makes creating and tracking habits entertaining, and you can create or join challenges with other people. You can also invite friends into a group and work together.&lt;/p&gt;
&lt;p&gt;I’ll send you an invite to our Party 🎉 if you send me your account information.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add habits, dailies and todos.&lt;/li&gt;
&lt;li&gt;A gamification system to keep you going with monsters, equipment, guilds and challenges.&lt;/li&gt;
&lt;li&gt;Make habits with friends or other people.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: I’ve accumulated a set of habits that I keep track of since I started using Habitica. These include regular reading sessions, sharpening my skill set, eating healthy, playing the guitar and numerous more.&lt;/p&gt;
&lt;p&gt;Personal challenges accompany habits. These challenges aim to gain new expertise or getting myself out of the comfort zone. For example, the creation of this website was a past challenge. My current challenge requires getting myself into the financial market.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/habitica-720.webp 720w, /blog/necessary-apps/assets/habitica.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/habitica.png&quot; alt=&quot;Example Habitica overview&quot; width=&quot;1024&quot; height=&quot;662&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example Habitica overview&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;airmail&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;5-airmail&quot;&gt;5. Airmail&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 5. Airmail&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://airmailapp.com/&quot;&gt;https://airmailapp.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Messaging, Todos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.google.com/gmail/&quot;&gt;GMail&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://sparkmailapp.com/&quot;&gt;Spark&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Everyone has an email client. The default Mail client on macOS has been a pain to me in the past. Apple either added weird updates or simplified the functionality to make it more accessible to anyone. So a more feature-rich email client was required.&lt;/p&gt;
&lt;p&gt;Airmail covered all the functionality that Apple Mail lacked in the past. They have recently switched to a paid-subscription model, and so far I’m paying it.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Well, receive and send emails ✉️&lt;/li&gt;
&lt;li&gt;Snooze and reschedule emails (todos, yay)&lt;/li&gt;
&lt;li&gt;Multiple accounts with different providers&lt;/li&gt;
&lt;li&gt;Apps for macOS, iOS and iPadOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: Similar to 2Do, I tend to keep the inbox as clean as possible. Incoming mails that need no action get either archived or are moved into a specific folder.&lt;/p&gt;
&lt;p&gt;Sometimes actions need to be taken from emails. Then I either create a todo and inform the sender, or I snooze the email to a point where I have time to deal with it. The general rule is to answer within 24 hours or less.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/airmail-720.webp 720w, /blog/necessary-apps/assets/airmail.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/airmail.png&quot; alt=&quot;Example Airmail window&quot; width=&quot;1024&quot; height=&quot;460&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example Airmail window&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;calendar&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;6-calendar&quot;&gt;6. Calendar&lt;/h3&gt;
&lt;p&gt;This section will be short. I use the standard Apple Calendar and have not yet switched to another tool such as &lt;a href=&quot;https://flexibits.com/fantastical&quot;&gt;Fantastical&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main reason is that I use the calendar for meetings only and not to schedule todos or deadlines. The only exemptions are birthdates or events.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: All calendars I have (school, work, private) have their feeds. These feeds are collected into Apple Calendar to generate an overview of all entries.&lt;/p&gt;
&lt;p&gt;The calendar notifies me about upcoming meetings with a simple notification prompt. Though that’s not enough, so let’s have a look at “In Your Face” next.&lt;/p&gt;
&lt;p&gt;&lt;span id=&quot;in-your-face&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;7-in-your-face&quot;&gt;7. In Your Face&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 7. In Your Face&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.inyourface.app/&quot;&gt;https://www.inyourface.app/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;Generally any calendar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;If you ever had a member that missed a meeting, was late or didn’t notify you about their absence, mandate that they install the “In Your Face” app.&lt;/p&gt;
&lt;p&gt;The notification prompt from a calendar entry is quickly closed and as we know: out of sight, out of mind. Suddenly you receive a message over Slack, that the meeting has started and you are missing. This experience is frustrating and embarrassing.&lt;/p&gt;
&lt;p&gt;This app helps you by showing a colossal overlay blocking your entire screen, informing you that there will be a meeting in x minutes.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shows an overlay blocking you from working&lt;/li&gt;
&lt;li&gt;You can set the pre-notification time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: I set the notification to appear 2 minutes before the meeting. This timeframe gives me enough time to stop whatever I was doing and join the event.&lt;/p&gt;
&lt;p&gt;Alternatively, if another task still occupies me, I can quickly inform the organiser about my absence.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/inyourface-720.webp 720w, /blog/necessary-apps/assets/inyourface.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/inyourface.png&quot; alt=&quot;Example In Your Face overlay&quot; width=&quot;1024&quot; height=&quot;640&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example In Your Face overlay&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;vscode-iterm&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;8-vscode-iterm&quot;&gt;8. VSCode &amp;amp; iTerm&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 8. VSCode &amp;amp; iTerm&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;https://code.visualstudio.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Productivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/alacritty/alacritty&quot;&gt;Alacritty&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.jetbrains.com/idea/&quot;&gt;IntelliJ IDEA&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;VSCode is my primary code editor. But I still use iTerm and Vim as companion tools for quick editing or running commands. Tmux has also become a great automation tool to streamline project initialisations. I’ve used &lt;a href=&quot;https://hyper.is/&quot;&gt;Hyper&lt;/a&gt; in the past, but moved back to iTerm because of stability issues.&lt;/p&gt;
&lt;p&gt;Other than that: &lt;code&gt;ws&lt;/code&gt; + &lt;code&gt;cd angelo.dini&lt;/code&gt; + &lt;code&gt;code&lt;/code&gt; = win.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast startup and excellent stability.&lt;/li&gt;
&lt;li&gt;Numerous extensions and exceptional community support.&lt;/li&gt;
&lt;li&gt;Easy to get used to, challenging to master.&lt;/li&gt;
&lt;li&gt;Automation capability via CLI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: I won’t go into much detail about my development workflow as it consists mostly of coding. However, I recommend having a look at my &lt;a href=&quot;https://github.com/FinalAngel/dotfiles&quot;&gt;dotfiles&lt;/a&gt;, as they show the setup quite thoroughly. I’ve also created a &lt;a href=&quot;/blog/dotfiles/&quot;&gt;blog post&lt;/a&gt; writing about my dotfiles setup there.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/vscode-720.webp 720w, /blog/necessary-apps/assets/vscode.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/vscode.png&quot; alt=&quot;Example VSCode window&quot; width=&quot;1024&quot; height=&quot;604&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example VSCode window&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;tower&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;9-tower&quot;&gt;9. Tower&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 9. Tower&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.git-tower.com/&quot;&gt;https://www.git-tower.com/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Productivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.sourcetreeapp.com/&quot;&gt;Sourcetree&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://desktop.github.com/&quot;&gt;GitHub Desktop&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Git is a blessing and can easily be handled from the terminal, but visualisation of code changes is underwhelming. Most of my Git workflow involves the command line interface via iTerm, but for some, I prefer a GUI over additional Vim plugins.&lt;/p&gt;
&lt;p&gt;Particularly for reviewing diffs and staging files or chunks, the command line lacks these visualisation tools. It’s very advantageous to scroll and navigate fast between two files from an interactive window and ensure the correct data gets committed. The rest is perfectly handled via the command line.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;View visualised diffs&lt;/li&gt;
&lt;li&gt;Push, pull, stage or stash files&lt;/li&gt;
&lt;li&gt;Manage repositories, remotes, tags and submodules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: I use Tower only for its diff view. Occasionally I also fire up &lt;a href=&quot;https://kaleidoscope.app/&quot;&gt;Kaleidoscope&lt;/a&gt; to compare files or folders in more depth.&lt;/p&gt;
&lt;p&gt;Tower is excellent to assess the diff between commits as well as stage chunks or files. I’m used to the command line tools in general, but that workflow is still preferred to me.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/tower-720.webp 720w, /blog/necessary-apps/assets/tower.webp 960w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/tower.png&quot; alt=&quot;Example Tower window&quot; width=&quot;960&quot; height=&quot;590&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example Tower window&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span id=&quot;plex&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;10-plex&quot;&gt;10. Plex&lt;/h3&gt;
&lt;div class=&quot;tbl&quot; role=&quot;region&quot; aria-label=&quot;Table: 10. Plex&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.plex.tv/&quot;&gt;https://www.plex.tv/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;Procrastination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternatives&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://emby.media/&quot;&gt;Emby&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://kodi.tv/&quot;&gt;Kodi&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Plex in itself is a hobby of mine. I’m a data hoarder and own a small server with my media files that I tinker around from time to time. I host all sorts of media files, not just TV Shows, Movies and Pictures.&lt;/p&gt;
&lt;p&gt;The most notable features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store and manage all your media files&lt;/li&gt;
&lt;li&gt;View your media files anywhere 📺&lt;/li&gt;
&lt;li&gt;Apps for macOS, iOS and iPadOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;: Plex is not the only tool used in that media-verse. It’s accompanied by Sonarr, Radarr, Lidarr, Mylar, Calibre, SABnzbd, Transmission and Ubooquity with lists stored on IMDb, Goodreads and Trakt.&lt;/p&gt;
&lt;p&gt;That toolbox allows me to automate and streamline the process of downloading, archiving and viewing media files on all my major devices. It might be worth to create a blog post about it once 😎.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/necessary-apps/assets/plex-720.webp 720w, /blog/necessary-apps/assets/plex.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/necessary-apps/assets/plex.png&quot; alt=&quot;Example Plex window&quot; width=&quot;1024&quot; height=&quot;616&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Example Plex window&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;This concludes the list of the top ten tools I use every day. Of course, this list is not complete nor final, and there are various other tools I’m using every day that I didn’t mention in more depth here. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1Password, Grammarly, Chrome&lt;/li&gt;
&lt;li&gt;Slack, Microsoft Teams, Discord&lt;/li&gt;
&lt;li&gt;GoodNotes, MarginNote, MindNode, Anki&lt;/li&gt;
&lt;li&gt;Affinity Photo, Affinity Designer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;to name a few. You can find a complete list of all my applications in my &lt;a href=&quot;https://github.com/FinalAngel/dotfiles/tree/master/homebrew&quot;&gt;dotfiles&lt;/a&gt;. I keep them up to date as I install new software.&lt;/p&gt;
&lt;p&gt;Now it’s your turn. I’d love to hear from you about interesting tools that optimise your workflow. Add a comment or reach out to me via &lt;a href=&quot;https://twitter.com/finalangel&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Improve your profile on GitHub]]></title><description><![CDATA[Your GitHub profile is a first impression. How to add a profile README, which tools and widgets are worth using, and where to find the traffic statistics.]]></description><link>https://angelo.dini.dev/blog/github-readme/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/github-readme/</guid><pubDate>Sun, 20 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;create-an-engaging-profile&quot;&gt;Create an engaging profile&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;GitHub is a fantastic platform to contribute and share your code. But it's not just that! Your profile page can provide a first impression on fellow members or companies interested in your work.&lt;/p&gt;&lt;p&gt;Like a resume, it should present your most significant work as well as personal interests and affiliations. Many members neglect the importance of an attractive digital appearance. To improve this, make sure to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add a short bio about your interests, goals or aspirations&lt;/li&gt;
&lt;li&gt;add information about where to reach you, like Twitter or a personal website&lt;/li&gt;
&lt;li&gt;create a showcase of repositories you have been considerably involved in, so your skills are presented favourably&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m not particularly interested in how many followers I have or whom I follow so far. So I’m leaving this off the list for now.&lt;/p&gt;
&lt;h3 id=&quot;express-yourself-by-adding-a-github-readme&quot;&gt;Express yourself by adding a GitHub README&lt;/h3&gt;
&lt;p&gt;The information provided above is static; it looks the same on almost every GitHub profile. It would be great to express yourself further by adding some custom content to stand out.&lt;/p&gt;
&lt;p&gt;I recently discovered that you can do just that by adding a customisable README to your profile:&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/github-readme/assets/profile-720.webp 720w, /blog/github-readme/assets/profile.webp 920w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/github-readme/assets/profile.png&quot; alt=&quot;My customised GitHub profile&quot; width=&quot;920&quot; height=&quot;360&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;My customised GitHub profile&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;GitHub even has &lt;a href=&quot;https://docs.github.com/en/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme&quot;&gt;documentation&lt;/a&gt; available, showing you step-by-step how to do that.&lt;/p&gt;
&lt;p&gt;In short, you have to create a new repository with a name that matches your &lt;strong&gt;username&lt;/strong&gt;. So for me, it was &lt;a href=&quot;https://github.com/finalangel/finalangel&quot;&gt;github.com/finalangel/finalangel&lt;/a&gt;. Next, you create a &lt;code&gt;README.md&lt;/code&gt; file and add the content you want to appear. Once ready, commit it, and your profile page will automatically show the content.&lt;/p&gt;
&lt;p&gt;There are &lt;a href=&quot;https://www.makeareadme.com/&quot;&gt;many resources&lt;/a&gt; available on how to use markdown to create a README. I recommend to familiarise yourself with the syntax to get the most out of it for your profile. Alternatively, you can use HTML right in the &lt;code&gt;README.md&lt;/code&gt; file to structure the content instead.&lt;/p&gt;
&lt;p&gt;Finally, I would like to mention some handy tools and inspirations I came along while creating my README.&lt;/p&gt;
&lt;h3 id=&quot;make-the-most-out-of-your-profile&quot;&gt;Make the most out of your profile&lt;/h3&gt;
&lt;p&gt;Adding a README to your profile is a simple improvement with many possibilities. You can even add dynamic elements, charts, blog posts, tweets, gifs and much more. A great resource is &lt;a href=&quot;https://github.com/abhisheknaiidu/awesome-github-profile-readme&quot;&gt;awesome-github-profile-readme&lt;/a&gt; that references many creative examples of other users’ profiles.&lt;/p&gt;
&lt;p&gt;There are also great dynamically generated GitHub widgets available on &lt;a href=&quot;https://github.com/anuraghazra/github-readme-stats&quot;&gt;github-readme-stats&lt;/a&gt; which are simple to implement. They can enrich the personality or message you want to express.&lt;/p&gt;
&lt;p&gt;In addition to that, you also get a free analytics tool for your GitHub profile. Go to the created repository and open up “&lt;strong&gt;Insights&lt;/strong&gt;” followed by “&lt;strong&gt;Traffic&lt;/strong&gt;”. These statistics offer some great insights into the number of visitors your profile receives.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Overall I recommend to keep it simple and don’t over clutter your profile page. The same goes for your resume. Less is more, keep it simple and focus on what you want to communicate with your visitors.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Getting into Gatsby]]></title><description><![CDATA[What I learned building three websites with Gatsby: picking a starter, page versus static GraphQL queries, Helmet, caching and deploying from GitLab CI.]]></description><link>https://angelo.dini.dev/blog/learning-gatsby/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/learning-gatsby/</guid><pubDate>Mon, 31 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;creating-gatsby-powered-websites&quot;&gt;Creating Gatsby powered websites&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;It seems like we are going back in time, building static websites without server-side code or database: introducing Gatsby.&lt;/p&gt;&lt;p&gt;Gatsby is a framework that builds upon the modern React library. Similar to &lt;a href=&quot;https://nextjs.org&quot;&gt;Next.js&lt;/a&gt;, it aims
to provide accessible tools for developing static progressive web apps (PWAs).&lt;/p&gt;
&lt;p&gt;It’s easy to get into it, if you know your React, and helps you to build fast and reliable websites that consume little resources.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gatsby is a free and open-source framework based on React that helps developers build blazing-fast websites and apps.
&lt;cite&gt;gatsbyjs.org&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This article summarises my experience building the following three websites using Gatsby:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flavours.dev/&quot;&gt;flavours.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://divio.com/&quot;&gt;divio.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and my website&lt;br /&gt;
&lt;small&gt;(I’m excusing myself from adding a link here 🤪)&lt;/small&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also do not intend this to be a step-by-step tutorial on how to get started with Gatsby. For that, have a look at Gatsby’s excellent &lt;a href=&quot;https://www.gatsbyjs.com/docs/quick-start/&quot;&gt;quick-start documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;getting-started-with-a-starter&quot;&gt;Getting started with a starter&lt;/h3&gt;
&lt;p&gt;Yeah… I just promised that this is &lt;strong&gt;not&lt;/strong&gt; going to be a step-by-step tutorial, though I have to mention the fantastic &lt;a href=&quot;https://www.gatsbyjs.com/starters/?v=0&quot;&gt;starter projects&lt;/a&gt; Gatsby provides.&lt;/p&gt;
&lt;p&gt;There you find over a hundred different opinionated Gatsby setups using best practices and plugins. You can quickly bootstrap these from the &lt;a href=&quot;https://www.gatsbyjs.com/docs/gatsby-cli/#new&quot;&gt;Gatsby CLI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We at Divio, used &lt;a href=&quot;https://github.com/fabe/gatsby-universal&quot;&gt;gatsby-universal&lt;/a&gt; as an initial starter project to create &lt;a href=&quot;https://flavours.dev/&quot;&gt;flavours.dev&lt;/a&gt;. It is still a great starter. However, today I would recommend &lt;a href=&quot;https://github.com/Vagr9K/gatsby-advanced-starter&quot;&gt;gatsby-advanced-starter&lt;/a&gt;, as it ships with more integrations out of the box and is actively maintained.&lt;/p&gt;
&lt;h3 id=&quot;know-your-react&quot;&gt;Know your React&lt;/h3&gt;
&lt;p&gt;Gatsby is based on React; as such, it is imperative to know React before trying out Gatsby. So head over to &lt;a href=&quot;https://reactjs.org/tutorial/tutorial.html&quot;&gt;reactjs.org&lt;/a&gt;, go through the tutorial and create a few sample React apps. You don’t have to be a React developer with ten years of experience to handle Gatsby, but at least familiarise yourself with React before giving Gatsby a spin.&lt;/p&gt;
&lt;p&gt;If you have already ten years of experience, let me know where you got those, considering React was released in 2013. Alternatively, if you are fluent in React, try setting up a Gatsby project from scratch and integrate TypeScript. It is a great combination. We did that while developing &lt;a href=&quot;https://divio.com/&quot;&gt;divio.com&lt;/a&gt;, and it was fun 🥳.&lt;/p&gt;
&lt;h3 id=&quot;taking-the-first-steps&quot;&gt;Taking the first steps&lt;/h3&gt;
&lt;p&gt;So why did we choose Gatsby over a traditional content management system? Well, because &lt;a href=&quot;https://flavours.dev/&quot;&gt;flavours.dev&lt;/a&gt; seemed to be a straightforward one-pager project with some content/assets and we wanted to have some fun with new technologies on Divio Cloud.&lt;/p&gt;
&lt;p&gt;We also needed the website to be super fast and responsive (the fast one), which is hard to achieve with dynamic content.&lt;/p&gt;
&lt;p&gt;Though our journey wasn’t without problems, obviously, best practices still need to be applied when dealing with the front-end code. Also, SEO needs to be spot on, by knowing which code to optimise where. That is where the starter helped us at the beginning but turned sideways once we needed to add further customisation.&lt;/p&gt;
&lt;p&gt;Some originated from not understanding how GraphQL worked in Gatsby’s context. Gatsby uses &lt;strong&gt;Page Queries&lt;/strong&gt; and &lt;strong&gt;Static Queries&lt;/strong&gt; through either the &lt;code&gt;&amp;lt;StaticQuery /&amp;gt;&lt;/code&gt; component or the &lt;code&gt;useStaticQuery&lt;/code&gt; hook. Just the sentence alone stirs up headaches. You have to know when to use what to avoid side-effects. Find out more on the &lt;a href=&quot;https://www.gatsbyjs.com/docs/static-vs-normal-queries/&quot;&gt;Gatsby documentation&lt;/a&gt; if you want to dig into the differences.&lt;/p&gt;
&lt;p&gt;I also had to get used of the concept of &lt;code&gt;&amp;lt;Helmet&amp;gt;&lt;/code&gt; at the beginning. It does a lot, and you can extend your pages easily with additional markup such as meta tags to enhance SEO. Many starters use Helmet, but you have to wrap your head around the implementation of each.&lt;/p&gt;
&lt;p&gt;Another issue we faced often was the cache. While &lt;code&gt;gatsby develop&lt;/code&gt; offers an excellent development environment, it’s far from perfect. Even &lt;code&gt;gatsby serve&lt;/code&gt; has its short-comings, and you always need to re-run the command to get to a testable state. Nothing beat testing on the production server at the end.&lt;/p&gt;
&lt;p&gt;We achieved a pretty remarkable result whilst keeping best practices in check, all thanks to Gatsby’s wonderful tools, plugins and community:&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/learning-gatsby/assets/performance-720.webp 720w, /blog/learning-gatsby/assets/performance.webp 1390w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/learning-gatsby/assets/performance.png&quot; alt=&quot;Speed score from 31st of August 2020 using Lighthouse&quot; width=&quot;1390&quot; height=&quot;502&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Speed score from 31st of August 2020 using Lighthouse&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id=&quot;taking-the-next-step&quot;&gt;Taking the next step&lt;/h3&gt;
&lt;p&gt;Once we finished our first project, we eagerly wanted to convert &lt;a href=&quot;https://divio.com/&quot;&gt;divio.com&lt;/a&gt; as well, with the following goals in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We wanted TypeScript with Gatsby to share code between the marketing website and the Control Panel.&lt;/li&gt;
&lt;li&gt;Add further linting and automation tools to cohere with the standards from the Divio Cloud.&lt;/li&gt;
&lt;li&gt;Improve the deployment process, to automatically deploy to Test/Live on Divio Cloud when a Pull Request is merged to master/develop.&lt;/li&gt;
&lt;li&gt;Find a way to integrate the Blog system into Gatsby.&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt; to share stories.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://divio.com/&quot;&gt;divio.com&lt;/a&gt; was undoubtedly a larger project. We had to migrate a substantial amount of content from the existing website into the new one.&lt;/p&gt;
&lt;p&gt;For this, we converted most content into Markdown files and loaded them through GraphQL. We did little experimentations already on &lt;a href=&quot;https://flavours.dev/&quot;&gt;flavours.dev&lt;/a&gt; but now we had to build a more robust system. We could have also integrated other tools such as &lt;a href=&quot;https://www.contentful.com/&quot;&gt;Contentful&lt;/a&gt;. However, we chose a more minimalistic approach.&lt;/p&gt;
&lt;p&gt;We practised the same approach for the Blog as well, basically migrating them to single Markdown files with an appropriate folder structure to source them through GraphQL.&lt;/p&gt;
&lt;p&gt;The integration of TypeScript, additional linting and deployment tools were pretty straightforward. The deployment bit was a bit more complicated, so I’d like to share the code for future references:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest

&lt;span class=&quot;token key atrule&quot;&gt;stages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; Build
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; Deploy

&lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm install
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build

&lt;span class=&quot;token key atrule&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm install &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;g @divio/cli
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; echo $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;DIVIO_DEPLOY_KEY&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; divio login
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; divio project&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;deploy live &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;remote&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;id $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;DIVIO_APP_ID&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == &quot;master&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;diving-deeper&quot;&gt;Diving deeper&lt;/h3&gt;
&lt;p&gt;I still followed the same principle when creating my websites. Most of the code was shared back and forth between the Divio marketing website and my own.&lt;/p&gt;
&lt;p&gt;We added nice animations through &lt;a href=&quot;https://www.framer.com/motion/&quot;&gt;Framer Motion&lt;/a&gt;, loaded data dynamically via APIs, worked with form validation and submissions, added page transitions, played around with &lt;a href=&quot;https://www.gatsbyjs.com/plugins/gatsby-image/&quot;&gt;gatsby-image&lt;/a&gt;, dug into &lt;a href=&quot;https://www.gatsbyjs.com/docs/node-apis/&quot;&gt;Gatsby’s Node API&lt;/a&gt; and even added automated tests.&lt;/p&gt;
&lt;p&gt;You can even subscribe to my Blog using an &lt;a href=&quot;https://angelo.dini.dev/rss.xml&quot;&gt;RSS Feed&lt;/a&gt;. All hosted neatly on &lt;a href=&quot;https://control.divio.com&quot;&gt;Divio Cloud&lt;/a&gt; with fast responses and fantastic support (cough 🤷).&lt;/p&gt;
&lt;p&gt;It has been an incredible journey so far, and I’ll keep using Gatsby, for now. If you’d like to hear more about implementation details or have some questions, let me know.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Automation using Dotfiles]]></title><description><![CDATA[Dotfiles are small scripts that set up, configure and maintain your system. A look at how mine are structured, what they install and how to get started.]]></description><link>https://angelo.dini.dev/blog/dotfiles/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/dotfiles/</guid><pubDate>Mon, 27 Jul 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;start-hacking-in-just-a-few-hours&quot;&gt;Start hacking in just a few hours&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;It's a beautiful, warm and sunny day outside. You are longing for your after-work beer to enjoy the rest of the day. But there is this super important project that's due by the end of the week and it's just 10 in the morning. Suddenly a wild hardware failure appears and you are out of pokeballs to solve the problem and safely continue.&lt;/p&gt;&lt;p&gt;Your computer doesn’t boot up anymore. You are hurrying to the next hardware store to get a replacement. Sitting through the setup or restore process takes a lot of time and you still have to set up the project as well to continue your work. It’s the end of the day. 😠&lt;/p&gt;
&lt;p&gt;In fairness, such a scenario does not happen that often, ideally. Though setting up a computer can be time-consuming. On top of that, all the little tweaks and settings that have been made over the years cannot easily be restored.&lt;/p&gt;
&lt;h3 id=&quot;dotfiles-to-the-rescue&quot;&gt;Dotfiles to the rescue&lt;/h3&gt;
&lt;p&gt;I first encountered the concept of &lt;em&gt;dotfiles&lt;/em&gt; about 8 years ago. I was automating the installation of multiple macOS workstations through scripts that were executed on the host system. &lt;a href=&quot;https://github.com/finalangel/osx-bootstrap&quot;&gt;osx-bootstrap&lt;/a&gt;, a collection of simple bash scripts, was my attempt to solving that problem.&lt;/p&gt;
&lt;p&gt;In essence, dotfiles are no different from the solution way back. They are a collection of scripts that run on certain conditions. Their power is only limited by your know-how of the environment/system. They can configure your entire system automatically, update apps and run maintenance scripts.&lt;/p&gt;
&lt;p&gt;This inspired me to &lt;a href=&quot;https://github.com/finalangel/dotfiles&quot;&gt;create my dotfiles&lt;/a&gt;. Their purpose is to shift my entire systems configuration into little neat shell scripts. Now, I can update and configure my machine with ease and without hassle.&lt;/p&gt;
&lt;p&gt;So if my computer decides to go to Valhalla, I simply get a new one, run the scripts and I’m ready to go. Sadly, the scripts do not automate the actual work yet &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;how-to-get-started&quot;&gt;How to get started&lt;/h3&gt;
&lt;p&gt;I highly recommend to fork and review the code first, before giving &lt;a href=&quot;https://github.com/finalangel/dotfiles&quot;&gt;my dotfiles&lt;/a&gt; a try. In theory, they can be installed on an existing system as well, but a fresh installation is recommended.&lt;/p&gt;
&lt;p&gt;To get started, follow the installation instruction in &lt;a href=&quot;https://github.com/FinalAngel/dotfiles/blob/master/README.md&quot;&gt;the readme&lt;/a&gt;. After a successful installation you should be able to run the following command in a terminal:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;dot --help&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lists all currently available commands that can be run. The options currently are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dot --install&lt;/code&gt; runs the installation process again. It’s intentional that this can be run again, as you make changes to the dotfiles yourself, certain installation actions need to run again.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dot --update&lt;/code&gt; runs the update process. This should ideally be run regularly as it runs version updates on several packages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dot --edit&lt;/code&gt; opens the dotfiles directory in your favourite editor so you can start hacking and adapting as fast as possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More functionality might be added in the future or add yours inside &lt;code&gt;bin/dot&lt;/code&gt;. It’s also helpful to run &lt;code&gt;alias&lt;/code&gt; in the terminal to see all available shorthand commands. Dotfiles normally add a bunch of them to your system.&lt;/p&gt;
&lt;figure&gt;&lt;picture&gt;&lt;source srcset=&quot;/blog/dotfiles/assets/use-case-720.webp 720w, /blog/dotfiles/assets/use-case.webp 1024w&quot; sizes=&quot;(max-width: 759px) calc(100vw - 32px), 688px&quot; type=&quot;image/webp&quot;&gt;&lt;img src=&quot;/blog/dotfiles/assets/use-case.png&quot; alt=&quot;Running my dotfiles in a shell through &amp;quot;dot --update&amp;quot;&quot; width=&quot;1024&quot; height=&quot;696&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/picture&gt;&lt;figcaption&gt;Running my dotfiles in a shell through &amp;quot;dot --update&amp;quot;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id=&quot;whats-the-structure-like&quot;&gt;What’s the structure like&lt;/h3&gt;
&lt;p&gt;I decided to follow a modular pattern in &lt;a href=&quot;https://github.com/finalangel/dotfiles&quot;&gt;my dotfiles&lt;/a&gt;. I didn’t want to spend much time figuring out where to make changes or add new features. Especially if I just discovered a new configuration/feature to integrate.&lt;/p&gt;
&lt;p&gt;As such, each folder contains its own independent logic/topic without much cross-linking. There are other patterns around. If you are interested, check out my &lt;a href=&quot;https://github.com/finalangel/dotfiles#credits&quot;&gt;credits section&lt;/a&gt; on GitHub with links to other repositories for inspiration.&lt;/p&gt;
&lt;p&gt;There are exceptions I’d like to highlight though:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;bin/&lt;/strong&gt; stores binary files that are added to the system &lt;code&gt;$PATH&lt;/code&gt; automatically. If you want to create your scripts, simply add them there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macos/&lt;/strong&gt; and &lt;strong&gt;linux/&lt;/strong&gt; contain system-specific configurations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scripts/&lt;/strong&gt; holds the main install and update scripts that include other modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;utils/&lt;/strong&gt; contain utility variables and functions that are used throughout the dotfiles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition to that, most folders implement an &lt;code&gt;install&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; file that handle the respective install/update tasks. Even though most operations are almost identical (and some could be merged), I still prefer the separation of context for better organisation 📁.&lt;/p&gt;
&lt;h3 id=&quot;whats-included&quot;&gt;What’s included&lt;/h3&gt;
&lt;p&gt;The dotfiles contain my flavour. As such, the tools used are tailored to my workflow. You might not agree and want to use alternative options. That being said, let me highlight some of the features included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;zsh/&lt;/strong&gt; module automatically loads every &lt;code&gt;aliases.zsh&lt;/code&gt;, &lt;code&gt;paths.zsh&lt;/code&gt; and &lt;code&gt;functions.zsh&lt;/code&gt; from other modules. This breaks the independence of that module but is a necessary design decision to keep maintainability.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;system/&lt;/strong&gt; module also breaks that chain by creating symlinks of every &lt;code&gt;*.symlink&lt;/code&gt; file into the users home directory &lt;code&gt;~/...&lt;/code&gt;. This is very helpful to keep configuration scripts, such as &lt;code&gt;.gitignore&lt;/code&gt; or &lt;code&gt;zshrc&lt;/code&gt;, inside the dotfiles.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;fonts/&lt;/strong&gt; module installs the most used fonts into your system. Gone are the days of looking for your CI/CD fonts.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;git/&lt;/strong&gt; module automatically configures your git settings and even helps set up a &lt;strong&gt;GPG key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;homebrew/&lt;/strong&gt; module installs common packages and applications without downloading them manually from every website.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;node/&lt;/strong&gt; and &lt;strong&gt;python/&lt;/strong&gt; modules configure my respective node and python environments to start hacking.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;tmux/&lt;/strong&gt;, &lt;strong&gt;vim/&lt;/strong&gt; and &lt;strong&gt;vscode/&lt;/strong&gt; modules setup my development environment and editors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is also a hidden &lt;strong&gt;private/&lt;/strong&gt; folder that holds scripts, that should not be public. I backup these settings through “Mackup” to my iCloud drive. The module holds scripts for my morning routine, our standup (opening google meet, load standup board, share screen, set do not disturb mode etc.) and other work-related automation.&lt;/p&gt;
&lt;p&gt;Simply automate your monotonous tasks and let the computer do the heavy lifting 🏋️.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Dotfiles are very powerful. With the right know-how, you can increase your productivity very efficiently. Though, it also requires an initial time investment setting everything up and then maintaining them. Your dotfiles evolve, as such, they are never finished. You gain knowledge and expertise that change your flow or tools. You may switch jobs and your expectations from the dotfiles are suddenly different.&lt;/p&gt;
&lt;p&gt;My dotfiles are still very much focused on macOS. I’m playing around using Linux/Manjaro on a separate partition. The support for Mac hardware is very limited and several manual configurations are required. So it feels natural to incorporate them into my dotfiles.&lt;/p&gt;
&lt;p&gt;I’d like to write a separate blog post about that adventure in the future 😉&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Finally, a website]]></title><description><![CDATA[Why it took two designs and six months to build this site with Gatsby on Divio Cloud, and what I plan to write about here on development, study and teaching.]]></description><link>https://angelo.dini.dev/blog/finally-a-website/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/finally-a-website/</guid><pubDate>Sat, 06 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;look-its-moving-its-alive&quot;&gt;Look! It’s moving. It’s alive. 🧟&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Creating this website was hard. The final result seems &quot;simple&quot; and &quot;clean&quot; but you know, it wasn't.&lt;/p&gt;&lt;p&gt;Development for this website started in December 2019 with the first mockups and drafts. Back then, the website looked quite different. It took roughly two separate designs and 6 months to get to this point. I’ve been able to overcome many obstacles to deliver a website I’m finally happy and proud about. It might still have glitches here or there but hey, nobody’s perfect. 🤷&lt;/p&gt;
&lt;p&gt;It’s not just the visuals that mattered to me but also what’s under the hood. I tried to make the website as fast, intuitive and elegant as possible while still looking good. The devil lies in the details… The website was created using &lt;a href=&quot;https://www.gatsbyjs.org/&quot;&gt;Gatsby&lt;/a&gt;, a very awesome React-based framework, and deployed using &lt;a href=&quot;https://control.divio.com/&quot;&gt;Divio Cloud&lt;/a&gt; to automate most of the boring stuff.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Design is not just what it looks like and feels like. Design is how it works.
&lt;cite&gt;Steve Jobs&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But I didn’t want to create just another website to wither away, it actually should serve a purpose 🥳. Yeah, really! I’m going to start a new chapter in my life by accomplishing a life-long dream of studying computer science. There are many opinions around on how to optimise studying. Ultimately everyone has to find their path. I try to find mine by summarising topics such as development, studying and teaching into interesting blog articles. By doing so I’ll be forced to do additional research, be precise and simplify as much as possible.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you can’t explain it simply, you don’t understand it well enough.
&lt;cite&gt;Albert Einstein&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My goal is to publish at least 1 blog post per month (to my future self: if that doesn’t work out, travel back in time and beat the s**t out of me…). The next blog post will be about my recently completed &lt;a href=&quot;https://github.com/FinalAngel/dotfiles&quot;&gt;dotfiles setup&lt;/a&gt;, so stay tuned and &lt;a href=&quot;/about/&quot;&gt;subscribe to my newsletter&lt;/a&gt;. I promise there will be no spam!&lt;/p&gt;
&lt;p&gt;Thanks to &lt;strong&gt;everyone&lt;/strong&gt; who helped testing while providing valuable feedback along the way. Don’t hesitate to &lt;a href=&quot;/about/&quot;&gt;contact me&lt;/a&gt; if something is not working as intended.&lt;/p&gt;
&lt;p&gt;So long, and thanks for all the 🐟.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item><item><title><![CDATA[Getting your habits in order]]></title><description><![CDATA[How habits form in the basal ganglia, why they are so hard to change, and how the cue, routine and reward loop helps you take back control of your own.]]></description><link>https://angelo.dini.dev/blog/getting-your-habits-in-order/</link><guid isPermaLink="false">https://angelo.dini.dev/blog/getting-your-habits-in-order/</guid><pubDate>Thu, 04 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;how-our-habits-work&quot;&gt;How our habits work&lt;/h2&gt;
&lt;p class=&quot;lead&quot;&gt;Habits shape us, our lives are affected by them every day. Most people don't even recognise the intricate balance they hold over our daily routines. Have you ever considered that your morning coffee is actually not a choice, but rather a habit that enslaves you to pick up a cup in the morning and pour in hot, black, liquid fluid?&lt;/p&gt;&lt;p&gt;These behaviours are encoded inside our basal ganglia, an area which is found deep within the cerebral hemispheres of our brain. This is where our habits, good or bad, are formed and retrieved.&lt;/p&gt;
&lt;h3 id=&quot;change-is-hard&quot;&gt;Change is hard&lt;/h3&gt;
&lt;p&gt;A lot of actions that we do intuitively are automatic, habitual behaviours. Our brain switches to autopilot and doesn’t need to think. This preserves several finite mental resources such as willpower. Willpower plays a key role in our daily driver of getting things done.&lt;/p&gt;
&lt;p&gt;Formed habits do not draw from that pool. This is why it is sometimes so hard to change bad behaviours. You lack the will (power) thereof.&lt;/p&gt;
&lt;p&gt;In our example, this vicious cycle starts with a cue triggered from your morning routine rewarding you with the first accomplishment of the day: a coffee.&lt;/p&gt;
&lt;h3 id=&quot;cue-routine-and-reward&quot;&gt;Cue, routine and reward&lt;/h3&gt;
&lt;p&gt;To grasp the basic concept of this circle, we need to analyse the underlying psychological patterns to be able to influence and ultimately, assert control over our habits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cue&lt;/strong&gt;: The cue is the first step in this process. You can identify the cue through 5 signals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where you were&lt;/li&gt;
&lt;li&gt;What time it was&lt;/li&gt;
&lt;li&gt;The emotional state you were in&lt;/li&gt;
&lt;li&gt;The social connection around you&lt;/li&gt;
&lt;li&gt;What preceded the cue before the habit emerged&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s apply our coffee example to these signals; The cue occurred in the morning, at the time you woke up. You were tired, your partner still in bed and eager to start your day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Routine&lt;/strong&gt;: The routine is normally easily identifiable. This is the process you fall into day by day. In our above example, it’s the process of waking up, getting your motors running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reward&lt;/strong&gt;: That’s the craving that needs to be satisfied. It is imperative to identify the reward before you are able to change your habit. In relation to our example, the reward can be the fresh taste of coffee in the morning or the effect it has throughout your day.&lt;/p&gt;
&lt;h3 id=&quot;improve-one-step-at-a-time&quot;&gt;Improve, one step at a time&lt;/h3&gt;
&lt;p&gt;Identifying the cue is the most difficult puzzle to solve. Besides the mentioned 5 signals, you can analyse the routine you are trapped in or figure out what the reward is.&lt;/p&gt;
&lt;p&gt;‘Till next time!&lt;/p&gt;
</content:encoded></item></channel></rss>