<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sidney Alcantara</title>
    <description>The latest articles on DEV Community by Sidney Alcantara (@notsidney).</description>
    <link>https://dev.to/notsidney</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F184714%2F7cbadc55-9a82-47ff-a341-7f3fcaa81844.jpg</url>
      <title>DEV Community: Sidney Alcantara</title>
      <link>https://dev.to/notsidney</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/notsidney"/>
    <language>en</language>
    <item>
      <title>How to add hotlink protection to your web fonts with Netlify Edge Functions and Deno</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Fri, 05 May 2023 01:33:22 +0000</pubDate>
      <link>https://dev.to/notsidney/how-to-add-hotlink-protection-to-your-web-fonts-with-netlify-edge-functions-and-deno-1jj6</link>
      <guid>https://dev.to/notsidney/how-to-add-hotlink-protection-to-your-web-fonts-with-netlify-edge-functions-and-deno-1jj6</guid>
      <description>&lt;h3&gt;
  
  
  Because the people crafting quality typefaces deserve to be paid
&lt;/h3&gt;

&lt;p&gt;While working on my redesigned personal website &lt;a href="https://sidney.me/?utm_source=dev.to&amp;amp;utm_medium=Blog&amp;amp;utm_campaign=How+to+add+hotlink+protection+to+your+web+fonts+with+Netlify+Edge+Functions+and%C2%A0Deno"&gt;sidney.me&lt;/a&gt;, I chose to use a paid font, &lt;a href="https://klim.co.nz/" rel="noopener noreferrer"&gt;Klim Type Foundry&lt;/a&gt;’s &lt;em&gt;&lt;a href="https://klim.co.nz/retail-fonts/the-future-mono/" rel="noopener noreferrer"&gt;The Future Mono&lt;/a&gt;&lt;/em&gt;. It was my first time using a paid web font for a project, so I read (skimmed) the &lt;a href="https://klim.co.nz/licences/web-fonts/" rel="noopener noreferrer"&gt;web font licence agreement&lt;/a&gt;. Curiously, it included this clause (emphasis mine):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;3d. Web Fonts File Protection&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You agree to use reasonable measures to ensure the Web Fonts are available only for the process of styling text on Your Website. At a minimum, and by way of illustration not limitation, reasonable measures include a.) preventing unlicensed third-party access, i.e. &lt;strong&gt;hotlinking&lt;/strong&gt; and b.) &lt;strong&gt;not allowing direct download&lt;/strong&gt; of the Fonts unrelated to the process of styling text for Your Website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In simpler terms, you must make sure you deploy the web font files so that no one can paste the URL in their browser to download it and that other sites can’t use the web font by linking to that URL.&lt;/p&gt;

&lt;p&gt;I’ve been using Netlify for years to host my websites, so I searched for a way to do it natively. I came across this &lt;a href="https://answers.netlify.com/t/hotlink-protection-for-web-fonts/2025" rel="noopener noreferrer"&gt;Netlify support forum thread&lt;/a&gt; that unfortunately concluded with, “We don’t really have a solution for this particular problem yet, but it’s one that might be covered in the future. Stay tuned!” In March 2020.&lt;/p&gt;

&lt;p&gt;The replies did have some suggestions, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using Netlify Functions (overkill for this use case and potentially slow) to check the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer" rel="noopener noreferrer"&gt;&lt;code&gt;Referer&lt;/code&gt; header&lt;/a&gt; in the HTTP request,&lt;/li&gt;
&lt;li&gt;using a more performant Cloudflare Worker to &lt;a href="https://developers.cloudflare.com/workers/examples/hot-link-protection" rel="noopener noreferrer"&gt;do that check&lt;/a&gt; (but I don’t want to sign up for another service), or&lt;/li&gt;
&lt;li&gt;serving the fonts from an S3 bucket or separate Apache server (breaking the whole point of the serverless stack) and using &lt;code&gt;.htaccess&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the last reply on that thread, Netlify has released &lt;a href="https://www.netlify.com/blog/announcing-serverless-compute-with-edge-functions/" rel="noopener noreferrer"&gt;Edge Functions&lt;/a&gt;. They run JavaScript or TypeScript code using &lt;a href="https://deno.land/" rel="noopener noreferrer"&gt;Deno&lt;/a&gt;, the hot new JS runtime meant to succeed Node, on CDN servers that are much closer to users than where typical serverless functions run. In other words: fast, fast, fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Setting up Edge Functions on your Netlify project is incredibly simple. Start by creating a &lt;code&gt;netlify/edge-functions&lt;/code&gt; directory and making a JS or TS file in that. Then, export a default function for Netlify to run. Let’s call it &lt;code&gt;hotlink-protection.ts&lt;/code&gt; and return “Hello world” as a basic response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// netlify/edge-functions/hotlink-protection.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we need to specify which routes this function should run on. In this case, we can limit it to font files. I only serve fonts in the WOFF2 format, as &lt;a href="https://docs.netlify.com/edge-functions/get-started/" rel="noopener noreferrer"&gt;browser support is excellent&lt;/a&gt;. In the root of your project, create a file called &lt;code&gt;netlify.toml&lt;/code&gt;, and set the path and the function name to be the same as the file name you chose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# netlify.toml&lt;/span&gt;

&lt;span class="nn"&gt;[[edge_functions]]&lt;/span&gt;
    &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/*.woff2"&lt;/span&gt;
    &lt;span class="py"&gt;function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hotlink-protection"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While we’re here, let’s add a cache header for our fonts. They’re immutable, static assets that won’t change: we need to specify that to the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# netlify.toml&lt;/span&gt;

&lt;span class="nn"&gt;[[headers]]&lt;/span&gt;
    &lt;span class="py"&gt;for&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/*.woff2"&lt;/span&gt;
    &lt;span class="nn"&gt;[headers.values]&lt;/span&gt;
        &lt;span class="py"&gt;Cache-Control&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"public, max-age=31536000, immutable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s get it working. We’re effectively creating middleware that modifies and returns the response—either the font file itself or a 403 Forbidden HTTP error. Edge Functions receive two arguments, &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;context&lt;/code&gt;. We’ll need to &lt;code&gt;await&lt;/code&gt; the response (the font data) using &lt;code&gt;context&lt;/code&gt;, so let’s change it to an &lt;code&gt;async&lt;/code&gt; function. Here’s the definition in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://edge.netlify.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, let’s get the &lt;code&gt;Referer&lt;/code&gt; header from &lt;code&gt;request&lt;/code&gt; and use a regular expression to test that it’s coming from our website. If there’s no &lt;code&gt;Referer&lt;/code&gt; or it doesn’t match, return a 403.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://edge.netlify.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;referer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;referer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\.)?&lt;/span&gt;&lt;span class="sr"&gt;sidney&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;me&lt;/span&gt;&lt;span class="se"&gt;(\/&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That regex looks gnarly, but most of its special characters are used to escape the special characters &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;.&lt;/code&gt;, which are also part of URLs. It matches &lt;code&gt;sidney.me&lt;/code&gt; and any subdomains and directories, so make sure to change those characters (excluding the &lt;code&gt;\.&lt;/code&gt; in between) when copy-pasting this snippet. For an explanation of the regex, check it out on &lt;a href="https://regex101.com/r/Vs3fbH/1" rel="noopener noreferrer"&gt;regex101.com&lt;/a&gt;. The top-right panel highlights what each character does.&lt;/p&gt;

&lt;p&gt;Then to return the font file as-is, we can use &lt;code&gt;await context.next()&lt;/code&gt; to get the next HTTP response. Here’s the &lt;strong&gt;final code&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// netlify/edge-functions/hotlink-protection.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://edge.netlify.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;referer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;referer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\.)?&lt;/span&gt;&lt;span class="sr"&gt;sidney&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;me&lt;/span&gt;&lt;span class="se"&gt;(\/&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can commit your changes and deploy your project to Netlify as usual. Once deployed, navigating to the font URL should display “Forbidden”.&lt;/p&gt;

&lt;p&gt;Note: if you want your fonts to work in &lt;strong&gt;branch deploys&lt;/strong&gt;, you’ll need a separate check for your Netlify domain name. In my case, it’s &lt;code&gt;sidney-me.netlify.app&lt;/code&gt; and the branch name and &lt;code&gt;--&lt;/code&gt; is prefixed: &lt;code&gt;branch-name--sidney-me.netlify.app&lt;/code&gt;. Here’s my regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prodRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\.)?&lt;/span&gt;&lt;span class="sr"&gt;sidney&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;me&lt;/span&gt;&lt;span class="se"&gt;(\/&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;devRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;sidney-me&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;netlify&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;app&lt;/span&gt;&lt;span class="se"&gt;(\/&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prodRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;devRegex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Subset your font files for additional protection and a performance boost
&lt;/h2&gt;

&lt;p&gt;Of course, this protection itself isn’t bulletproof. Someone can easily spoof the &lt;code&gt;Referer&lt;/code&gt; header or use DevTools to download the font files. The font data must be sent to users for their browsers to display the font. Once it’s on the user’s machine, there will always be a way for them to extract the font file.&lt;/p&gt;

&lt;p&gt;One way to mitigate this is to subset the font files only to include the characters you use on your site, excluding characters for other languages or writing systems. Also, fonts typically have &lt;a href="https://www.adobe.com/products/type/opentype.html" rel="noopener noreferrer"&gt;OpenType features&lt;/a&gt; like &lt;a href="https://fonts.google.com/knowledge/introducing_type/introducing_alternate_glyphs" rel="noopener noreferrer"&gt;alternate characters&lt;/a&gt;, primarily for stylistic purposes, and we can follow &lt;a href="https://github.com/google/fonts/issues/1582#issuecomment-394168818" rel="noopener noreferrer"&gt;Google Fonts’ example&lt;/a&gt; in removing them from the files we serve.&lt;/p&gt;

&lt;p&gt;For my site, I used &lt;code&gt;pyftsubset&lt;/code&gt; from FontTools to subset. Follow &lt;a href="https://github.com/fonttools/fonttools#installation" rel="noopener noreferrer"&gt;these instructions&lt;/a&gt; to install (note that you’ll need Python installed too). Here’s an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyftsubset the-future-mono-regular.woff2 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--output-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"the-future-mono-regular--subset.woff2"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--flavor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;woff2 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--unicodes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command subsets to the Latin character set and some punctuation symbols for a total of 385 characters. It also removes discretionary OpenType features as described in the &lt;a href="https://fonttools.readthedocs.io/en/latest/subset/index.html#glyph-set-expansion" rel="noopener noreferrer"&gt;subset docs&lt;/a&gt;. In my case, this more than halved the size of the font file, from 30.1 kB to 14.7 kB. This can add up and improve your Lighthouse performance scores.&lt;/p&gt;

&lt;p&gt;Then specify the Unicode range in your CSS to help the browser use an appropriate fallback font if necessary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"The Future Mono"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("the-future-mono-regular--subset.woff2")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;unicode-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;0000-00&lt;/span&gt;&lt;span class="n"&gt;FF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;0131&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;0152-0153&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;02&lt;/span&gt;&lt;span class="n"&gt;BB-02BC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;02&lt;/span&gt;&lt;span class="n"&gt;C6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;02&lt;/span&gt;&lt;span class="n"&gt;DA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;02&lt;/span&gt;&lt;span class="n"&gt;DC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2000-206&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2074&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="n"&gt;AC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2122&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2191&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2193&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2212&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="m"&gt;2215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="n"&gt;FEFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="n"&gt;FFFD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Serve fonts from a different project
&lt;/h2&gt;

&lt;p&gt;If you’re publishing your site’s code as open-source, make sure &lt;em&gt;not&lt;/em&gt; to include your font files in that repo. You don’t want to be part of the problem of GitHub becoming the &lt;a href="https://pixelambacht.nl/2017/github-font-piracy/" rel="noopener noreferrer"&gt;web’s largest font piracy site&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add your font files to your &lt;code&gt;.gitignore&lt;/code&gt; file with &lt;code&gt;*.woff2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you have already publicly published the repo, permanently remove the files from history (&lt;a href="https://pixelambacht.nl/2017/github-font-piracy/#fixing-the-problem" rel="noopener noreferrer"&gt;instructions&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Then, deploy your font files in a new &lt;strong&gt;private repository&lt;/strong&gt; as a separate Netlify project.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can keep the files in your &lt;strong&gt;local repository&lt;/strong&gt; for testing and leave your CSS unchanged, pointing to files in the same domain name. Then use a &lt;a href="https://docs.netlify.com/routing/redirects/rewrites-proxies/" rel="noopener noreferrer"&gt;rewrite&lt;/a&gt; to point to the other project’s domain name like so:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# netlify.toml&lt;/span&gt;

&lt;span class="nn"&gt;[[redirects]]&lt;/span&gt;
  &lt;span class="py"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/fonts/the-future-mono-*"&lt;/span&gt;
  &lt;span class="py"&gt;to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://your-project.netlify.app/the-future-mono/the-future-mono-:splat"&lt;/span&gt;
  &lt;span class="py"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
  &lt;span class="py"&gt;force&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; you must have the hotlink protection Edge Function on both projects for the matching URLs. If someone goes to your primary URL to get the font, the Edge Function on the private project will get the correct &lt;code&gt;Referer&lt;/code&gt; header and let anyone download or hotlink it.&lt;/p&gt;

&lt;p&gt;You can copy the same function into your primary repository and set it to the appropriate paths in &lt;code&gt;netlify.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# netlify.toml&lt;/span&gt;

&lt;span class="nn"&gt;[[edge_functions]]&lt;/span&gt;
  &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/fonts/the-future-mono-*"&lt;/span&gt;
  &lt;span class="py"&gt;function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hotlink-protection"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have the best of both worlds: the font is available for local development while protected when deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Here are some &lt;a href="https://blog.typekit.com/2009/07/21/serving-and-protecting-fonts-on-the-web/" rel="noopener noreferrer"&gt;suggestions from TypeKit&lt;/a&gt; on additional protections for web fonts.&lt;/li&gt;
&lt;li&gt;Alternatively, you can use &lt;a href="https://github.com/zachleat/glyphhanger" rel="noopener noreferrer"&gt;glyphhanger&lt;/a&gt; to subset web fonts based on what characters are used on your website.&lt;/li&gt;
&lt;li&gt;You can see which characters are in a Unicode range and do maths on ranges using &lt;a href="https://www.zachleat.com/unicode-range-interchange/" rel="noopener noreferrer"&gt;Unicode Range Interchange&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;You can see which OpenType features and all the characters are included in your font file using &lt;a href="https://wakamaifondue.com/" rel="noopener noreferrer"&gt;Wakamai Fondue&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you’re curious about web font performance, Zach Leatherman wrote an &lt;a href="https://www.zachleat.com/web/css-tricks-web-fonts/" rel="noopener noreferrer"&gt;excellent guide&lt;/a&gt; on its implementation on CSS-Tricks.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;And, of course, check out my personal site, &lt;a href="https://sidney.me/?utm_source=dev.to&amp;amp;utm_medium=Blog&amp;amp;utm_campaign=How+to+add+hotlink+protection+to+your+web+fonts+with+Netlify+Edge+Functions+and%C2%A0Deno"&gt;sidney.me&lt;/a&gt;, and let me know what you think on Mastodon, &lt;a href="https://indieweb.social/@notsidney" rel="noopener noreferrer"&gt;@notsidney@indieweb.social&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sidney.me/?utm_source=dev.to&amp;amp;utm_medium=Blog&amp;amp;utm_campaign=How+to+add+hotlink+protection+to+your+web+fonts+with+Netlify+Edge+Functions+and%C2%A0Deno"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frc85rty5kmorv51e740l.jpg" alt="Hi, I’m Sidney." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>netlify</category>
    </item>
    <item>
      <title>How and why you should store React UI state in the URL</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Fri, 10 Dec 2021 02:50:19 +0000</pubDate>
      <link>https://dev.to/notsidney/how-and-why-you-should-store-react-ui-state-in-the-url-34pi</link>
      <guid>https://dev.to/notsidney/how-and-why-you-should-store-react-ui-state-in-the-url-34pi</guid>
      <description>&lt;h3&gt;
  
  
  Deep linking in React, as simple as useState
&lt;/h3&gt;

&lt;p&gt;Have you ever used a complex web app with many features, modal windows, or side panels? Where you get to the perfect state with just the right information on the screen after a few clicks through different screens, but then you accidentally close the tab? (Or Windows decides to update?)&lt;/p&gt;

&lt;p&gt;It would be great if there were a way to return to this state without going through the same tedious process. Or be able to share that state so a teammate can work on the same thing you are.&lt;/p&gt;

&lt;p&gt;This problem could be solved with deep linking, which is used today in mobile apps to open the app to a specific page or UI state. But why does this not exist in many web apps?&lt;/p&gt;

&lt;p&gt;⏭ Click here to skip to the solution and code snippets. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bring back deep linking on the web
&lt;/h2&gt;

&lt;p&gt;The emergence of single-page applications (SPAs) has allowed us to craft new user experiences that are instantly interactive on the web. By doing more on the client side using JavaScript, we can respond to user events immediately, from opening custom dialog windows to live text editors like Google Docs.&lt;/p&gt;

&lt;p&gt;Traditional server-rendered websites send a request to get a new HTML page every single time. An excellent example is Google, which sends a request to its servers with the user’s search query in the URL: &lt;a href="https://www.google.com/search?q=your+query+here" rel="noopener noreferrer"&gt;https://www.google.com/search?&lt;strong&gt;q=your+query+here&lt;/strong&gt;&lt;/a&gt;. What’s great about this model is that if I filter by results from the past week, I can share the same search query by simply sharing the URL: &lt;a href="https://www.google.com/search?q=react+js&amp;amp;tbs=qdr:w" rel="noopener noreferrer"&gt;https://www.google.com/search?q=react+js&amp;amp;&lt;strong&gt;tbs=qdr:w&lt;/strong&gt;&lt;/a&gt;. And this paradigm is entirely natural for web users—sharing links has been part of the world wide web ever since it was invented!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx20gr9ovgelp6o8avmbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx20gr9ovgelp6o8avmbu.png" alt="Annotated screenshot of a Google search page. The search term input is highlighted and an arrow points to the corresponding part in the URL that stores the search term. The results are filtered to only show those from the past week, and another arrow points to the corresponding part in the URL that stores this data." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When SPAs came along, we didn’t need to store this data in the URL since we no longer need to make a server request to change what is displayed on the screen (hence &lt;em&gt;single-page&lt;/em&gt;). But this made it easy to lose a unique experience of the web, the shareable link.&lt;/p&gt;

&lt;p&gt;Desktop and mobile apps never really had a standardized way to link to specific parts of the app, and modern implementations of deep linking rely on URLs on the web. So when we build web apps that function more like native apps, why would we throw away the deep linking functionality of URLs that we’ve had for decades?&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead-simple deep linking
&lt;/h2&gt;

&lt;p&gt;When building a web app that has multiple pages, the minimum you should do is change the URL when a different page is displayed, such as &lt;code&gt;/login&lt;/code&gt; and &lt;code&gt;/home&lt;/code&gt;. In the React ecosystem, &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;React Router&lt;/a&gt; is perfect for client-side routing like this, and &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; is an excellent fully-featured React framework that also supports server-side rendering.&lt;/p&gt;

&lt;p&gt;But I’m talking about &lt;em&gt;deep&lt;/em&gt; linking, right down to the UI state after a few clicks and keyboard inputs. This is a killer feature for productivity-focused web apps, as it allows users to return right to the exact spot they were at even after closing the app or sharing it with someone else so they can start work without any friction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3o4grhdkx1g2dzw1zatb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3o4grhdkx1g2dzw1zatb.gif" alt="Screen recording of a modal window being opened, causing the URL to update to add `#modal=" width="720" height="540"&gt;&lt;/a&gt;&lt;/p&gt;


Notice how the URL updates to add `#modal="webhooks"` as the modal opens.





&lt;p&gt;You could use npm packages like &lt;a href="https://www.npmjs.com/package/query-string" rel="noopener noreferrer"&gt;query-string&lt;/a&gt; and write a basic React Hook to sync URL query parameters to your state, and there are &lt;a href="https://medium.com/swlh/using-react-hooks-to-sync-your-component-state-with-the-url-query-string-81ccdfcb174f" rel="noopener noreferrer"&gt;plenty&lt;/a&gt; of &lt;a href="https://www.npmjs.com/package/use-query-params" rel="noopener noreferrer"&gt;tutorials&lt;/a&gt; for &lt;a href="https://dev.to/gaels/an-alternative-to-handle-global-state-in-react-the-url--3753"&gt;this&lt;/a&gt;, but there’s a more straightforward solution.&lt;/p&gt;

&lt;p&gt;While exploring modern state management libraries for React for an architecture rewrite of our React app &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=How%20and%20why%20you%20should%20store%20React%20UI%20state%20in%20the%20URL"&gt;Rowy&lt;/a&gt;, I came across &lt;a href="https://jotai.org/" rel="noopener noreferrer"&gt;Jotai&lt;/a&gt;, a tiny atom-based state library inspired by the React team’s &lt;a href="https://recoiljs.org/" rel="noopener noreferrer"&gt;Recoil&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;The main benefit of this model is that state atoms are declared independent from the component hierarchy and can be manipulated from anywhere in the app. This solves the issue with React Context causing unnecessary re-renders, which I &lt;a href="https://betterprogramming.pub/how-to-useref-to-fix-react-performance-issues-4d92a8120c09" rel="noopener noreferrer"&gt;previously worked around with &lt;code&gt;useRef&lt;/code&gt;&lt;/a&gt;. You can read more about the atomic state concept in &lt;a href="https://jotai.org/docs/basics/concepts" rel="noopener noreferrer"&gt;Jotai’s docs&lt;/a&gt; and a more technical version in &lt;a href="https://recoiljs.org/docs/introduction/motivation" rel="noopener noreferrer"&gt;Recoil’s&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Jotai has a type of atom called &lt;a href="https://jotai.org/docs/api/utils#atom-with-hash" rel="noopener noreferrer"&gt;&lt;code&gt;atomWithHash&lt;/code&gt;&lt;/a&gt;, which syncs the state atom to the URL hash.&lt;/p&gt;

&lt;p&gt;Suppose we want a modal’s open state stored in the URL. Let’s start by creating an atom:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Then in the modal component itself, we can use this atom just like &lt;code&gt;useState&lt;/code&gt;:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;And here’s how it looks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgq08ssz4b61wl2pu4n2g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgq08ssz4b61wl2pu4n2g.gif" alt="Screen recording of a modal being opened, causing the URL to update to reflect the UI state, with #modalOpen=true being appended. When the modal is closed, it is replaced with modalOpen=false." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that’s it! It’s that simple.&lt;/p&gt;

&lt;p&gt;What’s fantastic about Jotai’s &lt;code&gt;atomWithHash&lt;/code&gt; is that it can store any data that &lt;code&gt;useState&lt;/code&gt; can, and it automatically stringifies objects to be stored in the URL. So I can store a more complex state in the URL, making it sharable.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=How%20and%20why%20you%20should%20store%20React%20UI%20state%20in%20the%20URL"&gt;Rowy&lt;/a&gt;, we used this technique to implement a UI for cloud logs. We’re building an open-source platform that makes backend development easier and eliminates friction for common workflows. So, reducing friction for sharing logs was perfect for us. You can see this in action on our demo, where I can link you to a specific deploy log: &lt;a href="https://demo.rowy.io/table/roadmap#modal=%22cloudLogs%22&amp;amp;cloudLogFilters=%7B%22type%22%3A%22build%22%2C%22timeRange%22%3A%7B%22type%22%3A%22days%22%2C%22value%22%3A7%7D%2C%22buildLogExpanded%22%3A1%7D" rel="noopener noreferrer"&gt;https://demo.rowy.io/table/roadmap#modal="cloudLogs"&amp;amp;cloudLogFilters={"type"%3A"build"%2C"timeRange"%3A{"type"%3A"days"%2C"value"%3A7}%2C"buildLogExpanded"%3A1}&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwr82hhr6b48242ad03d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwr82hhr6b48242ad03d.gif" alt="Screen recording of the deep link opening the Rowy demo web app to the cloud logs modal being open." width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Decoding the URL component reveals the exact state used in React:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;A side effect of &lt;code&gt;atomWithHash&lt;/code&gt; is that it pushes the state to the browser history by default, so the user can click the back and forward buttons to go between UI states.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghu788ii4pfg2idp70vz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghu788ii4pfg2idp70vz.gif" alt="Screen recording of the user clicking the back button in the browser repeatedly, causing the UI state to change with modals being opened and closed." width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This behavior is optional and can be disabled using the &lt;a href="https://jotai.org/docs/api/utils#atom-with-hash" rel="noopener noreferrer"&gt;&lt;code&gt;replaceState&lt;/code&gt; option&lt;/a&gt;:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;





&lt;p&gt;Thanks for reading! I hope this has convinced you to expose more of your UI state in the URL, making it easily shareable and reducing friction for your users—especially since it’s effortless to implement.&lt;/p&gt;

&lt;p&gt;You can follow me on Twitter &lt;a href="https://twitter.com/nots_dney" rel="noopener noreferrer"&gt;@nots_dney&lt;/a&gt; for more articles and Tweet threads about front-end engineering.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rowyio" rel="noopener noreferrer"&gt;
        rowyio
      &lt;/a&gt; / &lt;a href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;
        rowy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;a href="https://www.rowy.io/" rel="nofollow noopener noreferrer"&gt;
&lt;img width="100%" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F307298%2F218350866-cfd7c011-2247-4074-8b1d-06c26a4d0b96.png"&gt;
&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨
&lt;/h4&gt;
&lt;/div&gt;
&lt;p&gt;
Connect to your database and create Cloud Functions in low-code - without leaving your browser.&lt;br&gt;
Focus on building your apps
Low-code for Firebase and Google Cloud
&lt;/p&gt;

&lt;div&gt;
&lt;p&gt;&lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0ccce7901558d647206ce90e969c63d80ed1931e599e51fa9376491d5668ee19/68747470733a2f2f646362616467652e76657263656c2e6170702f6170692f7365727665722f666a4275676d767a5a50" alt="Rowy Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
    &lt;a href="http://www.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Website&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="http://docs.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Documentation&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Chat with us&lt;/b&gt;&lt;/a&gt; • 
    &lt;a href="https://twitter.com/rowyio" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Twitter&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/rowyio/rowy/commits/rc" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/479261b5c985c6803c34b0ba6da8dee3f2a3d1e261853fab7e9974fb48b7b0ad/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f726f7779696f2f726f77792f7263" alt="Last commit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rowyio/rowy/stargazers/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8adcfa7e078b22d59fc173e099b2337d30d5fe27e3c58a223968ac6477d94250/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f726f7779696f2f726f7779" alt="GitHub stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Demo 🛝&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;💥 Explore Rowy on &lt;a href="https://demo.rowy.io/" rel="nofollow noopener noreferrer"&gt;live demo playground&lt;/a&gt; 💥&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features ✨&lt;/h2&gt;

&lt;/div&gt;


  
    
    

    &lt;span class="m-1"&gt;20211004-RowyWebsite.mp4&lt;/span&gt;
    
  

  

  


&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Powerful spreadsheet interface for Firestore&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CMS for Firestore&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;Bulk import or export data - csv, json, tsv&lt;/li&gt;
&lt;li&gt;Sort and filter by row values&lt;/li&gt;
&lt;li&gt;Lock, Freeze, Resize, Hide and Rename columns&lt;/li&gt;
&lt;li&gt;Multiple views for the same collection&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Automate with cloud functions and ready made extensions&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Build cloud functions workflows on field level data changes
&lt;ul&gt;
&lt;li&gt;Use any NPM modules or APIs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Connect to your favourite tool with pre-built code blocks or create your own

&lt;ul&gt;
&lt;li&gt;SendGrid, Algolia, Twilio, Bigquery and more&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Rich and flexible data fields&lt;/h3&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;a href="https://docs.rowy.io/field-types/supported-fields" rel="nofollow noopener noreferrer"&gt;30+&lt;/a&gt;…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>UI design trends of today and how to apply them in your apps</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Tue, 23 Nov 2021 15:02:42 +0000</pubDate>
      <link>https://dev.to/notsidney/ui-design-trends-of-today-and-how-to-apply-them-in-your-apps-47kn</link>
      <guid>https://dev.to/notsidney/ui-design-trends-of-today-and-how-to-apply-them-in-your-apps-47kn</guid>
      <description>&lt;h3&gt;
  
  
  From outline icons to switch designs, the UIs of major OSes are all starting to look similar.
&lt;/h3&gt;

&lt;p&gt;If you’re like me, you get excited by major new software releases and enjoy reading roundups of &lt;a href="https://www.macstories.net/stories/ios-and-ipados-15-the-macstories-review/" rel="noopener noreferrer"&gt;every new feature&lt;/a&gt; and &lt;a href="https://www.androidpolice.com/tag/series_android_12_feature_spotlights/" rel="noopener noreferrer"&gt;UI tweak&lt;/a&gt;, comparing them to the previous version. With the release of Windows 11, Android 12, and iOS 15 this year (and last year’s macOS Big Sur), I’ve noticed quite a few similarities in the designs of the major operating systems. And in some cases, what appears to be convergence. These range from the use of outline icons to variable UI fonts and inset elements.&lt;/p&gt;

&lt;p&gt;These similarities show that designers are coming up with new standards for digital user interfaces and offer hints as to where UIs are headed over the next decade and how the thinking behind UI design is shifting.&lt;/p&gt;

&lt;p&gt;I’m a front-end engineer at &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=UI%20design%20trends%20of%20today%20and%20how%20to%20apply%20them%20in%20your%20apps"&gt;Rowy&lt;/a&gt;, and I recently redesigned its UI with the primary goal of improving the information-dense desktop experience. I also wanted to move away from the 2015-era, mobile-first Material Design we were following to something more modern and better complements the UIs we see today. The trends I noticed helped inform this redesign and will hopefully assist you when designing your apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Outline icons by default, complementing text&lt;/li&gt;
&lt;li&gt;Personalization based on the wallpaper&lt;/li&gt;
&lt;li&gt;Rounded corners are harmonious&lt;/li&gt;
&lt;li&gt;Variable UI fonts with optical sizes&lt;/li&gt;
&lt;li&gt;Focus on content&lt;/li&gt;
&lt;li&gt;Inset everything&lt;/li&gt;
&lt;li&gt;Differentiation beyond color&lt;/li&gt;
&lt;li&gt;And a standard switch design&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Outline icons by default, complementing text
&lt;/h2&gt;

&lt;p&gt;Icons play a significant part in UIs and have proven to be emblematic of the trends in UI design over the past few decades. When graphical user interfaces were novel, icons mimicked real-world objects to as great detail as possible within the limitations of display technology; they were made to bridge the user experience from the physical to the digital. This can be seen from Susan Kare’s icons for the original Macintosh to Microsoft’s photorealistic set in Windows Vista and 7. &lt;/p&gt;

&lt;p&gt;When UIs moved from this skeuomorphic style to a “flat” style with Windows 8 and iOS 7, icon design also shifted to a monochromatic, outline style. And with the release of Android 12 and Google’s new version of Material Design called Material You (or Material Design 3), all major OSes now use this style.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhusqdmm1xtxdku722f4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhusqdmm1xtxdku722f4e.png" alt="Icon designs of Segoe Fluent Icons, SF Symbols, and Material Icons. Icons are: account, heart, share, edit, reload, and search." width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But why does everyone use this style for icons? Designers aren’t just investing more into creating cohesive and complementary icon sets for design systems—designers are making icons just like typographers make type. This style recognizes how integral icons have become to the user experience, as important as text itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Icons are designed with similar considerations to typefaces, and often appear alongside text.&lt;/em&gt;&lt;br&gt;
— &lt;a href="https://m3.material.io/styles/typography/applying-scaling-type#d00fccbc-719a-4aa6-b663-e0e1a9904e76" rel="noopener noreferrer"&gt;Material Design 3 guidelines&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Iconography [is] designed to integrate seamlessly with San Francisco, the system font.&lt;/em&gt;&lt;br&gt;
— &lt;a href="https://developer.apple.com/sf-symbols/" rel="noopener noreferrer"&gt;Apple Human Interface Guidelines&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Apple’s SF Symbols is the prime example of this: it’s designed in the &lt;a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/#weights-and-scales" rel="noopener noreferrer"&gt;same nine weights&lt;/a&gt; as their system font and aligns with the text’s cap height. This also means icons can respect the user’s accessibility settings for bolder UI text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplk0eyqb37t9cuh226ub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplk0eyqb37t9cuh226ub.png" alt="From Apple: A diagram showing the square and arrow up symbol in all 27 weights and scales." width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;


Apple’s SF Symbols icon set is designed with the same nine weights as their system font. Source: &lt;a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/#weights-and-scales" rel="noopener noreferrer"&gt;Apple&lt;/a&gt;





&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpn8cq84cvzq1152ho3dc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpn8cq84cvzq1152ho3dc.png" alt="From Apple: 3 images of the plus circle symbol followed by the capitalized word add. In each, the word is the same size, but the symbol is a different size: small, medium, and large. 2 parallel horizontal lines appear across all three images. The top line shows the height of the capital letter A and the bottom line is the baseline under the word. Small: circle touches both lines. Medium: circle extends slightly above and below the lines. Large: vertical line of the plus sign almost touches both.&amp;lt;br&amp;gt;
" width="800" height="126"&gt;&lt;/a&gt;&lt;/p&gt;


They also align to the system font’s cap height to better complement text. Source: &lt;a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/#weights-and-scales" rel="noopener noreferrer"&gt;Apple&lt;/a&gt;





&lt;p&gt;Uber’s design team &lt;a href="https://medium.com/uber-design/where-to-the-journey-to-ubers-iconography-bf8efd2be446" rel="noopener noreferrer"&gt;similarly made icons&lt;/a&gt; to complement their UI font Uber Move, set in three weights for different degrees of emphasis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffof9e0v2784yf7a7egtb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffof9e0v2784yf7a7egtb.png" alt="Uber Move icon set in light, medium, and bold weights.&amp;lt;br&amp;gt;
" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;


Source: &lt;a href="https://medium.com/uber-design/where-to-the-journey-to-ubers-iconography-bf8efd2be446" rel="noopener noreferrer"&gt;Uber&lt;/a&gt;





&lt;h3&gt;
  
  
  Icons are designed to mimic our writing systems
&lt;/h3&gt;

&lt;p&gt;So it becomes clear why system icons are monochromatic and drawn with outlines: instead of mimicking physical objects, they mimic our writing systems. Modern alphabets, Latin in particular, are fundamentally composed of lines instead of filled shapes. They made a similar transition from pictographs that resemble physical objects (like Egyptian hieroglyphs) to more abstract representations of physical objects. For example, you don’t need to draw a bird to talk about a bird; you just write a set of letters or symbols that mean ‘bird’.&lt;/p&gt;

&lt;p&gt;By designing icons that resemble text, designers recognize that icons are a vital medium to communicate information to users. Icons can break language barriers, while text remains essential for those unfamiliar with icons. And designing both to appear similar can reduce cognitive load when reading icons alongside text.&lt;/p&gt;

&lt;p&gt;Another benefit of defaulting to outline icons is that filled icons can now be used to represent state or emphasis, like bold text. For instance, “active states are represented with filled icons” in &lt;a href="https://m3.material.io/components/navigation-bar/overview#5eebed09-2454-4630-ab41-3515acccbba9" rel="noopener noreferrer"&gt;Material Design 3&lt;/a&gt; and Apple suggests to “use the fill variant to indicate selection” or “to give a symbol more visual emphasis” in their &lt;a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/#variants" rel="noopener noreferrer"&gt;Human Interface Guidelines&lt;/a&gt;. Twitter’s 2021 redesign takes this a step further, with the active page being differentiated only by a filled icon and bold text, without any color change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffio43m70qzw0v4840gck.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffio43m70qzw0v4840gck.png" alt="Twitter navigation UI, with the Home page active. The home icon is filled and the text is bold, while other icons are outlined and in regular font weight.&amp;lt;br&amp;gt;
" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

Twitter only uses filled icons and bold text to differentiate the current page. The jury is still out on whether this is accessible enough.




&lt;p&gt;Predicting the future is a fool’s errand, but looking at the history of writing systems, they’ve seen convergence on what symbols represent what sounds and ideas: the symbol ‘a’ always means ‘a’ and the letters ‘ant’ mean the insect named ant. There remain variations in how these symbols are drawn (the capital A can have serifs or not) but they all fundamentally look the same across different typefaces.&lt;/p&gt;

&lt;p&gt;Iconography seems to be heading this way too—the “add folder” icon above has the same basic design across Apple’s, Google’s, and Microsoft’s icon sets: an outline of a closed Manila folder with a ‘+’ symbol. As icons become more commonplace in GUIs, people tend to agree on what symbols represent what ideas and differences become purely stylistic, just like text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gcqjn1argc4e9m0vg5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gcqjn1argc4e9m0vg5c.png" alt="Add folder icon in Segoe Fluent Icons, SF Symbols, and Material Icons." width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I achieve this?&lt;/strong&gt; When using icons in your apps, you should use the icon set designed with the font you’re using: this is trivial if you’re using system fonts, which now all have system icon fonts. But if you’re using another UI font, there probably isn’t an icon set designed specifically for that font. Using an outline icon set is sufficient, such as the open-source &lt;a href="https://feathericons.com" rel="noopener noreferrer"&gt;Feather icons&lt;/a&gt; or &lt;a href="https://iconic.app/" rel="noopener noreferrer"&gt;Iconic.app&lt;/a&gt;. And if you were using filled Material Icons like us, you can easily switch to the &lt;a href="http://fonts.google.com/icons" rel="noopener noreferrer"&gt;outline style&lt;/a&gt;. (It seems Google has now made this style the default as part of Material Design 3.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Personalization based on the wallpaper
&lt;/h2&gt;

&lt;p&gt;Personalization is becoming an increasingly important element of UI design. While customization has been present since the ’90s with custom wallpapers and themes, designers are now finding more ways to personalize UIs throughout the system, primarily from the user-selected wallpaper. Google’s &lt;a href="https://material.io/blog/announcing-material-you" rel="noopener noreferrer"&gt;Material Design blog&lt;/a&gt; says it best: “Users customize their desktops in the physical and digital worlds with images that are personal and provide comfort and joy.” The wallpaper is the first thing a user sees and serves as a backdrop for the UI. So it’s a no-brainer to adapt the rest of the UI to this element.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuj5dhbqhglrp1zeo5gp5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuj5dhbqhglrp1zeo5gp5.png" alt="From the left: Windows 11 menu with translucent background that blurs the wallpaper, macOS app sidebar with similar translucent background, iOS control center with similar translucent background, and Material You app design with color palette generated from wallpaper colors." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The technique of blending the wallpaper into the UI has been around for a while, from the semi-transparent dock and menus in Mac OS X’s Aqua interface to the translucent window title bars in Windows Vista and 7’s Aero Glass theme. iOS 7 famously brought blurred backgrounds to many parts of its UI, with entire screens like the Notification Center and Control Center using a blurred version of the user’s wallpaper.&lt;/p&gt;

&lt;p&gt;More recently, the wallpaper has started to influence almost the entirety of the UI. When Apple introduced dark mode in macOS Mojave, they incorporated a tint in window backgrounds based on the wallpaper, called Desktop Tinting. According to the &lt;a href="https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/#darkmode-colors" rel="noopener noreferrer"&gt;Human Interface Guidelines&lt;/a&gt;, it “helps windows blend more harmoniously with their surrounding content.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04cwbvqcoctyewvucv2u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04cwbvqcoctyewvucv2u.jpg" alt="Two screenshots, split vertically, of the System Preferences window in dark mode in macOS Mojave, with different wallpapers behind. The left half uses a wallpaper with dark blue tones. The right half uses a wallpaper with lighter orange tones. The window has a different background color that's slightly tinted to match the colors of the wallpapers." width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

Source: &lt;a href="https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/#darkmode-colors" rel="noopener noreferrer"&gt;Apple&lt;/a&gt;




&lt;p&gt;They later expanded on this in their major redesign in Big Sur by applying Desktop Tinting to light mode as well:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikqdmst5jh2t9dmaopsx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikqdmst5jh2t9dmaopsx.jpg" alt="Two screenshots, split vertically, of the System Preferences window in light mode in macOS Monterey. The left half has Desktop Tinting enabled and the right has it disabled." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

On the left, the window background is tinted purple to match the wallpaper. On the right, tinting is disabled, and the window background is pure gray.




&lt;p&gt;Windows 11 introduces a similar element with its Mica material, which their &lt;a href="https://docs.microsoft.com/en-us/windows/apps/design/style/mica" rel="noopener noreferrer"&gt;design guidelines&lt;/a&gt; describe as “an opaque material that incorporates the user’s theme and desktop wallpaper to create its highly personalized appearance.” It also uses this tint as a signifier for the currently active window.&lt;/p&gt;

&lt;p&gt;Taking it to the next level is Material You in Android 12, which tints the app’s background, the bright accent colors for buttons and other controls, and down to the more neutral text colors. An entire color palette is generated from each user’s unique wallpaper.&lt;/p&gt;

&lt;p&gt;It’s a critical element of their new design philosophy, &lt;a href="https://www.youtube.com/watch?v=Mlk888FiI8A&amp;amp;t=4410s" rel="noopener noreferrer"&gt;“Instead of form following function, what if form followed feeling?”&lt;/a&gt; It’s a radical rejection of the status quo in UI design, which seeks a “universal” design with the most technically superior interface to fulfill user needs. It will be interesting to see if others follow suit in this philosophy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5p1uftjju7cjjjaitei.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5p1uftjju7cjjjaitei.jpg" alt="Multiple phones arranged in a grid with different wallpapers and different UI color palettes derived from the wallpapers." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

Source: &lt;a href="https://material.io/blog/announcing-material-you" rel="noopener noreferrer"&gt;Google&lt;/a&gt;




&lt;h3&gt;
  
  
  Material Design built a color system
&lt;/h3&gt;

&lt;p&gt;What the Material Design team left out of their announcements is &lt;em&gt;how&lt;/em&gt; they achieved this, especially since &lt;a href="https://youtu.be/rLKtIGY2Mgc?t=319" rel="noopener noreferrer"&gt;they say&lt;/a&gt; they “had to find a way for any color combination to also have accessible contrast […] without testing each one.” Diving into the recently-published &lt;a href="https://github.com/material-foundation/material-color-utilities" rel="noopener noreferrer"&gt;Material color utilities repo on GitHub&lt;/a&gt; unveils all the mystery:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Material team built their own &lt;a href="https://github.com/material-foundation/material-color-utilities/blob/main/typescript/hct/hct.ts" rel="noopener noreferrer"&gt;color system&lt;/a&gt;: ‘hue, chroma, tone’, or ‘HCT’, based on the &lt;a href="https://en.wikipedia.org/wiki/Color_appearance_model#CAM16" rel="noopener noreferrer"&gt;CAM16&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/CIELAB_color_space" rel="noopener noreferrer"&gt;CIELAB&lt;/a&gt; (or LAB) color appearance models. CAM16 is a successor of LAB, designed to &lt;a href="https://programmingdesignsystems.com/color/perceptually-uniform-color-spaces/index.html" rel="noopener noreferrer"&gt;match how humans perceive color&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Key to these models is the ‘tone’ or L* value, which describes a color’s perceived luminance or lightness, with an L* value of 0 being black and 100 being white. This is very useful when creating accessible color palettes, ensuring colors have enough contrast based on perceived luminance.&lt;/li&gt;
&lt;li&gt;On the web, the &lt;a href="https://webaim.org/articles/contrast/" rel="noopener noreferrer"&gt;WCAG 2 guidelines&lt;/a&gt; prescribe a minimum contrast of 4.5:1 for body text. Directly using perceived luminance as a value to describe a color makes this much more straightforward, as the Material team &lt;a href="https://github.com/material-foundation/material-color-utilities/blob/main/typescript/hct/hct.ts#L28-L30" rel="noopener noreferrer"&gt;explains&lt;/a&gt;: “Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate […] a difference of 50 guarantees a contrast ratio &amp;gt;= 4.5.”&lt;/li&gt;
&lt;li&gt;With this knowledge, all that’s left is to generate a palette of colors with different tones or L* values and apply any hue to it. Then use sufficiently contrasting pairs for UI elements. For example, a button can have a background color with L* = 40 and white text (L* = 100), and it would easily pass the minimum contrast requirement (L* difference &amp;gt; 50).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahnh83zsz6togwihis9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahnh83zsz6togwihis9n.png" alt="Light color palette generated from a user-supplied red floral wallpaper. All the colors have warm, red tones, with different levels of lightness." width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

Source: &lt;a href="https://m3.material.io/styles/color/dynamic-color/user-generated-color" rel="noopener noreferrer"&gt;Google&lt;/a&gt;




&lt;h3&gt;
  
  
  LCH for the rest of us
&lt;/h3&gt;

&lt;p&gt;This is a powerful technique to effortlessly generate accessible colors, but it doesn’t require Material color utilities. LAB can be represented as LCH (luminosity, chroma, hue), similar to Material’s HCT, where the L value can be used to calculate contrast. (This is a better representation than existing ones like HSL, which can vary in &lt;em&gt;perceived&lt;/em&gt; lightness even if the lightness &lt;em&gt;value&lt;/em&gt; is the same.) You can learn more about LCH in &lt;a href="https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/" rel="noopener noreferrer"&gt;this article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(Also, LCH is making its way into web standards as part of &lt;a href="https://drafts.csswg.org/css-color/#lab-colors" rel="noopener noreferrer"&gt;CSS Color Level 4&lt;/a&gt;! So you can write &lt;code&gt;lch(40% 44 49)&lt;/code&gt; in CSS without needing to convert it to HSL or RGB, but this is currently &lt;a href="https://colord.omgovich.ru" rel="noopener noreferrer"&gt;only supported in Safari&lt;/a&gt;. Lea Verou, who wrote the article linked above, is part of the W3C CSS Working Group developing this very standard.)&lt;/p&gt;

&lt;p&gt;So, all you need is a starting color, convert it to LCH, and modify the L value to make a palette. Then use a pair of colors that have a difference of 50 or more in luminosity to ensure accessible contrast.&lt;/p&gt;

&lt;p&gt;We can see how this technique is used in the Material color system below: the tones in the palette match the LCH luminosity value. (They also modify the chroma (similar to saturation) and hue slightly across tones.) I made a &lt;a href="http://notsidney.github.io/material-you-palette-visual" rel="noopener noreferrer"&gt;small web app&lt;/a&gt; that displays the palettes generated by the Material color utilities to get the LCH values below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouthxlo5mihpxkfwpwy7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouthxlo5mihpxkfwpwy7.png" alt="13 tones derived from a key color. Tones are: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100. Underneath each tone is the associated LCH value. The L value matches the tone number." width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

Source: &lt;a href="https://m3.material.io/styles/color/the-color-system/key-colors-tones" rel="noopener noreferrer"&gt;Google&lt;/a&gt;




&lt;p&gt;Before Material’s HCT, the design teams of &lt;a href="https://design.lyft.com/re-approaching-color-9e604ba22c88" rel="noopener noreferrer"&gt;Lyft&lt;/a&gt; and &lt;a href="https://stripe.com/blog/accessible-color-systems" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt; explored the same issue, and Stripe also ended up using LCH. I’ve linked both of their explorations, which are excellent reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I achieve this?&lt;/strong&gt; There are a few tools to convert to and from LCH:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lea Verou made an &lt;a href="https://css.land/lch/" rel="noopener noreferrer"&gt;LCH color picker&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://colord.omgovich.ru" rel="noopener noreferrer"&gt;Colord website&lt;/a&gt; lets you convert from LCH to hex or RGB,&lt;/li&gt;
&lt;li&gt;if you have a Mac, Sindre Sorhus’ &lt;a href="https://sindresorhus.com/system-color-picker" rel="noopener noreferrer"&gt;System Color Picker&lt;/a&gt; is excellent, and&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://accessiblepalette.com" rel="noopener noreferrer"&gt;Accessible Palette&lt;/a&gt; generates a palette using LAB/LCH.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used this technique to add theming to &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=UI%20design%20trends%20of%20today%20and%20how%20to%20apply%20them%20in%20your%20apps"&gt;Rowy&lt;/a&gt;, allowing users to choose their own accent color while maintaining accessibility and tinting key UI elements. &lt;a href="https://github.com/rowyio/rowy/blob/92630a52351bffdb1d085a41dc46e515bee8faf3/src/theme/colors.ts#L20-L30" rel="noopener noreferrer"&gt;The source code&lt;/a&gt; has the exact LCH values I used.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F743x5fs8rh0mu1ar7nw9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F743x5fs8rh0mu1ar7nw9.png" alt="Two UI themes based on a different primary color. On the left, the primary color is purple and on the right, it's red. The UI colors for backgrounds and shadows are displayed as LCH. The L and C values are the same across the two themes, but the H value matches the hue of the primary color." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounded corners are harmonious
&lt;/h2&gt;

&lt;p&gt;When you have a box with rounded corners in a design and want to nest an inner box with a small gap, the inner element should have a smaller corner radius, with the difference being the gap size. If you make the corner radii equal for both, the inner box &lt;a href="https://css-tricks.com/public-service-announcement-careful-with-your-nested-border-radii/" rel="noopener noreferrer"&gt;looks out of place&lt;/a&gt;. This technique has been &lt;a href="https://graphicdesign.stackexchange.com/questions/8919/how-to-compute-the-radii-radiuses-of-corners-for-concentric-rounded-rects" rel="noopener noreferrer"&gt;known for a while&lt;/a&gt; in digital design and is even part of &lt;a href="https://www.w3.org/TR/css-backgrounds-3/#corner-shaping" rel="noopener noreferrer"&gt;the CSS3 spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1349782080021028865-440" src="https://platform.twitter.com/embed/Tweet.html?id=1349782080021028865"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1349782080021028865-440');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1349782080021028865&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;This is also present in hardware design: the screen’s rounded corners match the device frame on the &lt;a href="https://twitter.com/Mumblefluff/status/1061247788611190785" rel="noopener noreferrer"&gt;iPad Pro&lt;/a&gt; and iPhone X designs. On iPhones with rounded screens, the dock matches the screen’s curvature—Apple even provides the exact point size in &lt;a href="https://kylebashour.com/posts/finding-the-real-iphone-x-corner-radius" rel="noopener noreferrer"&gt;the software&lt;/a&gt;. You can also see other elements in the &lt;a href="https://blog.maxrudberg.com/post/166045445103/ui-design-for-iphone-x-top-elements-and-the-notch" rel="noopener noreferrer"&gt;video player&lt;/a&gt; matching the screen curvature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcsys5j1fr88m4gnvg1n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcsys5j1fr88m4gnvg1n.png" alt="Annotated screenshot of iPhone dock. The display corner radius is 47.33pt, the gap between the display and dock is 12pt, and the dock corner radius is 35.33pt." width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

These values were measured using the image provided in Sketch’s &lt;a href="https://www.sketch.com/docs/designing/templates/" rel="noopener noreferrer"&gt;built-in iOS App Icon template&lt;/a&gt; and the display corner radius for the iPhone 12 from Kyle Bashour’s &lt;a href="https://github.com/kylebshr/ScreenCorners" rel="noopener noreferrer"&gt;ScreenCorners library&lt;/a&gt;. The app icon radius remains consistent across the system, so it does not match the screen’s curvature.




&lt;p&gt;Oddly, this principle was not applied to UI elements, which used a single corner radius. In Material Design 2, both dialogs and inner buttons have a &lt;a href="https://material.io/design/shape/applying-shape-to-ui.html#baseline-shape-values" rel="noopener noreferrer"&gt;corner radius of 4dp&lt;/a&gt;, despite having a &lt;a href="https://material.io/components/dialogs#specs" rel="noopener noreferrer"&gt;gap of 8dp&lt;/a&gt; in between. macOS was similar in the Yosemite-era design, and Windows 10 used square corners for almost all UI elements.&lt;/p&gt;

&lt;p&gt;However, with Big Sur and Windows 11, this effect is approximated by increasing the corner radii of larger UI elements. In macOS, buttons now have a corner radius of 5pt and dialog windows 10pt, plus they all use &lt;a href="https://www.sketch.com/blog/2017/10/10/introducing-libraries-and-smooth-corners-in-sketch-47/#smooth-corners" rel="noopener noreferrer"&gt;‘smooth corners’&lt;/a&gt; to match the corners of Apple’s hardware. Meanwhile, Windows 11 famously rounds previously sharp corners, with buttons at 4px and windows 8px.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2akb0ip6l7g8kehe8iua.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2akb0ip6l7g8kehe8iua.png" alt="Annotated side-by-side screenshots of Windows 11 and macOS 12 dialog windows. The Windows 11 design uses 8px for the window corner radius and 4px for the button. The macOS design uses 10pt for the window and 5pt for the button." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also seems that UIs are becoming more rounded in general:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Big Sur increases corner radii and uses smooth corners, which look more rounded;&lt;/li&gt;
&lt;li&gt;iOS 15 introduces &lt;a href="https://developer.apple.com/design/human-interface-guidelines/ios/controls/buttons/" rel="noopener noreferrer"&gt;button styles&lt;/a&gt; with fully-rounded corners;&lt;/li&gt;
&lt;li&gt;Windows 11 removed sharp corners on most UI elements; and&lt;/li&gt;
&lt;li&gt;Android 12 increased the radii for &lt;a href="https://m3.material.io/components/dialogs/overview" rel="noopener noreferrer"&gt;dialogs&lt;/a&gt;, &lt;a href="https://m3.material.io/components/navigation-drawer/overview" rel="noopener noreferrer"&gt;navigation drawers&lt;/a&gt;, and fully round the corners of &lt;a href="https://m3.material.io/components/buttons/overview" rel="noopener noreferrer"&gt;buttons&lt;/a&gt;, in stark contrast to all previous versions of Material Design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How can I achieve this?&lt;/strong&gt; Set the smallest UI elements to some base corner radius, then set the larger containing elements like dialogs to a larger corner radius. Try to make them proportional to the distance between the smaller element, or double the smaller corner radius to simplify. Here’s how it looks in &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=UI%20design%20trends%20of%20today%20and%20how%20to%20apply%20them%20in%20your%20apps"&gt;our app&lt;/a&gt;, with the corner radii doubled:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcxay0hdgkvce2wl60gnv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcxay0hdgkvce2wl60gnv.png" alt="Annotated screenshot of a dialog window in Rowy. The window's corner radius is 8px and the button's is 4px." width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Variable UI fonts with optical sizes
&lt;/h2&gt;

&lt;p&gt;When typefaces were first developed, they were physical designs etched in metal, with fixed font sizes. When a typographer designed the same typeface for a different size, they modified the design to be optimal by altering facets like spacing and proportion: this is known as optical size. You can learn more about optical size in &lt;a href="https://pixelambacht.nl/2021/optical-size-hidden-superpower/" rel="noopener noreferrer"&gt;this article&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Variable fonts are a new font format based on OpenType that allows the designer to customize specific ‘variation axes’ (or variables) of a type design, such as a non-fixed weight, slant, and optical size. You can learn more about variable fonts in this excellent &lt;a href="https://variablefonts.io/" rel="noopener noreferrer"&gt;Variable Fonts Primer&lt;/a&gt;, which uses &lt;a href="https://github.com/TypeNetwork/Roboto-Flex" rel="noopener noreferrer"&gt;Roboto Flex&lt;/a&gt;, a variable font extension of Google’s Roboto typeface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrtt7856zxajbd5kkgy1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrtt7856zxajbd5kkgy1.png" alt="Text samples of Segoe UI Variable, SF Pro, and Google Sans at different optical sizes that highlight the differences in designs at different optical sizes." width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 2021, all major OSes now use this variable font technology to implement optical sizes in UI typography:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple’s system font San Francisco was released &lt;a href="https://developer.apple.com/videos/play/wwdc2015/804/" rel="noopener noreferrer"&gt;in 2015&lt;/a&gt; with two optical sizes: ‘Display’ for sizes 20 points and larger, and ‘Text’ for everything smaller. &lt;a href="https://developer.apple.com/videos/play/wwdc2020/10175/?time=153" rel="noopener noreferrer"&gt;In 2020&lt;/a&gt;, Apple released these fonts as a single variable font, SF Pro, with optical size as a variation axis. Apple’s system icons, SF Symbols, &lt;a href="https://developer.apple.com/videos/play/wwdc2021/10250/?time=411" rel="noopener noreferrer"&gt;also use variable font techniques&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For Windows 11, Microsoft redesigned its system font Segoe UI as &lt;a href="https://docs.microsoft.com/en-us/windows/apps/design/signature-experiences/typography" rel="noopener noreferrer"&gt;Segoe UI Variable&lt;/a&gt; with its own optical size axis.&lt;/li&gt;
&lt;li&gt;And as part of Material Design 3, Google introduces &lt;a href="https://youtu.be/rLKtIGY2Mgc?t=414" rel="noopener noreferrer"&gt;GS Text and GS Variable&lt;/a&gt;, an evolution of its corporate font Google Sans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also of note: these typefaces are all different styles of sans-serif. &lt;a href="https://en.wikipedia.org/wiki/San_Francisco_(sans-serif_typeface)" rel="noopener noreferrer"&gt;San Francisco&lt;/a&gt; is neo-grotesque, &lt;a href="https://en.wikipedia.org/wiki/Segoe" rel="noopener noreferrer"&gt;Segoe&lt;/a&gt; is humanist, and &lt;a href="https://en.wikipedia.org/wiki/Product_Sans" rel="noopener noreferrer"&gt;Google Sans&lt;/a&gt; is geometric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I achieve this?&lt;/strong&gt; Variable fonts are a relatively new technology, and producing them is expensive, so there aren’t as many of them around, especially in the free and open source space. So far, the only open source variable font with optical sizes that I’ve found is &lt;a href="https://codepen.io/RoelN/pen/PoPvdeV" rel="noopener noreferrer"&gt;Roboto Flex&lt;/a&gt;, but it doesn’t seem to be finished yet. Rasmus Andersson’s increasingly ubiquitous Inter typeface has a &lt;a href="https://twitter.com/rsms/status/1248657377877839872" rel="noopener noreferrer"&gt;display size in beta&lt;/a&gt;. In the meantime, using a more expressive typeface for prominent titles can elevate your design. For &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=UI%20design%20trends%20of%20today%20and%20how%20to%20apply%20them%20in%20your%20apps"&gt;Rowy&lt;/a&gt;, I used Inter as the typeface in small text and Space Grotesk in headings for brand expression.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8b9erfh5d34r43bkabt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8b9erfh5d34r43bkabt.png" alt="Typography scale used in Rowy. Headings use the font Space Grotesk and smaller UI text use Inter." width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus on content
&lt;/h2&gt;

&lt;p&gt;Flat design has been with us for over a decade, and its primary goal is to focus on the content by stripping away the clutter and ornamentation of UI elements. The &lt;a href="https://developer.apple.com/design/human-interface-guidelines/ios/overview/themes/" rel="noopener noreferrer"&gt;iOS 15 design guidelines&lt;/a&gt; state, “a crisp, beautiful interface help[s] people understand and interact with content while never competing with it.”&lt;/p&gt;

&lt;p&gt;The most recent OS releases iterate on this concept by having even less “chrome”. Navigation bars are transparent in &lt;a href="https://developer.apple.com/videos/play/wwdc2021-10059/?time=335" rel="noopener noreferrer"&gt;iOS 15&lt;/a&gt; and &lt;a href="https://m3.material.io/components/top-app-bar/overview" rel="noopener noreferrer"&gt;Android 12&lt;/a&gt; and blend into the background until you scroll.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsebut20dfeoxa0w65q7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvsebut20dfeoxa0w65q7.png" alt="A 2×2 grid of screenshots of navigation bars in iOS 15 (left column) and Android 12 (right column). The top row shows the navigation bars before scrolling, with a transparent background. The bottom row shows the bars with backgrounds after the user has scrolled." width="800" height="715"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the desktop, macOS Big Sur blends the title bar and toolbar until you scroll or hover over the bar for a few seconds. And some apps in Windows 11 don’t distinguish the title bar at all but house the content in a distinct card-like layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdo0qr1wfvyss2h2isq0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdo0qr1wfvyss2h2isq0.png" alt="On the top, two screenshots of the macOS toolbar design. On the left, the toolbar blends into the background by default. On the right, the toolbar has a shadow underneath as the user hovers over the toolbar. Below is a screenshot of the Windows 11 settings app, which always has a transparent background for the toolbar. The content below is housed in a card-like layer with a lighter background and shadow." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These elements all achieve the same goal: to reduce the amount of visual clutter around and elevate the visual prominence of the content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I achieve this?&lt;/strong&gt; If you have a navigation bar docked to an edge, blend it into the background when it doesn’t need to be distinguished, such as when the user hasn’t scrolled. And if you have primary content, house it in a subtle layer distinct from the background. We use a React UI library, MUI, which makes it easy for us to &lt;a href="https://mui.com/components/app-bar/#elevate-app-bar" rel="noopener noreferrer"&gt;achieve this effect&lt;/a&gt; where the navigation bar is only distinguished on scroll:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9odhtj4vfnka9i0d8ea4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9odhtj4vfnka9i0d8ea4.png" alt="Two screenshots side-by-side of navigation bars in Rowy. On the left, the navigation bar blends into the background in the initial state. On the right, the navigation bar has a white background and shadow after the user has scrolled." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inset everything
&lt;/h2&gt;

&lt;p&gt;In a similar vein, more UI elements are inset, no longer taking the entire width of their containers. When the iPhone X introduced an on-screen home indicator in place of the home button, Apple changed &lt;a href="https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/#designing-a-full-screen-experience" rel="noopener noreferrer"&gt;their guidelines&lt;/a&gt; to prescribe inset buttons with rounded corners, eschewing the &lt;a href="https://blog.maxrudberg.com/post/165590234593/ui-design-for-iphone-x-bottom-elements" rel="noopener noreferrer"&gt;full-width buttons&lt;/a&gt; introduced in iOS 7. Best practices changed for &lt;a href="https://blog.maxrudberg.com/post/165590234593/ui-design-for-iphone-x-bottom-elements" rel="noopener noreferrer"&gt;many other elements&lt;/a&gt; fixed to the bottom of the screen to adapt to the new iPhone design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1sbug3105qdc5izbd9jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1sbug3105qdc5izbd9jn.png" alt="Screenshot of the Windows 11 Settings app, macOS 12 Mail app, iOS 15 Settings app, and Android 12 notification panel. In all screenshots, none of the UI elements touch the edge of the windows or screens and are contained in elements with rounded corners." width="800" height="905"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With iOS 15, Apple is insetting ‘table views’ in more apps like Settings and Mail. It seems to be in response to the growing size of iPhone screens, with &lt;a href="https://developer.apple.com/design/human-interface-guidelines/ios/views/tables/" rel="noopener noreferrer"&gt;the guidelines&lt;/a&gt; noting, “in a compact environment, an inset grouped table can cause text wrapping, especially when content is localized.”&lt;/p&gt;

&lt;p&gt;In macOS Big Sur, they extend this design to the lists in Mail, consistent with the &lt;a href="https://developer.apple.com/design/human-interface-guidelines/ios/views/split-views/" rel="noopener noreferrer"&gt;iPadOS design&lt;/a&gt;. It’s also present in menus throughout the system, including the menu bar. Note the click targets extend to the edge of the menus, like the previous full-width design. Windows 11 shares the same style in its menus and navigation items. Android 12’s system UI and apps generally follow this style too.&lt;/p&gt;

&lt;p&gt;This style could improve accessibility as the separation between elements and their containers now extends to all four sides, but I haven’t found any research supporting this. When paired with harmoniously rounded corners, it can make menus look more modern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F779fk56ddg0va9ux1ued.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F779fk56ddg0va9ux1ued.png" alt="Two screenshots of menus in Rowy. In both, the menu items do not touch the edge of the menu container and items have rounded corners." width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Differentiation beyond color
&lt;/h2&gt;

&lt;p&gt;Designers are adding more ways to display states without relying on color, which is &lt;a href="https://www.smashingmagazine.com/2016/06/improving-color-accessibility-for-color-blind-users/" rel="noopener noreferrer"&gt;inaccessible&lt;/a&gt; for people with color blindness. Here are some examples I noticed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spotify added dots below the shuffle and repeat buttons instead of relying solely on changing their color &lt;a href="https://community.spotify.com/t5/Implemented-Ideas/All-Platforms-Make-Shuffle-Icons-Discernible-for-Color-Blind/idi-p/1556225" rel="noopener noreferrer"&gt;in 2017&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Material Design 3 displays a pill-shaped indicator and uses filled icons for active pages in the &lt;a href="https://m3.material.io/components/navigation-bar/overview" rel="noopener noreferrer"&gt;navigation bar&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Windows 11 adds a consistent, distinct line to selected items in &lt;a href="https://docs.microsoft.com/en-us/windows/apps/design/signature-experiences/geometry" rel="noopener noreferrer"&gt;lists and navigation panes&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8oo766v7t4lv7myyvq6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8oo766v7t4lv7myyvq6p.png" alt="Three screenshots. From the left: a Windows 11 navigation pane with a line on the left of the active page, the Spotify playback controls with repeat enabled and a dot below the repeat icon to signify state, and an Android 12 navigation bar with the current page signified by a pill shape behind the page icon." width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This informed the decision to redesign toggle buttons for &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=UI%20design%20trends%20of%20today%20and%20how%20to%20apply%20them%20in%20your%20apps"&gt;Rowy&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl81mg1lgttm46isbd28n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl81mg1lgttm46isbd28n.png" alt="Two screenshots of toggle button designs. On the left: Material Design 2, which uses a different background to signify the active button. On the right: Rowy, which uses a line on the bottom of the button to signify the active button." width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  And a standard switch design
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ozuyk97fxttlatqynib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ozuyk97fxttlatqynib.png" alt="Screenshots of toggle switch designs in Windows 11, macOS 12, iOS 15, and Android 12." width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Android 12, switches now have the same fundamental design across the major OSes. This makes it easier for a user to switch between these platforms and reduces cognitive load. &lt;/p&gt;

&lt;h2&gt;
  
  
  Where is UI design headed?
&lt;/h2&gt;

&lt;p&gt;The overarching theme I notice in all these design decisions is that designers put user interface design into perspective. They’re acutely aware of where digital interfaces fit in the human experience and interact with the physical world.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The convergence on outline icons to mimic text recognizes the importance of icons in communication.&lt;/li&gt;
&lt;li&gt;Increasingly personalized interface elements—especially the direction Material You is taking—acknowledge how people like to make the things they use their own, including the tech they use every day.&lt;/li&gt;
&lt;li&gt;Harmoniously rounded corners and inset elements are inspired by physical objects and industrial design, so our software matches the hardware more closely.&lt;/li&gt;
&lt;li&gt;The use of variable fonts with optical sizes hearkens back to typography’s origins, and along with differentiating elements beyond color, they improve usability for all, especially those with disabilities.&lt;/li&gt;
&lt;li&gt;The more minor things help, too: reducing visual clutter to elevate content allows users to focus on what they want to do. And using a standard switch design eliminates any cognitive load required to figure out what a UI element does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgnnc8jo7vtxje8jk1e4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgnnc8jo7vtxje8jk1e4.jpg" alt="Woman with VR headset on and controllers in hands at sunset." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

Photo by &lt;a href="https://unsplash.com/@viniciusamano?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Vinicius "amnx" Amano&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;




&lt;p&gt;This kind of thinking behind UI design sets us up nicely for the next generation of computing centered on &lt;del&gt;AR/VR&lt;/del&gt; the &lt;a href="https://en.wikipedia.org/wiki/Metaverse" rel="noopener noreferrer"&gt;metaverse&lt;/a&gt;, where metaverse-first experiences will have to answer how they improve the human experience and interact with the physical world. Designers are already looking at how to adapt design systems for this change. Looking at the decisions above, applying color science (Material’s HCT takes viewing conditions into account) and inset elements that elevate content help these UI elements transition from the 2D world to the 3D metaverse.&lt;/p&gt;




&lt;p&gt;Hey 👋 if you enjoyed this, you can follow me on Twitter @nots_dney, where I talk more about my work as a front-end engineer. Also, check out &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=UI%20design%20trends%20of%20today%20and%20how%20to%20apply%20them%20in%20your%20apps"&gt;Rowy&lt;/a&gt;, where we’re building an improved user experience for working on cloud platforms, starting with Google Cloud and Firebase.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rowyio" rel="noopener noreferrer"&gt;
        rowyio
      &lt;/a&gt; / &lt;a href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;
        rowy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;a href="https://www.rowy.io/" rel="nofollow noopener noreferrer"&gt;
&lt;img width="100%" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F307298%2F218350866-cfd7c011-2247-4074-8b1d-06c26a4d0b96.png"&gt;
&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨
&lt;/h4&gt;
&lt;/div&gt;
&lt;p&gt;
Connect to your database and create Cloud Functions in low-code - without leaving your browser.&lt;br&gt;
Focus on building your apps
Low-code for Firebase and Google Cloud
&lt;/p&gt;

&lt;div&gt;
&lt;p&gt;&lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0ccce7901558d647206ce90e969c63d80ed1931e599e51fa9376491d5668ee19/68747470733a2f2f646362616467652e76657263656c2e6170702f6170692f7365727665722f666a4275676d767a5a50" alt="Rowy Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
    &lt;a href="http://www.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Website&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="http://docs.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Documentation&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Chat with us&lt;/b&gt;&lt;/a&gt; • 
    &lt;a href="https://twitter.com/rowyio" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Twitter&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/rowyio/rowy/commits/rc" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/479261b5c985c6803c34b0ba6da8dee3f2a3d1e261853fab7e9974fb48b7b0ad/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f726f7779696f2f726f77792f7263" alt="Last commit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rowyio/rowy/stargazers/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8adcfa7e078b22d59fc173e099b2337d30d5fe27e3c58a223968ac6477d94250/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f726f7779696f2f726f7779" alt="GitHub stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Demo 🛝&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;💥 Explore Rowy on &lt;a href="https://demo.rowy.io/" rel="nofollow noopener noreferrer"&gt;live demo playground&lt;/a&gt; 💥&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features ✨&lt;/h2&gt;

&lt;/div&gt;


  
    
    

    &lt;span class="m-1"&gt;20211004-RowyWebsite.mp4&lt;/span&gt;
    
  

  

  


&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Powerful spreadsheet interface for Firestore&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CMS for Firestore&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;Bulk import or export data - csv, json, tsv&lt;/li&gt;
&lt;li&gt;Sort and filter by row values&lt;/li&gt;
&lt;li&gt;Lock, Freeze, Resize, Hide and Rename columns&lt;/li&gt;
&lt;li&gt;Multiple views for the same collection&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Automate with cloud functions and ready made extensions&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Build cloud functions workflows on field level data changes
&lt;ul&gt;
&lt;li&gt;Use any NPM modules or APIs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Connect to your favourite tool with pre-built code blocks or create your own

&lt;ul&gt;
&lt;li&gt;SendGrid, Algolia, Twilio, Bigquery and more&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Rich and flexible data fields&lt;/h3&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;a href="https://docs.rowy.io/field-types/supported-fields" rel="nofollow noopener noreferrer"&gt;30+&lt;/a&gt;…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>ux</category>
      <category>css</category>
      <category>webdev</category>
      <category>design</category>
    </item>
    <item>
      <title>How we built our virtual live event platform with Firestore and Firetable</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Thu, 18 Feb 2021 01:07:37 +0000</pubDate>
      <link>https://dev.to/notsidney/how-we-built-our-virtual-live-event-platform-with-firestore-and-firetable-j11</link>
      <guid>https://dev.to/notsidney/how-we-built-our-virtual-live-event-platform-with-firestore-and-firetable-j11</guid>
      <description>&lt;h3&gt;
  
  
  Behind the scenes from Antler’s virtual Demo Day
&lt;/h3&gt;

&lt;p&gt;As in-person events continue to be held online amid the ongoing Covid-19 pandemic, many events are finding new ways to connect with their audiences and deliver more personal, engaging experiences. It’s no different at Antler — we used to run physical Demo Day events to exhibit our portfolio companies, and now, we need to adapt the format for a decentralised, virtual audience.&lt;/p&gt;

&lt;p&gt;I’ve previously written about our first virtual event while explaining &lt;a href="https://medium.com/swlh/gatsby-won-against-next-js-in-this-head-to-head-7d446569ec57" rel="noopener noreferrer"&gt;why we chose Gatsby over Next.js&lt;/a&gt; to achieve excellent performance. Now we wanted to build on top of this foundation to deliver an even better live experience.&lt;/p&gt;

&lt;p&gt;We launched this new platform for our virtual &lt;a href="https://demoday.antler.co/sydney" rel="noopener noreferrer"&gt;Demo Day Rewired&lt;/a&gt; event in Sydney. For the first time, viewers could have their questions answered live, we actively surfaced useful information about each startup as they presented, and we made it even easier to get in touch with each startup’s founders.&lt;/p&gt;

&lt;p&gt;But Antler is present in 12 locations, each of which runs its own Demo Day, and we wanted to enable each site to deliver the same live experience we had in Sydney in one easy-to-use and customisable platform.&lt;/p&gt;

&lt;p&gt;Here’s how we did it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Enhancing interactivity with Firestore Listeners
&lt;/h1&gt;

&lt;p&gt;From the start, we envisioned this new virtual event experience would augment the live viewing experience by updating the page with relevant information as the live stream progresses, without the user ever having to reload the page.&lt;/p&gt;

&lt;p&gt;Specifically, we wanted to make it a lot easier for viewers to learn more about each startup as they presented by showing&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  more information about what they do,&lt;/li&gt;
&lt;li&gt;  background on who the founders are, and&lt;/li&gt;
&lt;li&gt;  a link to their slide deck that the viewer can read and download.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All we needed was a way to say &lt;em&gt;which&lt;/em&gt; startup was currently presenting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2kvf38pst132kattyej.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2kvf38pst132kattyej.gif" width="600" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We initially used Algolia to get a restricted public subset of our startups’ data and reduce the number of bytes downloaded by the user with its small JavaScript library &lt;a href="https://bundlephobia.com/result?p=algoliasearch" rel="noopener noreferrer"&gt;(at only 7.5 kB gzipped)&lt;/a&gt;. Unfortunately, by design, Algolia only fetches data &lt;em&gt;once,&lt;/em&gt; and we can’t easily update the front-end whenever data changes. So if we were to continue using it, we would need to periodically fetch new data — a very inefficient method, especially when there are no changes to data between each fetch.&lt;/p&gt;

&lt;p&gt;But since we already stored all our data on a Firestore database, we could use &lt;a href="https://firebase.google.com/docs/firestore/query-data/listen" rel="noopener noreferrer"&gt;Listeners&lt;/a&gt; to get realtime updates to our data effortlessly. Then, we could store which startup was currently presenting in a single Firestore document, listen to that doc’s updates, and update the page accordingly. And we don’t even have to do any particular configuration or write new code thanks to community-supported libraries like &lt;a href="https://www.npmjs.com/package/react-firebase-hooks" rel="noopener noreferrer"&gt;&lt;code&gt;react-firebase-hooks&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this setup, we could also make it much easier for viewers to contact each startup through a specialised pop-up form. This experience is a marked improvement from the previous in-person one, which asked viewers to physically divert attention from the presenters and open a specific URL on their phone. Now they could do that without even switching the tab — that’s a lot less work required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjtl1tx8yzklgxhbzbly0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjtl1tx8yzklgxhbzbly0.gif" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, we partnered with Slido, which provides easy tools to add Q&amp;amp;A and polls for live events, allowing viewers’ questions to be answered by presenters live on air.&lt;/p&gt;

&lt;p&gt;Adding these features enhances the level of interactivity in the live experience. It shows the viewer that we truly rethought the event format for an online virtual audience and not just a rudimentary port of the original.&lt;/p&gt;

&lt;h1&gt;
  
  
  Enabling customisation with Firetable
&lt;/h1&gt;

&lt;p&gt;Now that we’d settled on using Firestore to show the currently presenting startup in realtime, we could also use the same document to store the configuration for each event, such as the event title, time, and live stream URL.&lt;/p&gt;

&lt;p&gt;We wanted our global teams to configure their Demo Day as they see fit, so we needed a user-friendly UI to expose this config document to them. Thankfully, we didn’t have to build an entirely new UI to facilitate this, and we avoided the additional baggage of having to update the code when we add a new setting or creating a new UI element to configure a specific field.&lt;/p&gt;

&lt;p&gt;We were already using &lt;a href="https://firetable.io/?utm_source=Medium&amp;amp;utm_medium=website&amp;amp;utm_campaign=How%20we%20built%20our%20virtual%20live%20event%20platform%20with%20Firestore%20and%20Firetable&amp;amp;utm_content=MediumArticle" rel="noopener noreferrer"&gt;Firetable&lt;/a&gt;, our open-source project that combines a spreadsheet UI with the full power of Firestore. At Antler, it allows our team to easily manage and update our internal database and automate day-to-day tasks that involve it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9v7kwoamn5zol3bf30yc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9v7kwoamn5zol3bf30yc.gif" width="760" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We could continue using Firetable to expose those configuration fields directly — from text fields to toggles to dropdowns that link to other documents in the database — with minimal extra work on our part and little additional training for our team. Now we just had to decide &lt;em&gt;what&lt;/em&gt; can be configured and write the code to enable that in our Demo Day web app.&lt;/p&gt;

&lt;p&gt;While we initially used this setup for configuring basic settings for each event, we realised we could also use it to give our team full control over the viewing experience. Our Demo Day app has four pages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; a registration page to collect info on the viewer,&lt;/li&gt;
&lt;li&gt; a pre-event page so those who just registered can preview the startups,&lt;/li&gt;
&lt;li&gt; the live stream page with interactivity, and&lt;/li&gt;
&lt;li&gt; a post-event page so viewers can rewatch individual pitches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of setting timers to switch between states, we could now allow our team to change the page displayed through toggles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favrnd85at3b5ozh40q05.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favrnd85at3b5ozh40q05.jpg" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enabling this is especially useful if, for example, the live stream was running late and they weren’t ready to switch over from the pre-event page. And since it directly updates the Firestore document, it would also trigger the Firestore listener on the front-end, so again there would be zero page refreshes required. We were even able to extend this by adding small tweaks requested by one event as toggles, so we don’t modify other events and to let future events opt-in to these tweaks.&lt;/p&gt;

&lt;h1&gt;
  
  
  Ensuring performance with Gatsby
&lt;/h1&gt;

&lt;p&gt;While we were willing to accept the small performance cost of switching from the lean Algolia library to the beefier Firestore one, I wanted to continue to improve the performance of the app, especially during the first load.&lt;/p&gt;

&lt;p&gt;As detailed in &lt;a href="https://medium.com/swlh/gatsby-won-against-next-js-in-this-head-to-head-7d446569ec57" rel="noopener noreferrer"&gt;the previous article&lt;/a&gt;, we had minimal use of static site generation: we only used it to render the page skeleton while we waited for the Algolia query to finish. We wanted to eliminate this by including a snapshot of the config document as part of Gatsby’s static build. Then, when the Firestore Listener first loads, it will update the page data with the latest (mostly minor) updates.&lt;/p&gt;

&lt;p&gt;Also, embedding configs in the static build became a necessity since we allow our team to set each event’s meta tags, which Facebook, LinkedIn, and Google use to display on their sites. These platforms’ crawlers perform a single HTTP request on the main webpage and don’t run any JavaScript (such as the Firestore Listener), so we need to include this in the static build.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7a6789mfit1ihu6m42x4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7a6789mfit1ihu6m42x4.jpg" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To retrieve the Firestore document during Gatsby’s build process, we used &lt;a href="https://www.gatsbyjs.com/plugins/@deanc/gatsby-source-firestorer/?=firestore" rel="noopener noreferrer"&gt;&lt;code&gt;@deanc/gatsby-source-firestorer&lt;/code&gt;&lt;/a&gt; so the doc can be accessible in Gatsby’s GraphQL layer. Now I know what you’re thinking: this seems like unnecessary extra work to achieve this &lt;em&gt;in Gatsby&lt;/em&gt; and looks a lot simpler to implement in something like… Next.js. Unfortunately, we didn’t have enough time to build and test a Next.js implementation, and the current Gatsby implementation achieved the same result for our viewers anyway.&lt;/p&gt;

&lt;p&gt;Now that we cached our configs for the static build, we could rebuild the site at any time so that the viewer gets the latest data right as they load the page. But the question was now: &lt;em&gt;when&lt;/em&gt; do we rebuild the site? We couldn’t do this every time the config doc was updated — this would be every time a new startup presents, or every few minutes — and each rebuild would only update a small portion of the page. Rebuilding every time would be very inefficient and cost unnecessary build minutes from Netlify.&lt;/p&gt;

&lt;p&gt;We knew we had specific situations where a rebuild is necessary: when our team updates the social media meta tags and when they switch the current page. If the static-generated site displays another page to the one set in the config doc, it will flash to the new page when the Listener loads. This flashing is a poor and potentially confusing user experience, especially if a previously-registered user logs on to the live stream page, but has to see a flash of the registration page.&lt;/p&gt;

&lt;p&gt;Luckily, we could use &lt;a href="https://docs.netlify.com/configure-builds/build-hooks/" rel="noopener noreferrer"&gt;Netlify’s Build Hooks&lt;/a&gt; feature to trigger a new build via a Cloud Function. Then, our team could activate it right in Firetable with the single click of a button, again providing full control of the virtual event to our team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3do6ijuzy4cgye8up6xh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3do6ijuzy4cgye8up6xh.gif" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  More performance wins with thumbnails
&lt;/h1&gt;

&lt;p&gt;At the end of the previous article, I wrote about how we were displaying uncompressed images uploaded directly by our founders: this meant we were loading potentially lossless images, thousands of pixels wide, for an area that was only 80px wide.&lt;/p&gt;

&lt;p&gt;I also complained about the lack of WebP support in Safari (i.e. all iOS devices). Thankfully, the next major version, &lt;a href="https://www.macrumors.com/2020/06/22/webp-safari-14/" rel="noopener noreferrer"&gt;Safari 14, will support this&lt;/a&gt;. Unfortunately for WebP, I came across an article via Hacker News that details why &lt;a href="https://siipo.la/blog/is-webp-really-better-than-jpeg" rel="noopener noreferrer"&gt;WebP isn’t any better than a well-compressed JPEG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Considering these two factors, I decided to stick with JPEG and PNG when writing a Cloud Function that generates multiple, lossy-compressed thumbnails when images are upload. (I first wrote it for &lt;a href="https://www.github.com/AntlerVC/firetable/tree/master/cloud_functions%2Ffunctions%2Fsrc%2FcompressedThumbnail%2Findex.ts" rel="noopener noreferrer"&gt;displaying thumbnails on Firetable&lt;/a&gt; and reused it here.) These thumbnails reduced the number of bytes loaded significantly, down from multiple megabytes to just hundreds of kilobytes!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil5m40e8qj1miyk0dayz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil5m40e8qj1miyk0dayz.jpg" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, most viewers don’t have to look at whitespace when scrolling down the page or when new startups appear on-screen — those bytes should be downloading the live stream anyway. Our team can now also upload any image without worrying about its size. Plus, we won’t have to ask for images to be uploaded at specific sizes, and they won’t have to resize it in an image editor — or even learn how to use one.&lt;/p&gt;




&lt;p&gt;Thanks for reading! While I still can’t link the source code, you can have a look at our &lt;a href="https://demoday.antler.co/?utm_source=Medium&amp;amp;utm_medium=website&amp;amp;utm_campaign=How%20we%20built%20our%20virtual%20live%20event%20platform%20with%20Firestore%20and%20Firetable&amp;amp;utm_content=MediumArticle" rel="noopener noreferrer"&gt;virtual Demo Day events here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can follow me on Twitter &lt;a href="https://twitter.com/nots_dney" rel="noopener noreferrer"&gt;@nots_dney&lt;/a&gt; as I write more about what we’re building with &lt;a href="https://firetable.io/?utm_source=Medium&amp;amp;utm_medium=website&amp;amp;utm_campaign=How%20we%20built%20our%20virtual%20live%20event%20platform%20with%20Firestore%20and%20Firetable&amp;amp;utm_content=MediumArticle" rel="noopener noreferrer"&gt;Firetable&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rowyio" rel="noopener noreferrer"&gt;
        rowyio
      &lt;/a&gt; / &lt;a href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;
        rowy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;a href="https://www.rowy.io/" rel="nofollow noopener noreferrer"&gt;
&lt;img width="100%" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F307298%2F218350866-cfd7c011-2247-4074-8b1d-06c26a4d0b96.png"&gt;
&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨
&lt;/h4&gt;
&lt;/div&gt;
&lt;p&gt;
Connect to your database and create Cloud Functions in low-code - without leaving your browser.&lt;br&gt;
Focus on building your apps
Low-code for Firebase and Google Cloud
&lt;/p&gt;

&lt;div&gt;
&lt;p&gt;&lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0ccce7901558d647206ce90e969c63d80ed1931e599e51fa9376491d5668ee19/68747470733a2f2f646362616467652e76657263656c2e6170702f6170692f7365727665722f666a4275676d767a5a50" alt="Rowy Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
    &lt;a href="http://www.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Website&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="http://docs.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Documentation&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Chat with us&lt;/b&gt;&lt;/a&gt; • 
    &lt;a href="https://twitter.com/rowyio" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Twitter&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/rowyio/rowy/commits/rc" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/479261b5c985c6803c34b0ba6da8dee3f2a3d1e261853fab7e9974fb48b7b0ad/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f726f7779696f2f726f77792f7263" alt="Last commit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rowyio/rowy/stargazers/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8adcfa7e078b22d59fc173e099b2337d30d5fe27e3c58a223968ac6477d94250/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f726f7779696f2f726f7779" alt="GitHub stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Demo 🛝&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;💥 Explore Rowy on &lt;a href="https://demo.rowy.io/" rel="nofollow noopener noreferrer"&gt;live demo playground&lt;/a&gt; 💥&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features ✨&lt;/h2&gt;

&lt;/div&gt;


  
    
    

    &lt;span class="m-1"&gt;20211004-RowyWebsite.mp4&lt;/span&gt;
    
  

  

  


&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Powerful spreadsheet interface for Firestore&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CMS for Firestore&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;Bulk import or export data - csv, json, tsv&lt;/li&gt;
&lt;li&gt;Sort and filter by row values&lt;/li&gt;
&lt;li&gt;Lock, Freeze, Resize, Hide and Rename columns&lt;/li&gt;
&lt;li&gt;Multiple views for the same collection&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Automate with cloud functions and ready made extensions&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Build cloud functions workflows on field level data changes
&lt;ul&gt;
&lt;li&gt;Use any NPM modules or APIs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Connect to your favourite tool with pre-built code blocks or create your own

&lt;ul&gt;
&lt;li&gt;SendGrid, Algolia, Twilio, Bigquery and more&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Rich and flexible data fields&lt;/h3&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;a href="https://docs.rowy.io/field-types/supported-fields" rel="nofollow noopener noreferrer"&gt;30+&lt;/a&gt;…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>We refactored 10K lines of code in our open-source React project</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Tue, 09 Feb 2021 02:15:46 +0000</pubDate>
      <link>https://dev.to/notsidney/we-refactored-10k-lines-of-code-in-our-open-source-react-project-1a9a</link>
      <guid>https://dev.to/notsidney/we-refactored-10k-lines-of-code-in-our-open-source-react-project-1a9a</guid>
      <description>&lt;h3&gt;
  
  
  Know the how, the when, and the why behind our refactoring
&lt;/h3&gt;

&lt;p&gt;When working on any project, especially in the MVP stage, we as developers often prioritise one thing above all else when writing code: making sure it &lt;em&gt;works&lt;/em&gt;. Unfortunately, this can mean we write code hyperfocused on the MVP’s requirements, so we end up with code that is hard to maintain or cumbersome to expand. Of course, this isn’t a problem one can easily avoid since we don’t live in an ideal world. The forces of time are always against us — sometimes we just need to push something out.&lt;/p&gt;

&lt;p&gt;I’m a software engineer building &lt;a href="https://rowy.io/?utm_source=Medium&amp;amp;utm_medium=blog&amp;amp;utm_content=MediumArticle&amp;amp;utm_campaign=We%20Refactored%2010%2C000%2B%20Lines%20of%20Code%20in%20Our%20Open-Source%20React%C2%A0Project" rel="noopener noreferrer"&gt;Rowy&lt;/a&gt;, an open-source React app that combines a spreadsheet UI with Firestore and Firebase’s full power. We ran into this exact issue with some fundamental code: the code for all the different &lt;a href="https://docs.rowy.io/field-types/supported-fields" rel="noopener noreferrer"&gt;field types&lt;/a&gt;, from the simple &lt;code&gt;ShortText&lt;/code&gt; to the complex &lt;code&gt;ConnectTable&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;After refactoring, we now have a more solid foundation to build more features, we squashed a few hard-to-find bugs, and we now even a &lt;a href="https://docs.rowy.io/field-types/add" rel="noopener noreferrer"&gt;guide on how our contributors can write new field types&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When code smells and tech debt became big problems
&lt;/h2&gt;

&lt;p&gt;When we first started building Rowy, the idea was to build a spreadsheet interface, and naturally, the resulting product closely matched that. Looking at old screenshots, it’s remarkable how closely it resembles spreadsheet programs like Excel and Google Sheets:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkgkgz3y4szlo1q1r6psq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fkgkgz3y4szlo1q1r6psq.jpg" alt="An old screenshot of Rowy with a UI very similar to a spreadsheet program" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

A screenshot of an old build from February 2020




&lt;p&gt;We used &lt;a href="https://adazzle.github.io/react-data-grid/" rel="noopener noreferrer"&gt;React Data Grid&lt;/a&gt; to implement this. It accepts “formatter” components used to render cells and “editor” components used to edit cells when a user double-clicks the cell. We structured our code around this, with &lt;code&gt;formatters&lt;/code&gt; and &lt;code&gt;editors&lt;/code&gt; becoming folders alongside the code for &lt;code&gt;Table&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few months later, we added the &lt;code&gt;SideDrawer&lt;/code&gt;, a form-like UI that slides over the main table. It was designed to make it easier to edit all the fields of a single row at a time, which we found was an everyday workflow for our users. At the time, it seemed like the most logical way to structure this new code was similar to how we structured the &lt;code&gt;Table&lt;/code&gt;, so we created a &lt;code&gt;Fields&lt;/code&gt; folder in the &lt;code&gt;SideDrawer&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F54pxk5s8j04ficgwnz4l.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F54pxk5s8j04ficgwnz4l.gif" alt="Screen recording of a user selecting a cell and opening the side drawer" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But as we maintained this code, cracks began to show.&lt;/p&gt;

&lt;p&gt;One of our distinctive field types is &lt;code&gt;Action&lt;/code&gt;, which displays a button on the table that lets the user run code based on the row’s data using Firebase Cloud Functions and show the results in the very same cell. We’ve used it for novel applications such as setting our database’s access controls from right within Rowy using &lt;a href="https://firebase.google.com/docs/auth/admin/custom-claims" rel="noopener noreferrer"&gt;Firebase Auth custom roles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We had a bug where the Cloud Function wasn’t receiving the right parameters when called by &lt;code&gt;Action&lt;/code&gt; cells. But to update the code, we had to do it in &lt;em&gt;two separate locations&lt;/em&gt; — the &lt;code&gt;Table&lt;/code&gt; formatter and the &lt;code&gt;SideDrawer&lt;/code&gt; field. Not only that, it turns out we had &lt;em&gt;duplicated&lt;/em&gt; the code calling the Cloud Function due to time constraints. There was simply no clear location for that shared code, and the bug was too high-priority for us to have time to answer that question correctly.&lt;/p&gt;

&lt;p&gt;The final straw was when we noticed we had inconsistently implemented the column lock feature. Some fields remained editable in the &lt;code&gt;SideDrawer&lt;/code&gt; but not the &lt;code&gt;Table&lt;/code&gt; or vice versa, or we didn’t implement it at all for that field. This was a result of adding this feature &lt;em&gt;after&lt;/em&gt; we had implemented the minimum requirements for each field type, so we had to go through each &lt;code&gt;Table&lt;/code&gt; formatter and each &lt;code&gt;SideDrawer&lt;/code&gt; field — double the number of field types we had. This tedious manual process was clearly prone to errors.&lt;/p&gt;

&lt;p&gt;At this point, we knew it was time to refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactoring for success
&lt;/h2&gt;

&lt;p&gt;We identified the main problem: we &lt;strong&gt;didn’t have a single place to store code&lt;/strong&gt; for each field type. It was scattered throughout the codebase: &lt;code&gt;Table&lt;/code&gt; formatters and editors, &lt;code&gt;SideDrawer&lt;/code&gt; fields, column settings, and more. This scattering rapidly inflated the cost for adding new features for field types and weeding out bugs.&lt;/p&gt;

&lt;p&gt;The first thing we did was invert our approach to code structure entirely — instead of grouping code by each feature that would &lt;em&gt;use&lt;/em&gt; the field types, we grouped the code by the field types themselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fgpdidsjuxj66n9w7rzy5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fgpdidsjuxj66n9w7rzy5.png" alt="Diagram visualising the previous paragraph" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new approach translates to a new top-level component folder called &lt;code&gt;fields&lt;/code&gt;, comprising subfolders for each field type, and within each, we have files such as &lt;code&gt;TableCell.tsx&lt;/code&gt; and &lt;code&gt;SideDrawerField.tsx&lt;/code&gt;. Then we could export these features in a config object, so all this code would only need to be imported once by the consumer. This is similar to a problem solved by React Hooks: &lt;a href="https://reactjs.org/docs/hooks-intro.html#complex-components-become-hard-to-understand" rel="noopener noreferrer"&gt;grouping related code&lt;/a&gt; and not having to think about lifecycle methods.&lt;/p&gt;

&lt;p&gt;This approach also simplifies how we import a field’s code throughout the codebase. Previously in the &lt;code&gt;Table&lt;/code&gt; and &lt;code&gt;SideDrawer&lt;/code&gt;, we would rely on &lt;code&gt;switch&lt;/code&gt; statements that looped through each field type until we could fetch the correct component &lt;em&gt;and&lt;/em&gt; import each field one by one. So whenever we added a new field type, we would also have to add a new entry to these &lt;code&gt;switch&lt;/code&gt; blocks — again ballooning the cost of development. Instead, we could create a single array with every field config, then share it across the codebase. So we only need to define a new field type &lt;em&gt;once.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Additionally, the config object lets us quickly implement new features and ensure &lt;em&gt;all&lt;/em&gt; fields do so correctly. Now we could simply check if a field’s config has a property. And since we’re using TypeScript, each config object must implement &lt;a href="https://github.com/rowyio/rowy/blob/6e81191fc26714c4c212bae19cfc4c16440c322f/src/components/fields/types.ts#L9" rel="noopener noreferrer"&gt;our interface&lt;/a&gt;, which can enforce certain features (properties of the interface) to be of a particular type, such as a React component accepting specific props. This new functionality allowed us to fix column locking implementation and made it much easier to develop a new feature, default values for columns. All we had to do was add a new property to the interface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2n9v8omxdsf9om9rb6ke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2n9v8omxdsf9om9rb6ke.png" alt="Example of the significant amount of code we were able to reduce with the new code structure" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this in mind, our refactor not only made our code easier to maintain and fix bugs — but it also provided a much more solid foundation on which we can build advanced features for fields and removing extra costs to development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for the future
&lt;/h2&gt;

&lt;p&gt;Of course, we could have avoided all this pain and extra work had we initially gone with this approach. But we don’t live in an ideal world. All of the non-ideal solutions I mentioned above were the result of time constraints on our end, especially when we were working on other projects simultaneously, which directly impacted day-to-day work.&lt;/p&gt;

&lt;p&gt;Many of us work for a business that doesn’t have excellent code quality as its primary goal. As developers, we are hired to build tech solutions that meet business requirements, and the “how” is abstracted away. In this case, however, our poorly-structured code and the amount of accrued tech debt were directly impacting our ability to work.&lt;/p&gt;

&lt;p&gt;And whilst writing this article, I came across &lt;a href="https://refactoring.guru/" rel="noopener noreferrer"&gt;Refactoring.Guru&lt;/a&gt;, an excellent guide on refactoring. We clearly satisfied their first recommendation on when to refactor: “When you’re doing something for the third time, start refactoring.”&lt;/p&gt;

&lt;p&gt;This experience has taught us many valuable lessons on code structure and when a refactor is necessary. I hope you’ve gained some insights by reading about our journey.&lt;/p&gt;




&lt;p&gt;Thanks for reading! You can find out more about Rowy below and follow me on Twitter &lt;a href="https://twitter.com/nots_dney" rel="noopener noreferrer"&gt;@nots_dney&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rowyio" rel="noopener noreferrer"&gt;
        rowyio
      &lt;/a&gt; / &lt;a href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;
        rowy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;a href="https://www.rowy.io/" rel="nofollow noopener noreferrer"&gt;
&lt;img width="100%" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F307298%2F218350866-cfd7c011-2247-4074-8b1d-06c26a4d0b96.png"&gt;
&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨
&lt;/h4&gt;
&lt;/div&gt;
&lt;p&gt;
Connect to your database and create Cloud Functions in low-code - without leaving your browser.&lt;br&gt;
Focus on building your apps
Low-code for Firebase and Google Cloud
&lt;/p&gt;

&lt;div&gt;
&lt;p&gt;&lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0ccce7901558d647206ce90e969c63d80ed1931e599e51fa9376491d5668ee19/68747470733a2f2f646362616467652e76657263656c2e6170702f6170692f7365727665722f666a4275676d767a5a50" alt="Rowy Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
    &lt;a href="http://www.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Website&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="http://docs.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Documentation&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Chat with us&lt;/b&gt;&lt;/a&gt; • 
    &lt;a href="https://twitter.com/rowyio" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Twitter&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/rowyio/rowy/commits/rc" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/479261b5c985c6803c34b0ba6da8dee3f2a3d1e261853fab7e9974fb48b7b0ad/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f726f7779696f2f726f77792f7263" alt="Last commit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rowyio/rowy/stargazers/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8adcfa7e078b22d59fc173e099b2337d30d5fe27e3c58a223968ac6477d94250/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f726f7779696f2f726f7779" alt="GitHub stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Demo 🛝&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;💥 Explore Rowy on &lt;a href="https://demo.rowy.io/" rel="nofollow noopener noreferrer"&gt;live demo playground&lt;/a&gt; 💥&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features ✨&lt;/h2&gt;

&lt;/div&gt;


  
    
    

    &lt;span class="m-1"&gt;20211004-RowyWebsite.mp4&lt;/span&gt;
    
  

  

  


&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Powerful spreadsheet interface for Firestore&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CMS for Firestore&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;Bulk import or export data - csv, json, tsv&lt;/li&gt;
&lt;li&gt;Sort and filter by row values&lt;/li&gt;
&lt;li&gt;Lock, Freeze, Resize, Hide and Rename columns&lt;/li&gt;
&lt;li&gt;Multiple views for the same collection&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Automate with cloud functions and ready made extensions&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Build cloud functions workflows on field level data changes
&lt;ul&gt;
&lt;li&gt;Use any NPM modules or APIs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Connect to your favourite tool with pre-built code blocks or create your own

&lt;ul&gt;
&lt;li&gt;SendGrid, Algolia, Twilio, Bigquery and more&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Rich and flexible data fields&lt;/h3&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;a href="https://docs.rowy.io/field-types/supported-fields" rel="nofollow noopener noreferrer"&gt;30+&lt;/a&gt;…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to useRef to fix React performance issues</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Tue, 24 Nov 2020 00:59:18 +0000</pubDate>
      <link>https://dev.to/notsidney/how-to-useref-to-fix-react-performance-issues-e8p</link>
      <guid>https://dev.to/notsidney/how-to-useref-to-fix-react-performance-issues-e8p</guid>
      <description>&lt;h3&gt;
  
  
  And how we stopped our React Context re-rendering everything
&lt;/h3&gt;

&lt;p&gt;Refs are a seldom-used feature in React. If you’ve read the &lt;a href="https://reactjs.org/docs/refs-and-the-dom.html" rel="noopener noreferrer"&gt;official React guide&lt;/a&gt;, they’re introduced as an “escape hatch” out of the typical React data flow, with a warning to use them sparingly, and they’re primarily billed as the correct way to access a component’s underlying DOM element.&lt;/p&gt;

&lt;p&gt;But alongside the concept of Hooks, the React team introduced the &lt;a href="https://reactjs.org/docs/hooks-reference.html#useref" rel="noopener noreferrer"&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/a&gt; Hook, which extends this functionality:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useRef()&lt;/code&gt; is useful for more than the &lt;code&gt;ref&lt;/code&gt; attribute. It’s &lt;a href="https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables" rel="noopener noreferrer"&gt;handy for keeping any mutable value around&lt;/a&gt; similar to how you’d use instance fields in classes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While I overlooked this point when the new Hook APIs launched, it proved to be surprisingly useful.&lt;/p&gt;

&lt;p&gt;👉 Click here to skip to the solution and code snippets&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I’m a software engineer working on &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=How%20to%20useRef%20to%20Fix%20React%20Performance%20Issues"&gt;Rowy&lt;/a&gt;, an open-source React app that combines a spreadsheet UI with the full power of Firestore and Firebase. One of its key features is the &lt;strong&gt;side drawer&lt;/strong&gt;, a form-like UI to edit a single row, that slides over the main table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fj0revf9wzbzehbcvvc40.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fj0revf9wzbzehbcvvc40.gif" alt="Screen recording of a user selecting a cell and opening the side drawer" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the user clicks on a cell in the table, the side drawer can be opened to edit that cell’s corresponding row. In other words, what we render in the side drawer is dependent on the currently selected row — this should be stored in state.&lt;/p&gt;

&lt;p&gt;The most logical place to put this state is within the side drawer component itself because when the user selects a different cell, it should &lt;em&gt;only&lt;/em&gt; affect the side drawer. However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We need to &lt;em&gt;set&lt;/em&gt; this state from the table component. We’re using &lt;a href="https://github.com/adazzle/react-data-grid" rel="noopener noreferrer"&gt;&lt;code&gt;react-data-grid&lt;/code&gt;&lt;/a&gt; to render the table itself, and it accepts a callback prop that’s called whenever the user selects a cell. Currently, it’s the only way to respond to that event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;But the side drawer and table components are siblings, so they can’t directly access each other’s state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React’s recommendation is to &lt;a href="https://reactjs.org/docs/lifting-state-up.html" rel="noopener noreferrer"&gt;lift this state&lt;/a&gt; to the components’ closest common ancestor, in this case, &lt;code&gt;TablePage&lt;/code&gt;. But we decided against moving the state here because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;TablePage&lt;/code&gt; didn’t contain any state and was primarily a container for the table and side drawer components, neither of which received any props. We preferred to keep it this way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We were already sharing a lot of “global” data via a &lt;a href="https://reactjs.org/docs/context.html" rel="noopener noreferrer"&gt;context&lt;/a&gt; located close to the root of the component tree, and we felt it made sense to add this state to that central data store.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;small&gt;Side note: even if we put the state in &lt;code&gt;TablePage&lt;/code&gt;, we would have run into the same problem below anyway.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The problem was whenever the user selected a cell or opened the side drawer, the update to this global context would cause &lt;strong&gt;the entire app to re-render&lt;/strong&gt;. This included the main table component, which could have dozens of cells displayed at a time, each with its own editor component. This would result in a render time of around &lt;strong&gt;650 ms&lt;/strong&gt;(!), long enough to see a visible delay in the side drawer’s open animation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Frfq9p10yml5smp2fr34n.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Frfq9p10yml5smp2fr34n.gif" alt="Screen recording of delay in side drawer open animation" width="600" height="337"&gt;&lt;/a&gt;&lt;/p&gt;
Notice the delay between clicking the open button and when the side drawer animates to open



&lt;p&gt;The reason behind this is a key feature of context — the very reason why it’s better to use in React as opposed to global JavaScript variables:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All consumers that are descendants of a Provider will re-render whenever the Provider’s &lt;code&gt;value&lt;/code&gt; prop changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While this Hook into React’s state and lifecycle has served us well so far, it seems we had now shot ourselves in the foot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Aha Moment
&lt;/h2&gt;

&lt;p&gt;We first explored a few different solutions (from &lt;a href="https://github.com/facebook/react/issues/15156#issuecomment-474590693" rel="noopener noreferrer"&gt;Dan Abramov’s post&lt;/a&gt; on the issue) before settling on &lt;code&gt;useRef&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Split the context, i.e. create a new &lt;code&gt;SideDrawerContext&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
The table would still need to consume the new context, which still updates when the side drawer opens, &lt;a href="https://reactjs.org/docs/hooks-reference.html#usecontext" rel="noopener noreferrer"&gt;causing the table to re-render&lt;/a&gt; unnecessarily.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrap the table component in &lt;code&gt;React.memo&lt;/code&gt; or &lt;code&gt;useMemo&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
The table would still need to call &lt;code&gt;useContext&lt;/code&gt; to access the side drawer’s state and &lt;a href="https://reactjs.org/docs/react-api.html#reactmemo" rel="noopener noreferrer"&gt;neither API prevents it from causing re-renders&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memoize the &lt;code&gt;react-data-grid&lt;/code&gt; component used to render the table.&lt;/strong&gt;&lt;br&gt;
This would have introduced more verbosity to our code. We also found it prevented &lt;em&gt;necessary&lt;/em&gt; re-renders, requiring us to spend more time fixing or restructuring our code entirely, solely to implement the side drawer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While reading through the Hook APIs and &lt;code&gt;useMemo&lt;/code&gt; a few more times, I finally came across that point about &lt;code&gt;useRef&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useRef()&lt;/code&gt; is useful for more than the &lt;code&gt;ref&lt;/code&gt; attribute. It’s &lt;a href="https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables" rel="noopener noreferrer"&gt;handy for keeping any mutable value around&lt;/a&gt; similar to how you’d use instance fields in classes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And more importantly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useRef&lt;/code&gt; &lt;em&gt;doesn’t&lt;/em&gt; notify you when its content changes. Mutating the &lt;code&gt;.current&lt;/code&gt; property &lt;strong&gt;doesn’t cause a re-render&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that’s when it hit me:&lt;/p&gt;

&lt;h3&gt;
  
  
  We didn’t need to store the side drawer’s state — we only needed a reference to the function that sets that state.
&lt;/h3&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Keep the open and cell states in the side drawer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a ref to those states and store it in the context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Call the set state functions (inside the side drawer) using the ref from the table when the user clicks on a cell.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ocqb0cu860h0766mboy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ocqb0cu860h0766mboy.png" alt="Diagram representing the preceding list" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code below is an abbreviated version of the code used on Rowy and includes the TypeScript types for the ref:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;small&gt;Side note: since function components run the entire function body on re-render, whenever the &lt;code&gt;cell&lt;/code&gt; or &lt;code&gt;open&lt;/code&gt; state updates (and causes a re-render), &lt;code&gt;sideDrawerRef&lt;/code&gt; always has the latest value in &lt;code&gt;.current&lt;/code&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;This solution proved to be the best since:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The current cell and open states are stored inside the side drawer component itself, the most logical place to put it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The table component has access to its sibling’s state &lt;em&gt;when&lt;/em&gt; it needs it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When either the current cell or open states are updated, it only triggers a re-render for the side drawer component and not any other component throughout the app.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see how this is used in Rowy &lt;a href="https://github.com/rowyio/rowy/blob/6e81191fc26714c4c212bae19cfc4c16440c322f/src/contexts/ProjectContext.tsx#L178" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/rowyio/rowy/blob/6e81191fc26714c4c212bae19cfc4c16440c322f/src/components/SideDrawer/index.tsx#L37" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to useRef
&lt;/h2&gt;

&lt;p&gt;This doesn’t mean you should go ahead and use this pattern for everything you build, though. It’s best used for when you &lt;strong&gt;need to access or update another component’s state at specific times, but your component doesn’t depend or render based on that state&lt;/strong&gt;. React’s core concepts of lifting state up and one-way data flow are enough to cover most app architectures anyway.&lt;/p&gt;




&lt;p&gt;Thanks for reading! You can find out more about &lt;a href="https://rowy.io/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=How%20to%20useRef%20to%20Fix%20React%20Performance%20Issues"&gt;Rowy&lt;/a&gt; below and follow me on Twitter &lt;a href="https://twitter.com/nots_dney" rel="noopener noreferrer"&gt;@nots_dney&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rowyio" rel="noopener noreferrer"&gt;
        rowyio
      &lt;/a&gt; / &lt;a href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;
        rowy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Low-code backend platform. Manage database on spreadsheet-like UI and build cloud functions workflows in JS/TS, all in your browser.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;a href="https://www.rowy.io/" rel="nofollow noopener noreferrer"&gt;
&lt;img width="100%" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F307298%2F218350866-cfd7c011-2247-4074-8b1d-06c26a4d0b96.png"&gt;
&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨
&lt;/h4&gt;
&lt;/div&gt;
&lt;p&gt;
Connect to your database and create Cloud Functions in low-code - without leaving your browser.&lt;br&gt;
Focus on building your apps
Low-code for Firebase and Google Cloud
&lt;/p&gt;

&lt;div&gt;
&lt;p&gt;&lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0ccce7901558d647206ce90e969c63d80ed1931e599e51fa9376491d5668ee19/68747470733a2f2f646362616467652e76657263656c2e6170702f6170692f7365727665722f666a4275676d767a5a50" alt="Rowy Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
    &lt;a href="http://www.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Website&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="http://docs.rowy.io" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Documentation&lt;/b&gt;&lt;/a&gt; •
    &lt;a href="https://discord.gg/fjBugmvzZP" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Chat with us&lt;/b&gt;&lt;/a&gt; • 
    &lt;a href="https://twitter.com/rowyio" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Twitter&lt;/b&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/rowyio/rowy/commits/rc" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/479261b5c985c6803c34b0ba6da8dee3f2a3d1e261853fab7e9974fb48b7b0ad/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f726f7779696f2f726f77792f7263" alt="Last commit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rowyio/rowy/stargazers/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8adcfa7e078b22d59fc173e099b2337d30d5fe27e3c58a223968ac6477d94250/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f726f7779696f2f726f7779" alt="GitHub stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Demo 🛝&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;💥 Explore Rowy on &lt;a href="https://demo.rowy.io/" rel="nofollow noopener noreferrer"&gt;live demo playground&lt;/a&gt; 💥&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features ✨&lt;/h2&gt;

&lt;/div&gt;


  
    
    

    &lt;span class="m-1"&gt;20211004-RowyWebsite.mp4&lt;/span&gt;
    
  

  

  


&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Powerful spreadsheet interface for Firestore&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CMS for Firestore&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;Bulk import or export data - csv, json, tsv&lt;/li&gt;
&lt;li&gt;Sort and filter by row values&lt;/li&gt;
&lt;li&gt;Lock, Freeze, Resize, Hide and Rename columns&lt;/li&gt;
&lt;li&gt;Multiple views for the same collection&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Automate with cloud functions and ready made extensions&lt;/h3&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Build cloud functions workflows on field level data changes
&lt;ul&gt;
&lt;li&gt;Use any NPM modules or APIs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Connect to your favourite tool with pre-built code blocks or create your own

&lt;ul&gt;
&lt;li&gt;SendGrid, Algolia, Twilio, Bigquery and more&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Rich and flexible data fields&lt;/h3&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;a href="https://docs.rowy.io/field-types/supported-fields" rel="nofollow noopener noreferrer"&gt;30+&lt;/a&gt;…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rowyio/rowy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>javascript</category>
      <category>react</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Gatsby won against Next.js in this head-to-head</title>
      <dc:creator>Sidney Alcantara</dc:creator>
      <pubDate>Fri, 24 Apr 2020 14:47:31 +0000</pubDate>
      <link>https://dev.to/notsidney/gatsby-won-against-next-js-in-this-head-to-head-37ka</link>
      <guid>https://dev.to/notsidney/gatsby-won-against-next-js-in-this-head-to-head-37ka</guid>
      <description>&lt;h3&gt;
  
  
  I made the same web app in Gatsby and Next.js and found Gatsby performed better
&lt;/h3&gt;

&lt;p&gt;With the ongoing Covid-19 pandemic and social distancing measures, many events have been forced to migrate to online virtual events. I’m a software engineer at &lt;a href="https://antler.co/" rel="noopener noreferrer"&gt;Antler&lt;/a&gt;, which runs a global startup generator program that usually runs multiple in-person Demo Day events a year that showcase around a dozen new startups, and we faced the same situation.&lt;/p&gt;

&lt;p&gt;We wanted to deliver a solid online experience that puts the focus on the content — our portfolio companies’ pitches. With the wider audience of this event and the fact that it may be a user’s first exposure to Antler’s online presence, we needed to put our best foot forward and ensure it loads &lt;em&gt;fast&lt;/em&gt;. This was a great case for a highly-performant progressive web app (PWA).&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Displaying a skeleton while the data loaded made the app seem faster than just a blank page while the server loaded the data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gatsby’s static output was only &lt;em&gt;slightly&lt;/em&gt; faster than Next.js, but Gatsby’s plugins and documentation made for a better developer experience.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Server-side rendering or static site generation?
&lt;/h2&gt;

&lt;p&gt;For some background: all of our web products are built with React and the Material-UI library, so we stuck with that stack to keep development fast and ensure the new code is compatible with our other projects. The key difference is all our other React apps were bootstrapped with create-react-app and are rendered entirely on the client side (CSR), so users would be faced with a blank white screen while the initial JavaScript is parsed and executed.&lt;/p&gt;

&lt;p&gt;Because we wanted top-notch performance, we were looking into leveraging either server-side rendering (SSR) or static site generation (SSG) to improve this initial load experience.&lt;/p&gt;

&lt;p&gt;Our data will be sourced from Cloud Firestore via Algolia to have more granular, field-level control over public data access with restricted API keys. This also improves query performance: anecdotally, Algolia queries are faster and the Firestore JavaScript SDK is &lt;a href="https://bundlephobia.com/result?p=@firebase/firestore" rel="noopener noreferrer"&gt;86 KB gzipped&lt;/a&gt; compared to Algolia’s, which is &lt;a href="https://bundlephobia.com/result?p=algoliasearch" rel="noopener noreferrer"&gt;7.5 KB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We also wanted to make sure the data we serve is as fresh as possible in case any errors are published live. While the standard SSG practice is to perform these data queries at compile time, we expected frequent writes to our database from both our admin-facing interface, &lt;a href="https://firetable.io/" rel="noopener noreferrer"&gt;firetable&lt;/a&gt;, and our web portal for founders, causing multiple builds to run concurrently. Plus, our database structure may cause irrelevant updates to trigger new builds, making our CI/CD pipeline incredibly inefficient, so we needed the data to be queried whenever a user requests the page. Unfortunately, this means it could not be a “pure” SSG web app.&lt;/p&gt;

&lt;p&gt;Initially, the app was built with Gatsby since we had already been maintaining landing pages built in Gatsby and one of them was &lt;a href="https://firetable.io/" rel="noopener noreferrer"&gt;already bootstrapped with Material-UI&lt;/a&gt;. This initial version produced a page that initially displays a skeleton while the data was being loaded and achieved a first contentful paint time of around 1 second. 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fqsfns92zg6po1edv6ktc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fqsfns92zg6po1edv6ktc.gif" alt="Animation of the web app loading in a skeleton before populating real data" width="1400" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But since the data was being loaded on the client side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Users would have to wait after the initial page load to view the actual content and wait for four network requests to Algolia to finish.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There’s more work for the browser’s JavaScript engine as React needs to switch out the skeletons for the content. That’s extra DOM manipulation!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search engine crawlers might not be able to load the content and they generally prefer static sites.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So over a public holiday long weekend, I decided to experiment with a server-rendered version with Next.js. Lucky for me, Material-UI already had an &lt;a href="https://material-ui.com/getting-started/example-projects/" rel="noopener noreferrer"&gt;example project for Next.js&lt;/a&gt; so I didn’t have to learn the framework from the start — I just had to look through specific parts of the tutorial and documentation. Converting the app and querying the data on the server side on each request solved all three points I raised above and the end result was…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fwie1fr0yn5u4jdrn2upf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fwie1fr0yn5u4jdrn2upf.jpg" alt="Two screenshots of the Lighthouse results side-by-side. Left: Gatsby (SSG) results, right: Next.js (SSR)." width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

Results from running &lt;a href="https://developers.google.com/speed/pagespeed/insights/" rel="noopener noreferrer"&gt;Google PageSpeed Insights&lt;/a&gt;




&lt;p&gt;&lt;strong&gt;Roughly triple the time for the first contentful paint.&lt;/strong&gt;&lt;br&gt;
Plus, the Lighthouse speed index quadrupled and the time to first byte rose from 10–20 ms to 2.56 seconds.&lt;/p&gt;

&lt;p&gt;While it is noteworthy that the Next.js version is hosted on a different service (ZEIT Now vs Firebase Hosting — this may have also contributed to the higher TTFB), it was clear that pushing the data fetching step to the server produced a seemingly slower result, even if the content loaded at around the same time, because the user only sees a blank white page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fa0fv7jclegwdedipqz6g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fa0fv7jclegwdedipqz6g.gif" alt="Screen recording of the two versions of the app loading side-by-side" width="600" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

Screen recording of the two versions loading. They were not run simultaneously; the recordings were synced to the moment the Enter key was pressed.




&lt;p&gt;This highlights an important lesson in front-end development: give your users visual feedback. A study found that apps that used skeleton screens are &lt;a href="https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a" rel="noopener noreferrer"&gt;perceived to load faster&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The results also go against a sentiment you might have noticed if you’ve been reading articles about web development for the past few years:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  The client side isn’t evil.
&lt;/h3&gt;
&lt;h3&gt;
  
  
  SSR is not the catch-all solution to performance issues.
&lt;/h3&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Gatsby vs Next.js: static site generation performance
&lt;/h2&gt;

&lt;p&gt;While the two frameworks have been known exclusively for static site generation and server-side rendered apps respectively, &lt;a href="https://nextjs.org/blog/next-9-3" rel="noopener noreferrer"&gt;Next.js 9.3 overhauled its SSR implementation&lt;/a&gt; to rival Gatsby.&lt;/p&gt;

&lt;p&gt;At the time of writing, this update was just over a month old and was still featured on the main Next.js landing page and there weren’t many — if any — comparisons of the frameworks’ SSG implementations. So I decided to run an experiment myself.&lt;/p&gt;

&lt;p&gt;I reverted the changes made to the Gatsby version back to client-side data fetching and made sure both versions had the exact same feature set: I had to disable the SEO features, favicon generation, and PWA manifest, which were handled by Gatsby plugins. To compare just the JavaScript bundles produced by the frameworks, there were no images or other content loaded from external sources and both versions were deployed to Firebase Hosting. For reference, the two versions were built on Gatsby 2.20.9 and Next.js 9.3.4.&lt;/p&gt;

&lt;p&gt;I ran Lighthouse six times for each version on my local machine.&lt;br&gt;&lt;br&gt;
The results very slightly favour Gatsby:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fk90sjn676oazcilzdmnx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fk90sjn676oazcilzdmnx.jpg" alt="Two screenshots of the Lighthouse results side-by-side. Left: Gatsby results, right: Next.js." width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

Average scores and timings of the six Lighthouse runs per framework




&lt;p&gt;The Next.js version was just slightly behind Gatsby in the overall performance score, first contentful paint, and speed index. It also registered a higher max potential first input delay.&lt;/p&gt;

&lt;p&gt;Diving into the Chrome DevTools Network panel to find an answer, the Next.js version split the JavaScript payload into three more chunks (ignoring the manifest files generated), but resulted in a 20 KB smaller compressed payload. Could these extra requests have outweighed the gains made by the smaller bundle size so much that they hurt performance?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F8pdyzwyjuj85os6cay1r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F8pdyzwyjuj85os6cay1r.jpg" alt="Two screenshots of the Chrome DevTools Network panels side-by-side. Left: Gatsby results, right: Next.js." width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking into JavaScript performance, DevTools show the Next.js version took 300 ms longer to achieve first paint and spent a long time evaluating the runtime scripts. DevTools even flagged it as a “long task”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3gokb195cmdph88ora0h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3gokb195cmdph88ora0h.jpg" alt="Two screenshots of the Chrome DevTools Performance panels side-by-side. Left: Gatsby results, right: Next.js." width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I compared the two branches of the project to see if there were any implementation differences that could have caused the performance hit. Other than removing unused code and fixing missing TypeScript types, the only change was the implementation of smooth scrolling when navigating to specific parts of the page. This was previously in the &lt;code&gt;gatsby-browser.js&lt;/code&gt; file and was moved to a &lt;a href="https://nextjs.org/docs/advanced-features/dynamic-import" rel="noopener noreferrer"&gt;dynamically-imported component&lt;/a&gt; so it would only ever be run in the browser. (The npm package we’re using, &lt;a href="https://www.npmjs.com/package/smooth-scroll" rel="noopener noreferrer"&gt;smooth-scroll&lt;/a&gt;, requires the &lt;code&gt;window&lt;/code&gt; object at the time it’s imported.) This may very well be the culprit, but I’m just not familiar with how Next.js handles this feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gatsby has a superior developer experience
&lt;/h2&gt;

&lt;p&gt;Ultimately, I decided to stick with the Gatsby version. Disregarding the &lt;em&gt;very minor&lt;/em&gt; performance benefits over SSG Next.js (am I &lt;em&gt;really&lt;/em&gt; going to nitpick over a 0.6 second difference?), the Gatsby version had more PWA features already implemented and it would not have been worth the time to re-implement it.&lt;/p&gt;

&lt;p&gt;When initially building the Gatsby version, I was able to quickly add the final touches to make a more complete PWA experience. To implement page-specific SEO meta tags, I just had to &lt;a href="https://www.gatsbyjs.org/docs/add-seo-component/" rel="noopener noreferrer"&gt;read their guide&lt;/a&gt;. To add a PWA manifest, I just had to &lt;a href="https://www.gatsbyjs.org/packages/gatsby-plugin-manifest" rel="noopener noreferrer"&gt;use their plugin&lt;/a&gt;. And to properly implement favicons that support &lt;em&gt;all&lt;/em&gt; the different platforms, &lt;a href="https://css-tricks.com/favicon-quiz/" rel="noopener noreferrer"&gt;which remains a convoluted mess to this day&lt;/a&gt;, well, that’s already a part of the manifest plugin I just installed. Huzzah!&lt;/p&gt;

&lt;p&gt;Implementing those features in the Next.js version would have required more work Googling tutorials and best practices and would not have provided any benefit, especially since the Next.js version did not improve performance anyway. This was also the reason I decided to just disable these features when comparing against the Gatsby version. While the Next.js documentation is more succinct (likely since it’s &lt;a href="https://packagephobia.now.sh/result?p=next" rel="noopener noreferrer"&gt;leaner&lt;/a&gt; than &lt;a href="https://packagephobia.now.sh/result?p=gatsby" rel="noopener noreferrer"&gt;Gatsby&lt;/a&gt;) and I really like their gamified tutorial page, Gatsby’s more expansive documentation and guides provided more value in actually building a PWA, even if it looks overwhelming at first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1murwxc7iqs0emwcdfjp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1murwxc7iqs0emwcdfjp.jpg" alt="Screenshots of lists of Gatsby documentation pages and plugins" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a lot to be appreciated about Next.js, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Its learning curve &lt;em&gt;feels&lt;/em&gt; smaller thanks to its tutorial and shorter documentation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Its primary data fetching architecture revolves around &lt;code&gt;async&lt;/code&gt; functions and &lt;code&gt;fetch&lt;/code&gt;, so you don’t feel like you &lt;em&gt;need&lt;/em&gt; to learn GraphQL to fully utilise the framework.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It has TypeScript support out of the box, while Gatsby requires a separate plugin and it &lt;a href="https://www.gatsbyjs.org/packages/gatsby-plugin-typescript/#caveats" rel="noopener noreferrer"&gt;doesn’t even do type checking&lt;/a&gt; — that requires &lt;a href="https://www.gatsbyjs.org/packages/gatsby-plugin-typescript-checker/" rel="noopener noreferrer"&gt;its own plugin&lt;/a&gt;. (When converting the app to Next.js, this caused some issues as I didn’t even realise I had incorrect types, causing the compile to fail.)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With its overhauled SSG support, Next.js has become a powerful framework to easily choose between SSR, SSG, and CSR on a page-by-page basis.&lt;/p&gt;

&lt;p&gt;In fact, had I been able to fully statically generate this app, Next.js would be a better fit since I’d be able to use Algolia’s default JavaScript API and keep the data fetching code &lt;a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation" rel="noopener noreferrer"&gt;in the same file&lt;/a&gt; alongside the component. As Algolia does not have a built-in GraphQL API and there is no Gatsby source plugin for Algolia, implementing this in Gatsby would &lt;a href="https://www.gatsbyjs.org/docs/sourcing-from-private-apis/" rel="noopener noreferrer"&gt;require adding this code to a new file&lt;/a&gt; and goes against the more intuitive declarative way of specifying pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are always more performance improvements
&lt;/h2&gt;

&lt;p&gt;With that out of the way, there were even more performance improvements to be made to get ever closer to that 100 performance score in Lighthouse.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Algolia’s &lt;a href="https://goto.algolia.com/webmail/139121/401005525/255952b92d3c7d700712f8c77e2eaff6e4fad444f8f071d39994d6480fc25d54" rel="noopener noreferrer"&gt;March 2020 newsletter&lt;/a&gt; recommended adding a &lt;code&gt;preconnect&lt;/code&gt; hint to further improve query performance. (Unfortunately, the email had the wrong code snippet; &lt;a href="https://discourse.algolia.com/t/how-to-preconnect/9779/2" rel="noopener noreferrer"&gt;here’s the correct one&lt;/a&gt;.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Static files should be cached forever. These include the JS and CSS files generated by Gatsby’s webpack config. Gatsby has a &lt;a href="https://www.gatsbyjs.org/docs/caching/" rel="noopener noreferrer"&gt;great documentation page on this&lt;/a&gt; and even has plugins to generate the files for Netlify and Amazon S3. Unfortunately, we have to write our own for Firebase Hosting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The images we were serving are all JPEGs or PNGs uploaded by our founders and aren’t compressed or optimised. Improving this would require more complicated work and is beyond the scope of this project. Also: it would be &lt;em&gt;really nice&lt;/em&gt; to just convert all these images to WebP and store just one very efficient image format. Unfortunately, as with many PWA features, the Safari WebKit team continues dragging their feet on this and it is now &lt;a href="https://caniuse.com/#feat=webp" rel="noopener noreferrer"&gt;the only major browser without WebP support.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading! Normally, I would post a link to view the final project but due to legal reasons, it cannot be shared publicly.&lt;/p&gt;

&lt;p&gt;You can follow me on Twitter &lt;a href="https://twitter.com/nots_dney" rel="noopener noreferrer"&gt;@nots_dney&lt;/a&gt; to get updates as I’ll be writing and sharing more about my experiences as a front-end engineer.&lt;/p&gt;

</description>
      <category>gatsby</category>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
