<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" 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">
    <channel>
        <title>Ahmet Tınastepe</title>
        <link>https://www.tinas.dev/</link>
        <description>Hi, I'm Ahmet. I write about Vue, TypeScript, and the small lessons I pick up while building things for the web.</description>
        <lastBuildDate>Tue, 06 Oct 2026 20:28:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Ahmet Tınastepe</title>
            <url>https://www.tinas.dev/og/site.jpg</url>
            <link>https://www.tinas.dev/</link>
        </image>
        <copyright>© 2026 Ahmet Tınastepe</copyright>
        <atom:link href="https://www.tinas.dev/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[A Wagon for Your State]]></title>
            <link>https://www.tinas.dev/writing/a-wagon-for-your-state</link>
            <guid isPermaLink="false">https://www.tinas.dev/writing/a-wagon-for-your-state</guid>
            <pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How usewagen came to be, one package for reactive, type-safe state in the URL and in browser storage.]]></description>
            <content:encoded><![CDATA[
<p>After publishing <a href="https://npmx.dev/package/qpick" target="_blank" rel="noreferrer">qpick</a>, I started thinking about how I could extend it to storage. The URL and storage both only work with strings, so I wanted to solve the two of them in a single package. Why am I telling you this? The answer is <a href="https://usewagen.dev/" target="_blank" rel="noreferrer">usewagen</a>, but let's unpack the details first.</p>
<p>It all came from the need to use state that lives in the URL or in storage inside my code, in a way that's both reactive and type-safe. I had used plenty of different solutions for that before. One of them read and wrote storage through a proxy, which let me step in between and handle parsing and serializing right there in the proxy.</p>
<p>Using it looked roughly like this:</p>
<div class="language-js"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> SESSION_STORAGE_KEYS</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">@/utils/storage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createStorageManager</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">@/utils/storageManagerFactory</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> sessionStorageManager</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> createStorageManager</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">sessionStorage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> SESSION_STORAGE_KEYS</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  prefix</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">foo:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  placeholderData</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">    lastSessionExpiredShown</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> false</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// If not set, returns the placeholder</span></span>
<span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">console</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">log</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">sessionStorageManager</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">lastSessionExpiredShown</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// false</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// Set a value</span></span>
<span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">sessionStorageManager</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">lastSessionExpiredShown</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> true</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// Delete a value</span></span>
<span class="line"><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">delete</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> sessionStorageManager</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">lastSessionExpiredShown</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// Clear all managed keys</span></span>
<span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">sessionStorageManager</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">clear</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span></code></pre>
</div><p>And here's roughly one of the solutions I came up with for the route:</p>
<div class="language-js"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> computed</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">vue</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> useRoute</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> useRouter</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">vue-router</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> route</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRoute</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> router</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouter</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> showLabels</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> computed</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  get</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> ()</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">    const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> current</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> route</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">show_labels</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    if</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">current</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ===</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> undefined</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">return</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> true</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    return</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> current</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ===</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">true</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  set</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> val</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">    router</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">replace</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ...</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">route</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> show_labels</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> String</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">val</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>That's just an example for a single boolean. When several params had to change at once, I used other approaches as well.</p>
<p>Then <a href="https://vueuse.org/" target="_blank" rel="noreferrer">VueUse</a> came along, and I tried using its composables in my projects. That wasn't enough either, because there were cases where I had to read and write some of this state outside an effect scope. Bit by bit, that's how qpick came about first. Its goal was quite simple. There's state in the route, and I want to read and write it type-safely.</p>
<p>I modeled the approach on <a href="https://nuqs.dev/" target="_blank" rel="noreferrer">nuqs</a>. If you haven't heard of it, I highly recommend taking a look. Anyway, back to our topic.</p>
<p>After using the package for a while, I noticed some problems, and I also wanted the same solution for storage state. Route state, for example, had a race condition where two params written in the same tick could wipe each other out.</p>
<hr>
<p>Before writing usewagen, I spent some time thinking about the API, because qpick already has a parser system inside it, and I wanted the route and storage composables to share that same parser system. My first idea was to publish them as separate packages and combine them, with a core package and adapter packages for Vue, React and so on. That would have worked for storage, but getting it to work for the router would have been tough. Besides, nuqs is already popular in the React ecosystem, and for good reason.</p>
<p>Another thing I wanted was for the parsers in a project to be collected automatically, so they could be used in composables just by their name. At first I had packages like <code>@aioli/core</code>, <code>@aioli/vue-route</code> and <code>@aioli/vue-storage</code> in mind. What's <code>aioli</code>, you ask? A burger sauce, actually 😄. I hadn't put much thought into the name back then. Clearly I was hungry.</p>
<p>The problem was that both adapter packages depended on core and re-exported its parsers, so <code>parseAsBoolean</code>, for example, could be imported from both the router and the storage package. And since both packages depended on core, their versions had to be kept in sync, otherwise core couldn't be deduped and the app could end up with two copies of it. On top of that, I wanted to write a Vite plugin that collects the parsers and makes them available across the project. And of course, the utility functions for creating custom parsers come from core as well.</p>
<hr>
<p>Thinking about all of this, I decided to publish it as a single package. I can't really tell yet whether it will find its place in the community anyway, so I started building it to solve my own problem. It's still split on the inside. The parsers come from <code>usewagen</code>, while the composables and the Vite plugin live under <code>usewagen/router</code>, <code>usewagen/storage</code> and <code>usewagen/vite</code>. <code>vue-router</code> and <code>vite</code> are optional peer dependencies, so you only install what you use.</p>
<p>The goal is actually very simple. There's state in the route or in storage, I want to read and write it type-safely, and I want it to be reactive at the same time. That's exactly where the name came from. <em>Wagen</em> apparently means wagon in German, something to carry your state around in. At first I tried to name it just <code>wagen</code>, but npm's name policy put the brakes on that, so I went with <code>usewagen</code>.</p>
<p>Soo, how do you <em>use</em> usewagen?</p>
<p>It's very simple. Let's look at an example:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> route</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRoute</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> router</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouter</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> page</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> ref</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">Number</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">route</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">page</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> ??</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> 1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">))</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">watch</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> value</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> router</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">replace</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ...</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">route</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> String</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">))</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">watch</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  ()</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> route</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">query</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">  value</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> Number</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> ??</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> 1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">))</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>With usewagen, the same logic can be written like this:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> useRouteState</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen/router</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> page</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">withDefault</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>Custom parsers can be written like this:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> defineParser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">export</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsSlug</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> defineParser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">string</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  parse</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> raw</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> (</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">/</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">^</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">[</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">a-z0-9-</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">]</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">+</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">$</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">/</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">test</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">raw</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">?</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> raw</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> :</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> null</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>And used directly like this:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> slug</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">slug</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsSlug</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>To use them by name, they need to be registered first. This is where the Vite plugin comes in. It collects the parsers in a folder and serves them from a virtual module that you hand to <code>createWagen</code>.</p>
<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-72" id="tab-73" checked><label data-title="vite.config.ts" for="tab-73">vite.config.ts</label><input type="radio" name="group-72" id="tab-74" ><label data-title="main.ts" for="tab-74">main.ts</label></div><div class="blocks">
<div class="language-ts active"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> usewagen</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen/vite</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> defineConfig</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">vite</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">export</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> default</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> defineConfig</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  plugins</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> [</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">usewagen</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> dirs</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">src/parsers</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)]</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createWagen</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parsers</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">virtual:usewagen/parsers</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createApp</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">vue</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> App</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">./App.vue</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> wagen</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> createWagen</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parsers</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">createApp</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">App</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">use</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">wagen</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">mount</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">#app</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div></div></div>
<p>After that, the name is enough:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// by name</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> slug</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">slug</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> name</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">parseAsSlug</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// with a default value (parseAsSort being another parser in src/parsers)</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> sort</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">sort</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> name</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">parseAsSort</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> defaultValue</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">asc</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>You can also create as many scoped storages as you like:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createLocalStorage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createMemoryStorage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createSessionStorage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen/storage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> local</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> createLocalStorage</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> prefix</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">app:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> memory</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> createMemoryStorage</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span></code></pre>
</div><p>Maybe you need a storage for tests? Here it is! <code>createMemoryStorage</code> keeps everything in a <code>Map</code>, and you can hand it to a single state or to <code>createWagen</code> for the whole app. And if you need a store of your own, <code>createStorage</code> takes four functions and gives you the same API:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> createStorage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">usewagen/storage</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> store</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> new</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> Map</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">string</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> string</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> shared</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> createStorage</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">shared</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  getItem</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> key</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> store</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">get</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">key</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">??</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> null</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  setItem</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> void</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> store</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">set</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> value</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  removeItem</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> key</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> void</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> store</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">delete</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">key</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  keys</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> ()</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> [</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">...</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">store</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">keys</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()]</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  prefix</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">app:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>And that's how usewagen was born. If I'm honest, it's less an invention than a reunion of every solution I'd tried before, rethought and finally heading in the same direction.</p>
]]></content:encoded>
            <author>Ahmet Tınastepe</author>
        </item>
        <item>
            <title><![CDATA[How Five Minutes at MadVue Reshaped qpick's API]]></title>
            <link>https://www.tinas.dev/writing/how-five-minutes-at-madvue-reshaped-qpick-api</link>
            <guid isPermaLink="false">https://www.tinas.dev/writing/how-five-minutes-at-madvue-reshaped-qpick-api</guid>
            <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How a five-minute conversation at MadVue led to removing eq from qpick's parser system and embracing idempotency.]]></description>
            <content:encoded><![CDATA[
<p>Last week I attended <a href="https://madvue.es/" target="_blank" rel="noreferrer">MadVue</a> in Madrid. The talks covered everything from AI agent strategies to Vite 8, Oxc, module federation, and WebRTC. A full day well spent. But this post isn't about any of those.</p>
<p>It's about a five-minute conversation that changed how <a href="https://github.com/tinas/qpick" target="_blank" rel="noreferrer">qpick</a> works.</p>
<h2 id="the-conversation" tabindex="-1">The Conversation </h2>
<p>I'd been wanting to get feedback on qpick from <a href="https://bsky.app/profile/esm.dev" target="_blank" rel="noreferrer">Eduardo</a> (the author and maintainer of vue-router and pinia). He pulled up the docs, read through them for a few minutes, and pointed at something I'd completely overlooked: <strong>idempotency</strong>.</p>
<p>I'd spent so much time thinking about reactivity that I missed something equally fundamental to a URL parser library. When you parse a value from the URL and serialize it back, you should get the same string. Always. And if you parse <em>that</em> string again, you should get the same value. No matter how many times you repeat the cycle.</p>
<h2 id="what-the-spec-says" tabindex="-1">What the Spec Says </h2>
<p>If you look at the <a href="https://url.spec.whatwg.org/#goals" target="_blank" rel="noreferrer">URL Standard's goals</a>, idempotency is stated explicitly:</p>
<blockquote>
<p>Ensure the combination of parser, serializer, and API guarantee idempotence. For example, a non-failure result of a parse-then-serialize operation will not change with any further parse-then-serialize operations applied to it. Similarly, manipulating a non-failure result through the API will not change from applying any number of serialize-then-parse operations to it.</p>
</blockquote>
<p>A simple way to think about this: a function is idempotent when applying it twice gives the same result as applying it once.</p>
<div class="language-"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>f(f(x)) = f(x)</span></span></code></pre>
</div><p>For a URL parser, the equivalent guarantee is:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">parse</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">serialize</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">parse</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">str</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">))) </span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">===</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> parse</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">str</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>No matter how many round-trips you do, the value stabilizes after the first successful parse.</p>
<h2 id="removing-eq" tabindex="-1">Removing <code>eq</code> </h2>
<p>The first thing I did was remove the <code>eq</code> function from the parser config:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium has-diff" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">export</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> type</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> ParserConfig</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">></span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  parse</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> string</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T </span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">|</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> null</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  serialize</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> string</span></span>
<span class="line diff remove"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  eq</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">?:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">a</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> b</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> boolean </span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span></span></code></pre>
</div><p>The <code>eq</code> function existed for one reason: deciding whether a parameter should be cleared from the URL (i.e., whether the current value equals the default). But once you enforce idempotent serialization, you don't need a separate equality function. Two values are &quot;equal&quot; for URL purposes if and only if they serialize to the same string:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium has-diff" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">function</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> shouldClear</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">>(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">parser</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> Parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">>,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> value</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> clearOnDefault</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> boolean</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> boolean </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">  // ...</span></span>
<span class="line diff remove"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">  const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> eq</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">eq</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> ??</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> (</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">a</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> b</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> T</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> a</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ===</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> b</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span></span>
<span class="line diff remove"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">  return</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> eq</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> def</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span></span>
<span class="line diff add"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">  return</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">serialize</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">===</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">serialize</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">def</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span></span></code></pre>
</div><p>This is simpler and more correct. If your serializer is idempotent, comparing serialized output is the canonical way to check equality. No custom <code>eq</code> needed.</p>
<h2 id="the-new-api" tabindex="-1">The New API </h2>
<p>I also renamed <code>createParser</code> to <code>defineParser</code> to align with Vue ecosystem conventions (<code>defineComponent</code>, <code>defineStore</code>, <code>defineEmits</code>...). Small change, but it makes the API feel more at home in a Vue project.</p>
<p>Here's what a custom parser looks like in alpha-2:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">type</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> HexColor</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> number</span><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic"> // 0x000000–0xFFFFFF</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsHexColor</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> defineParser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">HexColor</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  parse</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">v</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">    const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parsedValue</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> Number</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">parseInt</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">v</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> 16</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    if</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> (</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">Number</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">isNaN</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">parsedValue</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">      return</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> null</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">    }</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    return</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parsedValue</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  serialize</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> v</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> v</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">toString</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">16</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">padStart</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">6</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">0</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// ?color=ff5733 → 16734003</span></span></code></pre>
</div><p>No <code>eq</code> to define. The only contract you need to uphold is that <code>parse</code> and <code>serialize</code> are inverses of each other, so that round-tripping always produces stable results.</p>
<h2 id="why-conferences-still-matter" tabindex="-1">Why Conferences Still Matter </h2>
<p>This change came from a five-minute hallway conversation. I could have stared at the code for weeks and not seen it, because I was too close to it, thinking in terms of reactivity and Vue internals instead of stepping back to ask &quot;what guarantees should a URL parser provide?&quot;</p>
<p>That's what events like MadVue give you. A different set of eyes, a different frame of reference, and access to people who've been thinking about these problems longer than you have. Oh, and apparently we were the first Turkish attendees in MadVue's history. 😅</p>
<p>You can find all the <strong>alpha-2</strong> changes on <a href="https://github.com/tinas/qpick/blob/main/CHANGELOG.md#v100-alpha2" target="_blank" rel="noreferrer">GitHub</a>. If you're using qpick, I'd love to hear how the new API feels.</p>
]]></content:encoded>
            <author>Ahmet Tınastepe</author>
        </item>
        <item>
            <title><![CDATA[The URL was always the state, we just kept ignoring it]]></title>
            <link>https://www.tinas.dev/writing/the-url-was-always-the-state-we-just-kept-ignoring-it</link>
            <guid isPermaLink="false">https://www.tinas.dev/writing/the-url-was-always-the-state-we-just-kept-ignoring-it</guid>
            <pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A new composable for syncing URL parameters with reactive state in Vue.]]></description>
            <content:encoded><![CDATA[
<p>Every Vue app eventually has <em>that</em> moment. You build a product listing page with filters, sorting, pagination, the works. It feels great. Then someone shares the link, the other person opens it, and everything is gone. All that carefully managed state lived in a <code>ref()</code> somewhere in memory, not in the URL where it actually belonged.</p>
<p>So you do the responsible thing: you start syncing state with <code>route.query</code>. You write a watcher. Then another one. Then you realize <code>route.query.page</code> is a string and your pagination logic just did <code>&quot;2&quot; + 1 = &quot;21&quot;</code>. You add <code>parseInt</code>. A fallback. Another watcher for the back button. Forty lines later, you have a working page parameter and you haven't even started on <code>sort</code>.</p>
<p>To be fair, the title is intentionally dramatic. URL is not where all app state should live: it only carries strings, and browsers/servers impose practical URL length limits. So the right target is shareable, navigable UI state (like filters, sorting, pagination), not large or sensitive data.</p>
<p>I got tired of writing that code. So I wrote <a href="https://github.com/tinas/qpick" target="_blank" rel="noreferrer">qpick</a></p>
<h2 id="what-s-in-a-name" tabindex="-1">What's in a Name? </h2>
<p>The name comes from <strong>q</strong> (query) and <strong>pick</strong>, as in cherry-picking the exact parameters you need from the URL. I wanted something short enough that you wouldn't resent typing it in every import statement, and honest enough that you'd know what it does before reading a single line of documentation. You pick query params, you get reactive state.</p>
<h2 id="so-what-does-it-actually-do" tabindex="-1">So What Does It Actually Do? </h2>
<p>qpick gives you a single composable, <code>useRouteState</code>, that turns URL parameters into writable, type-safe, reactive refs. If you've ever used <code>ref()</code> in Vue, you already know how to use qpick. The only difference is that the source of truth lives in the address bar instead of component memory.</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium has-highlighted" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">import</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> useRouteState</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> from</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">qpick</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span></span>
<span class="line"></span>
<span class="line highlighted"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> page</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// page.value is a number. Always. Never null, never "2", never undefined.</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// Setting page.value = 3 navigates to ?page=3.</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// Hitting the back button sets it back. Reactively.</span></span></code></pre>
</div><p>That's not pseudocode, it's the actual API. No setup ceremony beyond what you already have with <code>vue-router</code>, no plugin registration required.</p>
<h2 id="why-i-built-this" tabindex="-1">Why I Built This </h2>
<p>I went looking for something like this in the Vue ecosystem and came up mostly empty. The React world has <a href="https://nuqs.47ng.com/" target="_blank" rel="noreferrer">nuqs</a>, which nails this problem with a clean, composable-driven approach. Vue had router utilities, state management libraries that could optionally sync with the URL, but nothing that treated URL parameters as first-class reactive primitives with the same ergonomics you'd expect from a plain <code>ref()</code>.</p>
<p>I wanted something that feels like writing normal Vue code, with an API that looks and behaves like JavaScript you already know. <code>parseAsInteger</code>, <code>parseAsFloat</code>, <code>parseAsBoolean</code>... if you've used <code>parseInt</code> or <code>parseFloat</code> before, the naming and mental model are immediately familiar. Declare what you need from the URL, get a ref back, wire it up with <code>v-model</code>, and move on with your day.</p>
<h2 id="parsers-or-how-to-stop-worrying-about-strings" tabindex="-1">Parsers, or: How to Stop Worrying About Strings </h2>
<p>The thing that makes all of this work is the parser system. URLs only speak strings, but your app thinks in numbers, booleans, arrays, dates, and union types. Parsers handle the translation in both directions, and they carry their types with them:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> search</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">q</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// string | null</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> page</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// number, parsed, typed, guaranteed non-null</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> sort</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">sort</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> parseAsStringLiteral</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">([</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">price</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">name</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">rating</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">])</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">price</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// 'price' | 'name' | 'rating', narrowed at the type level</span></span></code></pre>
</div><p>Every parser has a <code>.default()</code> method that does two things at once: it guarantees a non-null type so you can stop writing <code>page ?? 1</code> everywhere, and it keeps the URL clean by stripping the parameter when the value matches the default. A page in its default state gets a clean URL, no <code>?page=1&amp;sort=price&amp;inStock=true</code> clutter.</p>
<p>qpick ships with parsers for the common cases like <code>parseAsFloat</code>, <code>parseAsBoolean</code>, <code>parseAsDate</code>, <code>parseAsArrayOf</code>, and <code>parseAsJson</code>, but the one I find myself reaching for constantly is <code>parseAsStringLiteral</code>. It gives you runtime validation and TypeScript type narrowing from a single declaration. If someone manually types <code>?sort=banana</code> into the address bar, the value quietly falls back to the default instead of propagating nonsense through your app.</p>
<p>When the built-ins don't cover your case, <code>createParser</code> lets you bring your own types:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsPriceRange</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> createParser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">&#x3C;{</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> min</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> number</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">;</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> max</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> number </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}></span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  parse</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">    const</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> [</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">min</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> max</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">]</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">split</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">-</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">map</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">Number</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    return</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> Number</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">isNaN</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">min</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">||</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> Number</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">isNaN</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">max</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C">?</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> null</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> :</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> min</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> max</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  serialize</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    return</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> `${</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">min</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">-</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">${</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">value</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">max</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}`</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">  eq</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">a</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> b</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">    return</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> a</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">min</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ===</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> b</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">min</span><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934"> &#x26;&#x26;</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> a</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">max</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> ===</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> b</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">max</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#928374;--shiki-light-font-style:italic;--shiki-dark:#928374;--shiki-dark-font-style:italic">// ?price=50-200 → { min: 50, max: 200 }</span></span></code></pre>
</div><p>Same pattern, same guarantees, your own domain types.</p>
<div class="info custom-block"><p class="custom-block-title">Update (May 2026)</p>
<p>In qpick alpha-2, <code>createParser</code> has been renamed to <code>defineParser</code> and the <code>eq</code> option has been removed in favor of idempotent serialization. Equality is now determined by comparing serialized output (<code>serialize(a) === serialize(b)</code>), which is both simpler and more correct. See <a href="https://www.tinas.dev/writing/how-five-minutes-at-madvue-reshaped-qpick-api">How Five Minutes at MadVue Reshaped qpick's API</a> for the full story behind this change.</p>
</div>
<h2 id="more-than-one-parameter" tabindex="-1">More Than One Parameter </h2>
<p>Most pages you'll work on have several URL parameters that need to stay in sync. qpick handles this by accepting an array of configs and returning an object of refs:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019">const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> filters</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> useRouteState</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">([</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  {</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">q</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsString</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">''</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">},</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  {</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">},</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  {</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">sort</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> parseAsStringLiteral</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">([</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">price</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">name</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">rating</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">])</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">price</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">},</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  {</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">categories</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> parseAsArrayOf</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">parseAsString</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">([]) </span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">},</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">])</span></span></code></pre>
</div><p>Each key becomes a reactive ref: <code>filters.q.value</code>, <code>filters.page.value</code>, and so on. But here's the part that matters. When your user types a new search query, you probably want to reset to page 1. If you set <code>filters.q.value</code> and <code>filters.page.value</code> separately, that's two router navigations, two history entries, and a flash of intermediate state that nobody asked for. So instead:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">filters</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">set</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> q</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">headphones</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2"> page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B"> 1</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> }</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><p>One navigation. One history entry. No intermediate flicker. There's also <code>filters.reset()</code> to restore everything to defaults, and <code>filters.toObject()</code> when you need a plain object for an API call:</p>
<div class="language-ts"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">watch</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  ()</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> filters</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">toObject</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">()</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#076678;--shiki-dark:#83A598">  params</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> =></span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> {</span></span>
<span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">    fetchProducts</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#076678;--shiki-dark:#83A598">params</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">  },</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div><h2 id="configs-that-travel-between-components" tabindex="-1">Configs That Travel Between Components </h2>
<p>If you've used <a href="https://tanstack.com/query" target="_blank" rel="noreferrer">TanStack Query</a>, the pattern here might feel familiar. TanStack Query encourages you to define query options in a shared file so that any component can reference the same query without duplicating configuration. qpick borrows that idea for URL state: define your configs once, import them wherever you need them, and let the shared source of truth (in this case, the URL) keep everything in sync.</p>
<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-90" id="tab-91" checked><label data-title="composables/states.ts" for="tab-91">composables/states.ts</label></div><div class="blocks">
<div class="language-ts active"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">export</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> searchState</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> routeStateOptions</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">search</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsString</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">''</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  urlKey</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">q</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9D0006;--shiki-dark:#FB4934">export</span><span style="--shiki-light:#AF3A03;--shiki-dark:#FE8019"> const</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> pageState</span><span style="--shiki-light:#427B58;--shiki-dark:#8EC07C"> =</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F"> routeStateOptions</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">{</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  key</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984"> '</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26">page</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">'</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">  parser</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">:</span><span style="--shiki-light:#076678;--shiki-dark:#83A598"> parseAsInteger</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">.</span><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">default</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">(</span><span style="--shiki-light:#8F3F71;--shiki-dark:#D3869B">1</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">,</span></span>
<span class="line"><span style="--shiki-light:#7C6F64;--shiki-dark:#A89984">}</span><span style="--shiki-light:#3C3836;--shiki-dark:#EBDBB2">)</span></span></code></pre>
</div></div></div>
<p>Now <code>ProductList.vue</code> and <code>ProductPagination.vue</code> can both import <code>pageState</code>, call <code>useRouteState(pageState)</code>, and they stay in sync automatically. Not through a store or provide/inject or an event bus, but because they both read from and write to the same URL. Change the page in one component, the other one reacts. That's just how refs work when they share a source of truth.</p>
<h2 id="the-small-things-that-add-up" tabindex="-1">The Small Things That Add Up </h2>
<p>Beyond the core API, there are a few design decisions that ended up mattering more than I expected in daily use.</p>
<p><code>urlKey</code> remapping lets your code use <code>filters.search.value</code> while the URL shows <code>?q=...</code>. You get developer-friendly names in your components and user-friendly (or API-friendly) names in the address bar, without any disconnect between the two.</p>
<p>Path parameters work too. If your route is <code>/products/:id</code>, you can use <code>{ key: 'id', parser: parseAsInteger }</code> and qpick reads from <code>route.params</code> automatically. You can mix path and query params in the same <code>useRouteState</code> call, each with its own parser and default.</p>
<p>History mode is configurable per parameter. A search input should probably use <code>replace</code> since nobody wants 50 history entries from typing a word. A page change should use <code>push</code> because you want the back button to work. When you batch-update with <code>set()</code>, qpick resolves conflicts with a simple rule: push wins over replace, because losing a history entry is worse than gaining an extra one.</p>
<p>And the plugin is entirely optional. If you have <code>vue-router</code>, <code>useRouteState</code> works out of the box. The plugin only exists for cases where you want to override the global defaults.</p>
<h2 id="give-it-a-spin" tabindex="-1">Give It a Spin </h2>
<div class="language-bash"><button title="Copy code" data-copied="Copied" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes gruvbox-light-medium gruvbox-dark-medium" style="--shiki-light:#3c3836;--shiki-dark:#ebdbb2;--shiki-light-bg:#fbf1c7;--shiki-dark-bg:#282828" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#B57614;--shiki-dark:#FABD2F">pnpm</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26"> add</span><span style="--shiki-light:#79740E;--shiki-dark:#B8BB26"> qpick</span></span></code></pre>
</div><p>The whole thing is one composable, a set of parsers, and a <a href="https://github.com/tinas/qpick" target="_blank" rel="noreferrer">README</a> that should get you running in a few minutes. If you've ever caught yourself manually syncing <code>route.query</code> with a ref and thought &quot;there has to be a better way&quot;, I'd love for you to give it a try.</p>
]]></content:encoded>
            <author>Ahmet Tınastepe</author>
        </item>
    </channel>
</rss>