<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kevin Bailey]]></title><description><![CDATA[I'm a web developer who cares deeply about building great UX/DX using tools like TypeScript, Remix, React, etc.]]></description><link>https://kevinabailey.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 10:38:33 GMT</lastBuildDate><atom:link href="https://kevinabailey.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Great Routing Lever: File Based or Config]]></title><description><![CDATA[The other day I stumbled across a poll from Ryan Toronto getting his followers thoughts on whether they prefer file based routing or config based.

Looking at the options I immediately thought there's clearly an option missing.

Let's start with a li...]]></description><link>https://kevinabailey.com/the-great-routing-lever-file-based-or-config</link><guid isPermaLink="true">https://kevinabailey.com/the-great-routing-lever-file-based-or-config</guid><dc:creator><![CDATA[Kevin Bailey]]></dc:creator><pubDate>Fri, 02 Feb 2024 23:39:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706916979399/24f44ef7-e2a7-47be-870c-b9e8cc3df620.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The other day I stumbled across a poll from Ryan Toronto getting his followers thoughts on whether they prefer file based routing or config based.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706917720574/c97c587c-d76a-44fd-968f-cb68cf16a1a5.png" alt class="image--center mx-auto" /></p>
<p>Looking at the options I immediately thought there's clearly an option missing.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706805424193/8c9be81c-2fab-4c3e-b7d4-f0521778ffe7.jpeg" alt="Image of Zoidberg with a meme caption of &quot;why not both?&quot;" class="image--center mx-auto" /></p>
<h2 id="heading-lets-start-with-a-little-of-my-history">Let's start with a little of my history</h2>
<p>I started programming at the age 12 before I even knew that what I was doing was called programming. My first steps into the web were in PHP. You know, I needed to have that "cool" clan website that me and all my other 12 year old friends could post messages on talking about how "cool" we were playing medal of honor. Thus my introduction to file based routing. It was awesome, easy to understand and to find whatever horribly named php file I had that rendered that music filled flash animation intro page. Again, needed to express how "cool" my clan was.</p>
<p>After high school I got my resume out there will all that PHP experience I had built up. I'd get call after call of interviewers interested in me because they liked my resume, but the moment they found out I just finished high school they all just assumed I wouldn't want to work for them. Which lead me to college and learning things like Java, C# and eventually ASP webforms (which was still very much file based). After getting that degree, I started a job at a design agency.</p>
<p>Now I was fresh into my career and ASP.NET MVC 3 was the new hotness. This was when config based routing started getting injected into my veins. There were default conventions for my controllers and actions but as more and more versions of Microsoft's framework were released, there were more and more config options to change things to my liking.</p>
<p>Now, I'll save you 15 years of personal history of using different web technologies and frameworks like .NET, PHP, Knockout, Durandal, Aurelia, Angular and React; and fast forward to being a Remix supporter.</p>
<p>File based routing was back and with it the rush of nostalgia. The memories of being a teenager working on my silly hobby listening to the sweet rhythm of Suga Suga by Baby Bash. Boy did I miss file based routing. There was something so simple about having your URL match the file system. No longer was I pouring through config files to figure out where that button was that I needed to update.</p>
<h2 id="heading-so-which-should-you-choose">So which should you choose?</h2>
<p>It depends, everyone's favorite answer. I love file based routing because for me, it's easier to wrap my head around my routes and find them. File based routing is great for small to medium sized apps, but often, over time as you add feature after feature, file based routing starts to get messy. Which brings me to one of my favorite things about Remix: levers!</p>
<p>There are tradeoffs for just about everything in life. Some are better than others for the situation. Remix gives you tons of levers to pull and push depending on what you need. That includes the ability to pull that lever from "file based routing" to "config based routing".</p>
<p>There is one thing about levers that often gets overlooked: it's not a toggle button, it's not binary, it's not a simple on and off switch. You can take that lever (or as the roller skating DJ side of me likes to think of it more like a slider) and push it as far as you need to in either direction. Is your project starting to get too big and needs some reorganizing? Do it. Want to keep some file based routing? Do it.</p>
<h2 id="heading-some-of-this-some-of-that-and-a-little-of-both">Some of this, some of that, and a little of both</h2>
<p>At work we've got a new shiny SaaS product (+4 years in the making). Overtime we've gone from CRA straight to Remix and slowly converting our pages to use the loaders and actions. This product has what our product team calls "modules" which are different feature sets that our customers can enable or disable. Some of these features are larger than others and it was important to make sure these features didn't accidentally bleed into one another.</p>
<p>We created a custom routing configuration for Remix that would more easily allow us to keep these "modules" separated. Here's what a typical remix project's "app" folder often looks like for our company:</p>
<pre><code class="lang-plaintext">/app
    /components
    /helpers
    /routes
    /utils
</code></pre>
<p>This is fine for smaller projects, but our project needed some separation. Nothing super special about this, but this is what we ended up with:</p>
<pre><code class="lang-plaintext">/app
    /auth
    /core
    /feature-1
    /feature-2
    /portal
    /shared
</code></pre>
<p>Under each of these "module" folders, they each have their own routes, components, helpers and utilities. That way, each "module" can have it's own components that are only related to it's module. Anything that is shared across multiple modules, like a button for example, can be found under the "shared" module. Here's a quick description of what some of these "modules" do that'll hopefully give enough context to how we split things in the code further down:</p>
<ul>
<li><p><strong>auth</strong> - all routes related to handling Azure B2C authentication, signups, invites, etc.</p>
</li>
<li><p><strong>core</strong> - this is the core module that every customer has. It has settings, users, permissions, navigation, etc.</p>
</li>
<li><p><strong>shared</strong> - there are no routes here, this is just a shared place to put more generic components, helpers and utilities.</p>
</li>
<li><p><strong>feature-X</strong> - these are what our product team calls "modules" that differ which are enabled from customer to customer.</p>
</li>
<li><p><strong>portal</strong> - part of our product is helping our customers gather documents and forms from their suppliers. When our customer needs a document from one of their supplier, they create a document upload task. An email gets sent to that supplier with a specialized URL that allows them to complete the task assigned to them easily without having to log in. This module really should probably be a separate site since authentication and the look and feel are very different, but this was a very early feature.</p>
</li>
</ul>
<p>Alright, let's get to some code. Here's our routes config for Remix:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { loadModuleRoutes } <span class="hljs-keyword">from</span> <span class="hljs-string">'./module-routes.js'</span>

<span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('@remix-run/dev').AppConfig}</span> </span>*/</span>
<span class="hljs-keyword">var</span> appConfig = {
  <span class="hljs-attr">routes</span>: <span class="hljs-keyword">async</span> defineRoutes =&gt; {
    <span class="hljs-keyword">return</span> loadModuleRoutes(
      appConfig.appDirectory, 
      [
        { 
          <span class="hljs-attr">path</span>: <span class="hljs-string">'auth'</span> 
        },
        { 
          <span class="hljs-attr">path</span>: <span class="hljs-string">'portal'</span>, 
          <span class="hljs-attr">prefix</span>: <span class="hljs-string">'portal'</span>,
          <span class="hljs-attr">layout</span>: <span class="hljs-string">'portal/routes/__portal-layout'</span>,
        },
        {
          <span class="hljs-attr">path</span>: <span class="hljs-string">'feature-1'</span>,
            <span class="hljs-attr">prefix</span>: <span class="hljs-string">'feature-1'</span>,
          <span class="hljs-attr">rootLayout</span>: <span class="hljs-string">'core/routes/__global-layout'</span>,
          <span class="hljs-attr">layout</span>: <span class="hljs-string">'feature-1/routes/__feature-1-layout'</span>,
        },
        { 
          <span class="hljs-attr">path</span>: <span class="hljs-string">'core'</span>, 
          <span class="hljs-attr">rootLayout</span>: <span class="hljs-string">'core/routes/__global-layout'</span>
        }
      ],
      defineRoutes,
    )
  },
  <span class="hljs-comment">// rest of remix config here...</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> appConfig
</code></pre>
<p>Let's briefly talk about what's going on here. We have a custom <code>module-routes.js</code> file that exports a <code>loadModuleRoutes</code> function. This function takes 3 arguments. First the app directory (aka "app"), second an array of our desired modules, and last is the <code>defineRoutes</code> function provided by Remix. The part that is most interesting is our array of modules, here's the properties:</p>
<ul>
<li><p><strong>path</strong> - this is the folder path to our module that contains file based routing in a routes folder (<code>app/[path]/routes</code>).</p>
</li>
<li><p><strong>prefix</strong> (optional) - this is if we want to prefix the url with a segment. In the example above for our "portal" module, we prefix with "portal" so all routes in that module are under the URL <code>/portal/*</code>. (You'll note that we have multiple modules that don't have prefixes. Core adds things like <code>/users/*</code> and <code>/settings</code> whereas Auth adds <code>/login</code>, <code>/signup</code>, etc.</p>
</li>
<li><p><strong>rootLayout</strong> - most of our modules share the same navigation shell. This property points to what file path is our main layout, the piece that adds all of the styling, navigation, etc.</p>
</li>
<li><p><strong>layout</strong> - some modules need to setup their own layout to populate any providers that they might need. I could have done a pathless layout in the module itself, but we didn't want to clutter the routes folder when we were already halfway configured for it.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🤨</div>
<div data-node-type="callout-text"><strong>rootLayout </strong>and <strong>layout</strong> are slightly confusing we admit. The only reason why <strong>rootLayout</strong> needs to exist is because of that special "portal" route that we honestly think should be it's own standalone site since it doesn't share authentication, global navigation, etc. Hopefully we'll find time in the future to make that happen.</div>
</div>

<p>Now for the <strong>loadModuleRoutes</strong> code. A couple of disclaimers: we wrote this code a year or so ago, future Kevin rarely likes past Kevin's styling of coding so I'm sure there are problems. There are better ways to do things, but I rather finish writing this post than clean this guy up. I've added a few more comments in the code to explain some details.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">One of our feature modules is getting pretty big, so we're also using <code>remix-flat-routes</code> to take advantage of it's <a target="_blank" href="https://github.com/kiliman/remix-flat-routes?tab=readme-ov-file#nested-folders-with-flat-files-convention--new-in-v051">nested folders with flat files convention</a>.</div>
</div>

<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { flatRoutes } <span class="hljs-keyword">from</span> <span class="hljs-string">'remix-flat-routes'</span>

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loadModuleRoutes</span>(<span class="hljs-params">appDirectory, moduleConfigs, defineRoutes</span>) </span>{
    appDirectory = appDirectory || <span class="hljs-string">'app'</span>
    <span class="hljs-keyword">const</span> routes = {}

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> moduleConfig <span class="hljs-keyword">of</span> moduleConfigs) {
        <span class="hljs-comment">// load module routes from `remix-flat-routes`</span>
        <span class="hljs-keyword">const</span> moduleRoutes = flatRoutes(<span class="hljs-string">`<span class="hljs-subst">${moduleConfig.path}</span>/routes`</span>, defineRoutes, {
            <span class="hljs-attr">appDir</span>: appDirectory,
        })

        <span class="hljs-comment">// loop over each route and add parent routes, prefixes, etc.</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> key <span class="hljs-keyword">of</span> <span class="hljs-built_in">Object</span>.keys(moduleRoutes)) {
            <span class="hljs-keyword">const</span> route = moduleRoutes[key]

            <span class="hljs-comment">// if no parentId, default to `root`    </span>
            route.parentId = route.parentId || <span class="hljs-string">'root'</span>

            <span class="hljs-comment">// if our module has a prefix,            </span>
            <span class="hljs-comment">// our current route's parent is root,</span>
            <span class="hljs-comment">// and we aren't the layout</span>
            <span class="hljs-comment">// add the prefix to the route path </span>
            <span class="hljs-keyword">if</span> (
                moduleConfig.prefix &amp;&amp;
                route.parentId === <span class="hljs-string">'root'</span> &amp;&amp;
                route.id !== moduleConfig.layout
            ) {
                <span class="hljs-comment">// if we have path, add the prefix on the front</span>
                <span class="hljs-keyword">if</span> (route.path) {
                    route.path = moduleConfig.prefix + <span class="hljs-string">'/'</span> + route.path
                } <span class="hljs-keyword">else</span> {
                    <span class="hljs-comment">// otherwise we're the index of the module</span>
                    <span class="hljs-comment">// set our path to the prefix  </span>
                    route.path = moduleConfig.prefix
                }
            }

            <span class="hljs-comment">// if our module has a layout defined,</span>
            <span class="hljs-comment">// our parent route is the root and</span>
            <span class="hljs-comment">// we aren't the layout, set our parent to the layout</span>
            <span class="hljs-keyword">if</span> (
                moduleConfig.layout &amp;&amp;
                route.parentId === <span class="hljs-string">'root'</span> &amp;&amp;
                route.id !== moduleConfig.layout
            ) {
                route.parentId = moduleConfig.layout
            }

            <span class="hljs-comment">// if our module has a root layout defined,</span>
            <span class="hljs-comment">// our parent route is the root and</span>
            <span class="hljs-comment">// we aren't the root layout, set our parent to the root layout</span>
            <span class="hljs-keyword">if</span> (
                moduleConfig.rootLayout &amp;&amp;
                route.parentId === <span class="hljs-string">'root'</span> &amp;&amp;
                route.id !== moduleConfig.rootLayout
            ) {
                route.parentId = moduleConfig.rootLayout
            }
        }

        <span class="hljs-comment">// merge our module routes to our full routes object</span>
        mergeRoutes(moduleRoutes, routes)
    }

    <span class="hljs-keyword">return</span> routes
}
</code></pre>
<p>Hopefully the comments in that file make it clear what we're doing. This is a great way to get the best of both worlds. Being able to have some extra configuration to break things up into feature modules that our product team wants, but still be able to take advantage of file based routing with co-located components.</p>
<p>Pull the levers that help you solve the problem you have.</p>
]]></content:encoded></item><item><title><![CDATA[Delete Code with Knip]]></title><description><![CDATA[One of my favorite things to do as a software developer is delete code. Nothing is more satisfying than deleting hundreds of thousands of lines of code that are no longer needed in your application. Even if it's only 5 lines of code, I still get all ...]]></description><link>https://kevinabailey.com/delete-code-with-knip</link><guid isPermaLink="true">https://kevinabailey.com/delete-code-with-knip</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Kevin Bailey]]></dc:creator><pubDate>Tue, 31 Oct 2023 19:13:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698779518104/5c32d7cf-851d-4dcf-b14a-d4465a3755ea.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of my favorite things to do as a software developer is delete code. Nothing is more satisfying than deleting hundreds of thousands of lines of code that are no longer needed in your application. Even if it's only 5 lines of code, I still get all warm and fuzzy from it.</p>
<p>One of the benefits of removing code is you no longer have to maintain it. It's just gone. You'll never ask yourself, "Is this code still needed?" if it doesn't exist. Have you ever opened up your codebase and found some code written way too long ago and wondered if it was safe to remove? It's kind of like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698356850427/19808252-eabc-4efe-9467-e1b98aad7337.jpeg" alt class="image--center mx-auto" /></p>
<blockquote>
<p>"Not sure if it's safe to remove this bit, so we'll leave it there just in case."<br />- some developer</p>
</blockquote>
<p>Now the magic question is, how do we find code to remove? Insert Knip</p>
<h2 id="heading-what-is-knip">What is Knip?</h2>
<p>Now normally when you are browsing the <s>Twitter</s> 𝕏 developer crowd you typically end up with a bunch of hot takes, arguments over whose framework sucks, blah blah blah. But for every 50 hot takes, occasionally you will land on something useful posted on 𝕏. That's where I first heard about <a target="_blank" href="https://github.com/webpro/knip"><strong>Knip</strong></a>. The creators describe it as:</p>
<blockquote>
<p>"Knip finds <strong>unused files, dependencies and exports</strong> in your JavaScript and TypeScript projects. Less code and dependencies lead to improved performance, less maintenance and easier refactorings."</p>
</blockquote>
<p>Now this isn't the first tool of its kind. Many different languages and frameworks have similar tools to help you find unused code. The trick is helping <strong>Knip</strong> know more about your project so it can help you find all those magical lines of code just waiting to be deleted.</p>
<h1 id="heading-an-existing-complex-project">An Existing Complex Project</h1>
<p>For the rest of this article, I'll talk about the steps I've taken on a project at work to get it up and running using <strong>Knip</strong>. Due to the private nature of the project, I'll be focusing more on <strong>Knip</strong> and less on the decisions that were made on the project itself.</p>
<h2 id="heading-initial-setup-and-run">Initial Setup and Run</h2>
<p>First thing first, let's install it as a dev dependency, add it as a script in our package.json and just run it and see what happens.</p>
<p>Install Knip:</p>
<pre><code class="lang-powershell">npm install knip -<span class="hljs-literal">-save</span><span class="hljs-literal">-dev</span>
</code></pre>
<p>Updated our <code>package.json</code> scripts:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"knip"</span>: <span class="hljs-string">"knip"</span>
  }
}
</code></pre>
<p>Now let's run it and see what happens:</p>
<pre><code class="lang-powershell">npm run knip
</code></pre>
<p>Now if you have any sizable project, once this finishes turning the numbers, you'll likely get a giant list of unused things to delete. Our project had so many things that my terminal in vscode wouldn't even show me the whole thing. It was massive.</p>
<h2 id="heading-false-positives-amp-default-configuration">False Positives &amp; Default Configuration</h2>
<p>Skimming the list of what has been reported by <strong>Knip</strong>, I learned quickly that a huge amount of the findings were NOT safe to delete. In fact, tons of what was shown there were things I knew were being used everywhere. Let's check out the <a target="_blank" href="https://github.com/webpro/knip#default-configuration">default configuration</a> for <strong>Knip</strong>.</p>
<p>A simplified example of their defaults from their documentation is:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"entry"</span>: [<span class="hljs-string">"index.js"</span>, <span class="hljs-string">"src/index.js"</span>],
  <span class="hljs-attr">"project"</span>: [<span class="hljs-string">"**/*.js"</span>]
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is a simplification of their defaults. There's a lot more to them that you can read <a target="_blank" href="https://github.com/webpro/knip#entry-files">here</a>. They try very hard to automatically detect what frameworks and libraries you're using to help out. They have a very cool <a target="_blank" href="https://github.com/webpro/knip#plugins">plugin system</a> that helps which we'll talk about later in this article.</div>
</div>

<p>Our project at work is built with <a target="_blank" href="https://remix.run/">Remix</a>. If you've used that framework before you'll immediately know that Remix has several entry files: "root.tsx", "entry.client.tsx" and "entry.server.tsx" files along with every single route file.</p>
<p>Fortunately, <strong>Knip</strong> has several plugins that get automatically detected for us and used to make it smarter for understanding your code. It even has a <a target="_blank" href="https://github.com/webpro/knip/tree/main/src/plugins/remix">plugin for Remix</a>.</p>
<p>Their Remix plugin will be enabled when it finds any dependencies or dev dependencies that include <code>@remix-run</code> in the name. If it detects Remix, it will add a whole load of entry files:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"remix"</span>: {
    <span class="hljs-attr">"entry"</span>: [
      <span class="hljs-string">"remix.config.js"</span>,
      <span class="hljs-string">"remix.init/index.js"</span>,
      <span class="hljs-string">"app/root.tsx"</span>,
      <span class="hljs-string">"app/entry.{client,server}.{js,jsx,ts,tsx}"</span>,
      <span class="hljs-string">"app/routes/**/*.{js,ts,tsx}"</span>,
      <span class="hljs-string">"server.{js,ts}"</span>
    ]
  }
}
</code></pre>
<p>This is fantastic if your project doesn't modify any of the default conventions in Remix, it should just work. But what do you do if you've heavily modified your Remix routing conventions? Well, you're in luck because our project does exactly that.</p>
<h2 id="heading-disabling-built-in-plugins">Disabling Built-In Plugins</h2>
<p>Our project at work customizes how routing works in Remix because of its scale and product requirements. Typically with Remix, you'll see an "app" folder that contains other folders like "components", "routes", etc. We've customized our routing to be split into logical modules that our product team has defined. You can think of these are features customers can turn on and off depending on what they pay for. We also have a few modules that are dedicated to authentication, core sections that all customers get, and even shared components that are used across multiple modules.</p>
<p>So an example of our file structure may look roughly like this:</p>
<pre><code class="lang-plaintext">app
├── auth
│   ├── helpers
│   ├── routes
│   └── services
├── core
│   ├── components
│   ├── helpers
│   └── routes
├── module-1
│   ├── components
│   ├── helpers
│   ├── routes
│   └── utils
├── module-2
│   ├── components
│   └── routes
└── shared
    └── compoents
</code></pre>
<p>As you as see, the Remix plugin for Knip isn't exactly what we want, so we need to disable it and configure new entry points for our application. We'll create a <code>knip.json</code> file in the root to use as our config. We'll also add a <code>"$schema"</code> value at the top to help vscode give us some intellisense.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://unpkg.com/knip@2/schema.json"</span>,
  <span class="hljs-attr">"remix"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"entry"</span>: [
    <span class="hljs-string">"server.js"</span>,
    <span class="hljs-string">"remix.config.js"</span>,
    <span class="hljs-string">"app/root.tsx"</span>,
    <span class="hljs-string">"app/entry.{client,server}.tsx"</span>,
    <span class="hljs-string">"app/{auth,core,module-1,module-2}/routes/**/{index,_layout}.{ts,tsx}"</span>,
    <span class="hljs-string">"app/{auth,core,module-1,module-2}/routes/*.{ts,tsx}"</span>,
    <span class="hljs-string">"app/core/routes/__global-layout.tsx"</span>
  ],
  <span class="hljs-attr">"project"</span>: [
    <span class="hljs-string">"remix.config.js"</span>, 
    <span class="hljs-string">"server.js"</span>, 
    <span class="hljs-string">"module-routes.js"</span>, 
    <span class="hljs-string">"app/**/*.{tsx,ts}"</span>
  ],
}
</code></pre>
<p>Let's walk through each section and give a quick summary of what we're doing here.</p>
<hr />
<pre><code class="lang-json">  <span class="hljs-string">"remix"</span>: <span class="hljs-literal">false</span>,
</code></pre>
<p>This disables their built-in Remix plugin.</p>
<hr />
<pre><code class="lang-json">  <span class="hljs-string">"entry"</span>: [
    <span class="hljs-string">"server.js"</span>,
    <span class="hljs-string">"remix.config.js"</span>,
    <span class="hljs-string">"app/root.tsx"</span>,
    <span class="hljs-string">"app/entry.{client,server}.tsx"</span>,
    <span class="hljs-string">"app/core/routes/__global-layout.tsx"</span>
    <span class="hljs-string">"app/{auth,core,module-1,module-2}/routes/*.{ts,tsx}"</span>,
    <span class="hljs-string">"app/{auth,core,module-1,module-2}/routes/**/{index,_layout}.{ts,tsx}"</span>,
  ],
</code></pre>
<p>Here we've defined our new entry files. Most of this is similar to the Remix plugin until we get to the section about our modules and their routes. The other files should be easy to understand so I'll talk about the last 3.</p>
<p><code>app/core/routes/__global-layout.tsx</code></p>
<p>We have a global layout that has the primary navigation for our application that most modules use. We have configurations where an individual module can opt out of the global layout if they want to build some other kind of navigation or perhaps be used as an unauthenticated section.</p>
<p><code>app/{auth,core,module-1,module-2}/routes/*.{ts,tsx}</code></p>
<p>This entry includes any <code>ts</code> or <code>tsx</code> files that are in the root of our routes folder.</p>
<p><code>app/{auth,core,module-1,module-2}/routes/**/{index,_layout}.{ts,tsx}</code></p>
<p>This one is a bit more hairy without some context. To try not to bore you, I'll try to make this quick, we're using <a target="_blank" href="https://github.com/kiliman">Kiliman</a>'s <a target="_blank" href="https://github.com/kiliman/remix-flat-routes">remix-flat-routes</a> for our routing. We love being able to co-locate components, hooks, utilities, and any other files we need that are only used for a specific route. This helps us not bloat our other folders with code that is only used for a single page. Remix v2 by default uses a flat routes convention but we've opted into using Kiliman's flat routes library because of its ability to have "Nested folders" while still enjoying the flat files convention and co-locating our code.</p>
<p>Example using default flat routes with Remix:</p>
<pre><code class="lang-plaintext">routes
├── admin
├── admin._index
├── admin.users._index
├── admin.users.$userId._index
├── admin.users.$userId.edit
├── admin.users.create
├── admin.settings._index
├── some-route
├── some-route._index
├── some-route.nested-item._index
├── some-other
└── some-other._index
</code></pre>
<p>Imagine having other 50+ routes. This is where the "Nested folders" come in. You can simply add group routes in another folder and as long as it's suffixed with "+" it won't be treated as a flat route but a grouping of flat routes.</p>
<p>Example using remix-flat-routes with "Nested folders":</p>
<pre><code class="lang-plaintext">routes
├── admin+
│   ├── _index
│   ├── users._index
│   ├── users.$userId._index
│   ├── users.$userId.edit
│   ├── users.create
│   ├── settings._index
│   └── _layout.tsx
├── some-route+
│   ├── _index
│   ├── nested-item._index
│   └── _layout.tsx
└── some-other+
    ├── _index
    └── _layout.tsx
</code></pre>
<p>Now when I'm looking at our routes, I'm greeted with fewer folders that group my routes in logical ways.</p>
<hr />
<pre><code class="lang-json">  <span class="hljs-string">"project"</span>: [
    <span class="hljs-string">"remix.config.js"</span>, 
    <span class="hljs-string">"server.js"</span>, 
    <span class="hljs-string">"module-routes.js"</span>, 
    <span class="hljs-string">"app/**/*.{tsx,ts}"</span>
  ],
</code></pre>
<p>Our project root has lots of different folders. Several of those folders are strictly for our QA team for them to write automation that is separate from our application. So we're defining all the files we want Knip to check. We've got 3 root files then everything else is every file under our "app" directory.</p>
<p>Let's run Knip again and see if our list is any better. Spoiler alert, it's not yet. We have a MASSIVE list still, many falling under <code>Unused exported types</code> or <code>Unused exported types in namespaces</code>.</p>
<h2 id="heading-unused-exported-types">Unused Exported Types</h2>
<p>397 unused exported types. That's a lot of "unused" exported types. Let's first talk about what this is, why my project has so many of them, and what we can do about it.</p>
<h3 id="heading-what-is-an-unused-exported-type">What is an unused exported type?</h3>
<p>Hopefully, the phrase speaks for itself, but it is a TypeScript <code>type</code> or <code>interface</code> that is being exported, but is never imported into another file.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> FavoriteColor = <span class="hljs-string">'red'</span> | <span class="hljs-string">'blue'</span> | <span class="hljs-string">'green'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Person {
  name: <span class="hljs-built_in">string</span>
  age: <span class="hljs-built_in">number</span>
  favoriteColor: FavoriteColor
}

<span class="hljs-comment">/** 
 * Some other code in the same file, that may or may not reference 
 * our `FavoriteColor` or `Person` types. Note no other files 
 * reference these 2 types.
*/</span>
</code></pre>
<p>If nothing in your code base is importing these types, then they will be listed by knip.</p>
<h3 id="heading-why-does-my-project-have-so-many-of-these">Why does my project have so many of these?</h3>
<p>Our project has a pattern for how we create React components. Here's an example below.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ButtonProps {
  <span class="hljs-comment">// button props here</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Button = <span class="hljs-function">(<span class="hljs-params">props: ButtonProps</span>) =&gt;</span> {
  <span class="hljs-comment">// return button here</span>
}
</code></pre>
<p>When we make a component, we export the component itself and that gets used throughout our application. Along with each component, we define an interface with its properties. We also export this type, in cases where another component that consumes it, wants to use its prop type.</p>
<p>After you've built hundreds of components like this, it makes sense why knip has such a large list of unused exports. Now what to do about it?</p>
<h3 id="heading-what-are-my-options-to-remove-these-unused-exported-types">What are my options to remove these unused exported types?</h3>
<p>The obvious answer is to stop exporting it. Boom problem solved. But that wasn't a solution that I was happy with because I'm a stubborn developer and I exported the type for a reason. That reason was "just in case" a consumer wanted to use it.</p>
<p>So now what? Luckily there are some <a target="_blank" href="https://github.com/webpro/knip#ignore-exports-used-in-file">configuration options</a> we can use with Knip.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"ignoreExportsUsedInFile"</span>: {
    <span class="hljs-attr">"interface"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"type"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"enum"</span>: <span class="hljs-literal">true</span>
  },
}
</code></pre>
<p>You can either set <code>ignoreExportsUsedInFile</code> for everything by setting that property to <code>true</code> or you can opt into certain things being ignored. As you can see from the example above. We've decided to ignore exports used in files for interfaces, types and enums.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>We've made a lot of progress in getting Knip up and going on a fairly large project. We still have some more work to go but we're already starting to see some benefits and finding lots of unused code. You know what unused code means... I get to delete it!</p>
]]></content:encoded></item><item><title><![CDATA[Forms & React's Virtual Dom]]></title><description><![CDATA[I've been working with HTML for over 20 years so I know a thing or two about how it works. When I decided to shift from a full-stack ASP.NET developer to primarily focusing on where I'm the most passionate about, Front End, I picked up React and ran....]]></description><link>https://kevinabailey.com/forms-and-reacts-virtual-dom</link><guid isPermaLink="true">https://kevinabailey.com/forms-and-reacts-virtual-dom</guid><category><![CDATA[React]]></category><dc:creator><![CDATA[Kevin Bailey]]></dc:creator><pubDate>Thu, 12 Oct 2023 21:42:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697147183277/deb5c933-cc5e-413b-a9fa-8cd510eaaaab.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been working with HTML for over 20 years so I know a thing or two about how it works. When I decided to shift from a full-stack ASP.NET developer to primarily focusing on where I'm the most passionate about, Front End, I picked up React and ran. I love React and don't see myself switching to another framework for a long time, especially with all the great stuff the <a target="_blank" href="https://remix.run">Remix</a> team is making with their framework (I guess I'm full stack again? lol), but the other day I ran into something that tripped me up about React's Virtual Dom.</p>
<h2 id="heading-the-scenario">The Scenario</h2>
<p>Let's first lay out the scenario. I was tasked with creating a form, one of the fields in our form was to select an assignee (think of an assignee as an existing contact in your app). When you click a "select assignee" you get a modal dialog with tools to help you find who you are looking for, you select it, then hit the submit button for the modal. This updates the form with the selected assignee.</p>
<p>We have 2 forms, the <strong>main form</strong> on the page, and one in our <strong>dialog form</strong>. In the HTML world, this is pretty straightforward and works fine. The idea is the dialog would be at the bottom of the body, you can submit the <strong>dialog form</strong> and then javascript would take over, validate that form and populate that data in the <strong>main form</strong>.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'hidden'</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'assigneeId'</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'button'</span>&gt;</span>Select Assignee<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- other fields and such --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">dialog</span> <span class="hljs-attr">id</span>=<span class="hljs-string">'select-assignee'</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span>&gt;</span>
        <span class="hljs-comment">&lt;!-- UI to search and find an assignee --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'hidden'</span> <span class="hljs-attr">name</span>=<span class="hljs-string">'assigneeId'</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'submit'</span>&gt;</span>Select<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dialog</span>&gt;</span>
</code></pre>
<blockquote>
<p>"Why isn't the dialog inside the main form? why have 2 forms?" - no one</p>
</blockquote>
<p>When you press the <code>Select Assignee</code> button in the <strong>main form</strong>, we open the dialog. That dialog can be canceled at any time by closing it out by pressing <code>ESC</code>, hitting the <code>X</code> close button or clicking <code>Cancel</code>. We don't want to change the underlying form until they submit the <strong>dialog form</strong>.</p>
<h2 id="heading-react-portals">React Portals</h2>
<p>One of my favorite features of React is how easy it is to create dialogs that render their dom at the bottom of your <code>body</code>. This is pretty standard practice to get all the z-indexing to work nicely and have your dialog be on top of everything on your page. The added benefit here is that your portaled element still has access to all the parent contexts. Portaling something in React, keeps it as a child in React's virtual dom.</p>
<p>Let's look at a striped down version of our <strong>main form</strong> and <strong>dialog form</strong> in React.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MainForm</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> &lt;form&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">'hidden'</span> name=<span class="hljs-string">'assigneeId'</span> /&gt;
        &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">'button'</span>&gt;Select Assignee&lt;/button&gt;
        &lt;DialogForm /&gt;
        {<span class="hljs-comment">/* other fields and such */</span>}
    &lt;/form&gt;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DialogForm</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> React.createPortal(&lt;dialog id=<span class="hljs-string">'select-assignee'</span>&gt;
        &lt;form&gt;    
            {<span class="hljs-comment">/* UI to search and find an assignee */</span>}
            &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">'hidden'</span> name=<span class="hljs-string">'assigneeId'</span> /&gt;
            &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">'submit'</span>&gt;Select&lt;/button&gt;
        &lt;/form&gt;
    &lt;/dialog&gt;, <span class="hljs-built_in">document</span>.body)
}
</code></pre>
<p>If you know the virtual dom well, you've probably already put two and two together. Even though the rendered HTML looks like our first example, React's component tree looks different. Our two forms are still nested.</p>
<pre><code class="lang-plaintext">MainForm
    - form
        - input
        - button
        - DialogForm
            - form
                - input
                - button
</code></pre>
<p>So when you submit the <strong>dialog form</strong>, you are also submitting the <strong>main form</strong>. As someone who has spent 20+ years with HTML, this threw me. I knew about the virtual dom, and I feel like I know a lot more than most React devs. For whatever reason this didn't make sense until I fully isolated it then the lightbulb came on.</p>
<h2 id="heading-solutions">Solutions</h2>
<p>As with all things, there are many ways to potentially fix this. 3 come to mind quickly.</p>
<ol>
<li><p>Move <code>&lt;DialogForm&gt;</code> outside of the <strong>main form</strong>.</p>
</li>
<li><p><code>event.stopPropagation()</code> on the submit handler for the <strong>dialog form.</strong></p>
</li>
<li><p>Don't use a <code>&lt;form&gt;</code> in the <strong>dialog form.</strong></p>
</li>
</ol>
<h3 id="heading-1-move-outside-of-the-main-form">1. Move <code>&lt;DialogForm&gt;</code> Outside of the <strong>Main</strong> Form</h3>
<p>This is a solution and one that will work. In my scenario at work, having more context would tell you this was not an easy option. At work, we create many different form elements. Ranging from <code>&lt;TextField /&gt;</code> to <code>&lt;DatePicker /&gt;</code> to <code>&lt;AssigneeSelector /&gt;</code>. You see where I'm going with this. Our end goal is to allow our developers to build forms quickly that have complicated fields. Building a form is as simple as...</p>
<pre><code class="lang-typescript">&lt;Form&gt;
    &lt;TextField label=<span class="hljs-string">'Title'</span> name=<span class="hljs-string">'title'</span> /&gt;
    &lt;Select label=<span class="hljs-string">'type'</span> name=<span class="hljs-string">'type'</span> /&gt;
    &lt;AssigneeSelector label=<span class="hljs-string">'Assignee'</span> name=<span class="hljs-string">'assigneeId'</span> /&gt;
&lt;/Form&gt;
</code></pre>
<h3 id="heading-2-eventstoppropagation-on-the-submit-handler-for-the-dialog-form">2. <code>event.stopPropagation()</code> on the Submit Handler for the <strong>Dialog Form</strong></h3>
<p>Depending on the complexity of the <strong>dialog form</strong>. This solution works rather well and allows our nested form to work without trying to submit our <strong>main form</strong>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> DialogFormProps {
    open?: <span class="hljs-built_in">boolean</span>
    onSelected: <span class="hljs-function">(<span class="hljs-params">assigneeId: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DialogForm</span>(<span class="hljs-params">{ open, onSelected}: DialogFormProps</span>) </span>{
    <span class="hljs-keyword">const</span> onSubmit = <span class="hljs-function">(<span class="hljs-params">event: React.FormEvent</span>) =&gt;</span> {
        event.preventDefault()
        event.stopPropagation()
        <span class="hljs-comment">// do validation on the form</span>
        onSelected(selectedAssigneeId)
    }

    <span class="hljs-keyword">return</span> React.createPortal(&lt;dialog id=<span class="hljs-string">'select-assignee'</span>&gt;
        &lt;form onSubmit={onSubmit}&gt;    
            {<span class="hljs-comment">/* UI to search and find an assignee */</span>}
            &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">'hidden'</span> name=<span class="hljs-string">'assigneeId'</span> /&gt;
            &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">'submit'</span>&gt;Select&lt;/button&gt;
        &lt;/form&gt;
    &lt;/dialog&gt;, <span class="hljs-built_in">document</span>.body)
}
</code></pre>
<h3 id="heading-3-dont-use-a-in-the-dialog-form">3. Don't use a <code>&lt;form&gt;</code> in the <strong>Dialog Form</strong>.</h3>
<p>There's a lot to be considered with an option like this. It all depends on the complexity of the form in your dialog. Are there lots of fields that need validation to run against? Are you using a 3rd party validation tool that knows how to handle forms? etc.</p>
<p>We have a few of these components that don't use a <code>&lt;form&gt;</code> because the dialog might be a simple list that you scroll through and select a single item from. Not hard to handle a little state and there's virtually no validation needed. Did you select something or not?</p>
<h2 id="heading-final-remarks">Final Remarks</h2>
<p>React's virtual dom is not a perfect 1 to 1 match of what gets rendered in the browser's dom. Portaling elements give you a lot of power to get dialogs, toasts, dropdowns, etc flexibility of where they render in the browser's dom. But at the end of the day, it's React's virtual dom that is handling events just because your HTML forms aren't nested, doesn't mean your React forms aren't.</p>
]]></content:encoded></item></channel></rss>