<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Certa's tech blog]]></title><description><![CDATA[Certa's tech blog]]></description><link>https://blog.certa.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1641219698203/Lhlmj3449.png</url><title>Certa&apos;s tech blog</title><link>https://blog.certa.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 10:23:57 GMT</lastBuildDate><atom:link href="https://blog.certa.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Dependency Management Nightmares: Our Journey from Yarn to pnpm]]></title><description><![CDATA[Ever wondered what lurks in the depths of your node_modules folder? Adding or updating a dependency in your monorepo can create complex, hard-to-debug problems when not set up properly. It's like a hidden landmine, waiting to explode at any moment. �...]]></description><link>https://blog.certa.dev/dependency-management-nightmares-our-journey-from-yarn-to-pnpm</link><guid isPermaLink="true">https://blog.certa.dev/dependency-management-nightmares-our-journey-from-yarn-to-pnpm</guid><category><![CDATA[dependency management]]></category><category><![CDATA[pnpm]]></category><category><![CDATA[package manager]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Pawan Kolhe]]></dc:creator><pubDate>Fri, 08 Aug 2025 06:46:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742632889002/c7107d29-348d-4110-a65b-1d6cc03138a3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ever wondered what lurks in the depths of your node_modules folder? Adding or updating a dependency in your monorepo can create complex, hard-to-debug problems when not set up properly. It's like a hidden landmine, waiting to explode at any moment. 💥</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737027247393/8c7b8d71-0aaf-464c-a6e8-86db783517e6.gif" alt class="image--center mx-auto" /></p>
<p>In this post, I'll dive deep into the challenges of dependency management in monorepos and share powerful solutions we discovered along the way. You'll learn about:</p>
<ul>
<li><p><strong>Common dependency management pitfalls</strong> we encountered and how to troubleshoot them</p>
</li>
<li><p><strong>Real-world bugs</strong> that drove our decision to change package managers</p>
</li>
<li><p><strong>How to implement centralized dependency version control</strong> across all packages</p>
</li>
<li><p><strong>Techniques to safeguard against dependency conflicts</strong> before they occur</p>
</li>
</ul>
<p>While our journey ultimately led us from Yarn v1 (Yarn Classic) to pnpm, the insights apply to any monorepo setup regardless of your current package manager.</p>
<p><em>Note: While these issues are specific to monorepo setups, the lessons are valuable even if you're working with a traditional repository structure.</em></p>
<h1 id="heading-the-phantom-dependency-problem">👻 The Phantom Dependency Problem</h1>
<p><strong>TLDR;</strong> Phantom dependencies are the ghosts haunting your monorepo—modules your code secretly uses without declaring, causing everything to work fine today but mysteriously break tomorrow.</p>
<h2 id="heading-a-real-world-example">A Real-World Example</h2>
<p>What seemed like a simple task—removing an unused dev dependency—turned into an unexpected nightmare that exposed a fundamental flaw in our dependency management strategy.</p>
<p>I was tasked with removing <a target="_blank" href="https://www.npmjs.com/package/cypress">cypress</a> from our monorepo because we no longer used it for E2E tests. It seemed simple—just delete some code and take cypress out of the package.json file. As a <a target="_blank" href="https://stackoverflow.com/a/22004559/5257154">dev dependency</a>, it was only used for tooling and wasn't bundled with our application, so I assumed it wouldn't even require QA testing. But, oh boy, was I wrong!</p>
<h3 id="heading-the-setup-a-simple-monorepo">🛠️ The Setup: A Simple Monorepo</h3>
<p>Let's examine a typical npm monorepo with two internal packages:</p>
<ul>
<li><p><code>@certa/platform</code>: The entry point for our React application</p>
</li>
<li><p><code>@certa/common</code>: A utility package with commonly used functions</p>
</li>
</ul>
<p>Our application simply displays the current date in a specific format:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737045792045/073440a8-a086-4c7e-abbf-352e095914a5.png" alt class="image--center mx-auto" /></p>
<p>Here is the project structure:</p>
<pre><code class="lang-plaintext">packages/
├── common/
│   ├── src/
│   │   └── index.js
│   └── package.json
└── platform/
    ├── src/
    │   ├── App.js
    │   └── index.js
    └── package.json
package.json
</code></pre>
<p>In our <strong>root</strong> <code>package.json</code>, we have:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npm-monorepo-example"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run start --workspace=@certa/platform"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"cypress"</span>: <span class="hljs-string">"^5.0.0"</span>
  }
}
</code></pre>
<p>For <code>@certa/common</code>, we have:</p>
<pre><code class="lang-json"><span class="hljs-comment">// packages/common/package.json</span>
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@certa/common"</span>,
  <span class="hljs-attr">"module"</span>: <span class="hljs-string">"src/index.js"</span>,
  <span class="hljs-attr">"dependencies"</span>: {}
}
</code></pre>
<p>And the <code>index.js</code> file contains:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// packages/common/src/index.js</span>
<span class="hljs-keyword">import</span> { format } <span class="hljs-keyword">from</span> <span class="hljs-string">"date-fns"</span>;

<span class="hljs-comment">// date-fns v1 format</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DATE_FORMAT = <span class="hljs-string">"DD/MM/YYYY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">date</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> format(date, DATE_FORMAT);
};
</code></pre>
<p><strong>Hold on!</strong> Did you notice that <code>date-fns</code> isn't defined in <code>@certa/common</code>'s <code>package.json</code> or the workspace root? Yet, the code still works! This is a phantom dependency—an import that shouldn't work but does. We'll explain why shortly.</p>
<p>For <code>@certa/platform</code>, we have:</p>
<pre><code class="lang-json"><span class="hljs-comment">// packages/platform/package.json</span>
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@certa/platform"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"react-scripts start"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@certa/common"</span>: <span class="hljs-string">"^1.0.0"</span>,
    <span class="hljs-attr">"date-fns"</span>: <span class="hljs-string">"^2.30.0"</span>,
    <span class="hljs-attr">"react"</span>: <span class="hljs-string">"^18.2.0"</span>,
    <span class="hljs-attr">"react-dom"</span>: <span class="hljs-string">"^18.2.0"</span>,
    <span class="hljs-attr">"react-scripts"</span>: <span class="hljs-string">"^5.0.1"</span>
  }
}
</code></pre>
<p>And here's <code>App.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// packages/platform/src/App.js</span>
<span class="hljs-keyword">import</span> { formatDate } <span class="hljs-keyword">from</span> <span class="hljs-string">"@certa/common"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Print date: {formatDate(new Date())}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span></span>;
}
</code></pre>
<p>After running <code>npm install</code>, here's how <code>date-fns</code> is structured in <code>node_modules</code>:</p>
<pre><code class="lang-javascript">node_modules/
└── date-fns@<span class="hljs-number">1.30</span><span class="hljs-number">.1</span>
packages/
├── common/
│   └── ... date-fns@<span class="hljs-number">1.30</span><span class="hljs-number">.1</span> (inherited)
└── platform/
    └── node_modules/
        └── date-fns@<span class="hljs-number">2.30</span><span class="hljs-number">.0</span>
</code></pre>
<h3 id="heading-the-mystery-where-did-date-fns1301-come-from">🕵️ The Mystery: Where Did <code>date-fns@1.30.1</code> Come From?</h3>
<p>If you peek into the root <code>node_modules</code> folder, you'll find many dependencies not explicitly listed in your workspace. This happens because npm flattens the <code>node_modules</code> structure, allowing access to all packages regardless of what's in your <code>package.json</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737279558971/cb1ccf01-d21f-4850-98fd-dfe87c046b12.png" alt class="image--center mx-auto" /></p>
<p>In our case, <code>date-fns@1.30.1</code> is a <strong>transitive dependency</strong> of <code>cypress</code>. Specifically, <code>cypress</code> depends on <code>@cypress/listr-verbose-renderer</code>, which depends on <code>date-fns@1.30.1</code>.</p>
<blockquote>
<p>A <strong>transitive dependency</strong> is a library that your code indirectly relies on because one of your direct dependencies requires it to function properly.</p>
</blockquote>
<h3 id="heading-the-common-misconception-about-dependencies">🤨 The Common Misconception About Dependencies</h3>
<p>Many developers assume that if a dependency like <code>date-fns</code> is added to the application entry point (<code>@certa/platform</code>), the same version will be used throughout the application. This is false.</p>
<p>In reality:</p>
<ul>
<li><p>The <code>import { format } from "date-fns"</code> in <code>@certa/common</code> uses <code>date-fns@1.30.1</code></p>
</li>
<li><p>An import in <code>App.js</code> of <code>@certa/platform</code> would use <code>date-fns@2.30.0</code></p>
</li>
</ul>
<p>This happens because Node.js resolves dependencies from the nearest <code>node_modules</code> folder.</p>
<h3 id="heading-the-breaking-change-removing-a-dev-dependency">🧨 The Breaking Change: Removing a Dev Dependency</h3>
<p>Let's remove <code>"cypress": "^5.0.0"</code> from the root <code>package.json</code> and run our application:</p>
<pre><code class="lang-plaintext">npm start
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737050306944/9f65f1ba-3929-418d-b145-7ce3c34abee3.png" alt class="image--center mx-auto" /></p>
<p>Our application breaks! 💥 But why? 😱</p>
<p>After removing cypress, the dependency structure changes:</p>
<pre><code class="lang-javascript">node_modules/
└── date-fns@<span class="hljs-number">2.30</span><span class="hljs-number">.0</span>
packages/
├── common/
│   └── ... date-fns@<span class="hljs-number">2.30</span><span class="hljs-number">.0</span> (inherited)
└── platform/
    └── node_modules/
        └── date-fns@<span class="hljs-number">2.30</span><span class="hljs-number">.0</span>
</code></pre>
<p>The version of <code>date-fns</code> in <code>@certa/common</code> changed from <code>v1.30.1</code> to <code>v2.30.0</code>!</p>
<p><img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExbGgxY2xyZXF0djk3YndudXV1c2x6eGduemRzbm1qM2EzdTFkYmpqeCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zrmTqopWm4W5cPg8Ah/giphy.gif" alt class="image--center mx-auto" /></p>
<p>This inconsistency stems from npm and Yarn Classic's behavior of <strong>hoisting dependencies to the root</strong>. While this optimization saves disk space, it creates unpredictable dependency resolution.</p>
<p><em>The source code for this example can be found</em> <a target="_blank" href="https://github.com/PawanKolhe/npm-monorepo-example"><em>here</em></a><em>.</em></p>
<h1 id="heading-finding-a-stricter-package-manager">🧐 Finding a Stricter Package Manager</h1>
<p>This dependency version inconsistency was a real eye-opener, and it made us determined to prevent it from happening again. We wanted tighter control over dependencies, ensuring that only the packages explicitly listed in <code>package.json</code> were available in our monorepo.</p>
<p>Since npm and Yarn Classic were designed with different principles, they don't offer a solution to this problem. Thus began our quest for an alternative package manager.</p>
<p>We compared the two main alternatives: <strong>Yarn Berry (v2+)</strong> and <strong>pnpm</strong>. Both have reached feature parity, so our decision came down to:</p>
<ul>
<li><p>How the <code>node_modules</code> folder ensures dependency strictness</p>
</li>
<li><p>Installation speed</p>
</li>
<li><p>Storage consumption</p>
</li>
<li><p>Migration difficulty</p>
</li>
</ul>
<p>There are already many comprehensive blog posts like <a target="_blank" href="https://blog.logrocket.com/javascript-package-managers-compared/">this</a> and <a target="_blank" href="https://nodesource.com/blog/nodejs-package-manager-comparative-guide-2024">this</a> comparing the two in detail, so I’ll not dive into the details.</p>
<p>We chose <strong>pnpm</strong> for these reasons:</p>
<ul>
<li><p><strong>Strict dependency access by default</strong> - exactly what we needed</p>
</li>
<li><p><strong>Superior performance</strong> - we measured a <strong>70%</strong> installation speed boost over Yarn Classic <a target="_blank" href="https://pnpm.io/benchmarks">check the benchmarks</a> in our monorepo</p>
</li>
<li><p><strong>Compatibility with existing tooling</strong> - unlike Yarn Berry's radical approach of eliminating <code>node_modules</code> entirely, pnpm works with existing Node.js tools out-of-the-box</p>
</li>
</ul>
<h2 id="heading-how-pnpm-ensures-strict-dependency-access">🔒 How pnpm Ensures Strict Dependency Access</h2>
<p>pnpm took inspiration from npm v2's nested structure but implemented it using symbolic links:</p>
<pre><code class="lang-json">node_modules
└── .pnpm
    ├── bar@<span class="hljs-number">1.0</span><span class="hljs-number">.0</span>
    │   └── node_modules
    │       └── bar -&gt; &lt;store&gt;/bar
    │           ├── index.js
    │           └── package.json
    └── foo@<span class="hljs-number">1.0</span><span class="hljs-number">.0</span>
        └── node_modules
            └── foo -&gt; &lt;store&gt;/foo
                ├── index.js
                └── package.json
</code></pre>
<p>The <code>node_modules/.pnpm</code> directory itself (where dependencies are actually stored) is a flat structure.</p>
<p>Only packages explicitly listed in your package.json (just <code>cypress</code> in our example below) are visible in the top-level node_modules folder. Indirect dependencies remain hidden:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737105478871/657a393e-e20a-45d7-8d8b-a5e6cb22ac03.png" alt class="image--center mx-auto" /></p>
<p>The <code>.pnpm</code> directory contains all installed dependencies, including multiple versions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737105655171/39e95488-64d2-4381-a632-afe8836229bb.png" alt class="image--center mx-auto" /></p>
<p>This structure guarantees strict access to only referenced dependencies. Learn more about it <a target="_blank" href="https://pnpm.io/symlinked-node-modules-structure">here</a>.</p>
<h1 id="heading-the-dangers-of-multiple-dependency-versions">🎏 The Dangers of Multiple Dependency Versions</h1>
<p>Imagine you need to upgrade a library to a newer version.</p>
<p>When upgrading libraries in a large monorepo (which often contains 10+ packages), teams frequently opt to update one package at a time. This incremental approach is more manageable but can lead to using multiple versions of the same dependency in a single application.</p>
<p>So, can we use multiple versions of a dependency in a single bundled application?</p>
<p>Simple answer is: <strong>Yes you can, but it comes at a cost</strong>. The cost is:</p>
<ol>
<li><p><strong>Increased bundle size:</strong> The multiple versions will have to be bundled in your production build</p>
</li>
<li><p><strong>Risk of incompatibility</strong>: Different versions of a dependency may have breaking API changes and could lead to unexpected behavior or errors in your application</p>
</li>
</ol>
<p>The increased bundle size is obvious, but incompatibility issue is not, so let’s explore two specific issues we encountered.</p>
<h2 id="heading-incompatible-apis-between-versions">🧩 Incompatible APIs Between Versions</h2>
<p>It's not always clear when different versions of <code>date-fns</code> are being used across various internal packages, especially when these packages are managed by different teams. Library APIs often change from one version to another, which can lead to problems.</p>
<p>Let's dive into an example to illustrate how these API changes can cause issues. We'll modify the <code>App.js</code> example above to show this in action.</p>
<p>In <code>@certa/common</code>, we have defined <code>DATE_FORMAT</code> constant to store the data format string. We'll use this constant in the <code>format</code> function imported from <code>@certa/platform</code> to display the date in a different locale.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// packages/common/src/index.js</span>

<span class="hljs-comment">// data-fns v1</span>
<span class="hljs-keyword">import</span> { format } <span class="hljs-keyword">from</span> <span class="hljs-string">"date-fns"</span>;

<span class="hljs-comment">// date-fns v1 format</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DATE_FORMAT = <span class="hljs-string">"DD/MM/YYYY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">date</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> format(date, DATE_FORMAT);
};
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// packages/platform/src/App.js</span>
<span class="hljs-keyword">import</span> { formatDate, DATE_FORMAT } <span class="hljs-keyword">from</span> <span class="hljs-string">"@certa/common"</span>;

<span class="hljs-comment">// data-fns v2</span>
<span class="hljs-keyword">import</span> { format } <span class="hljs-keyword">from</span> <span class="hljs-string">"date-fns"</span>;
<span class="hljs-keyword">import</span> { de } <span class="hljs-keyword">from</span> <span class="hljs-string">"date-fns/locale"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Print date: {formatDate(new Date())}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Print date: {format(new Date(), DATE_FORMAT, { locale: de })}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>Let’s run the app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737050306944/9f65f1ba-3929-418d-b145-7ce3c34abee3.png" alt class="image--center mx-auto" /></p>
<p>Looks like there’s an error. ❌ Turns out the format syntax changed from <code>date-fns</code> v1 to v2 . The <code>format()</code> API expects v2 format (<code>dd/MM/yyyy</code>) but was given v1 format (<code>DD/MM/YYYY</code>).</p>
<p>It's not immediately obvious when different versions of <code>date-fns</code> are used across various internal packages, especially if those packages are managed by different teams.</p>
<p>Errors like these are quite likely to happen. At Certa, we've learned valuable lessons from such experiences. Now, we make sure not to use multiple versions of a dependency unless it's absolutely necessary.</p>
<h2 id="heading-multiple-dependency-instances-problem">⛓️ Multiple Dependency Instances Problem</h2>
<p>Another serious issue occurs with libraries that use React Context. Here's a real example we faced when upgrading <code>react-intl</code> from v5 to v7:</p>
<p>In our application, we use <a target="_blank" href="https://www.npmjs.com/package/react-intl">react-intl</a> for internationalization across our platform. We planned to upgrade from <code>react-intl@5.8.0</code> to <code>react-intl@7.1.1</code>.</p>
<p>Imagine a monorepo with just two packages. What happens if you use different versions of a dependency in each package? This might occur if you're trying to upgrade gradually, focusing on one package at a time. Let's explore the impact of this approach.</p>
<p>After updating only <code>@certa/common</code> to <code>react-intl@7.1.1</code>, here's how the code for both packages would look.</p>
<p><code>@certa/common</code></p>
<pre><code class="lang-json"><span class="hljs-comment">// packages/common/package.json</span>
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@certa/common"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"react-intl"</span>: <span class="hljs-string">"^7.1.1"</span>
  }
}
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// packages/common/src/Welcome.jsx</span>
<span class="hljs-keyword">import</span> { useIntl } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-intl"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Welcome = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> intl = useIntl();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>
      {intl.formatMessage({
        id: "myMessage",
        defaultMessage: "Hello world"
      })}
    <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span></span>
  );
};
</code></pre>
<p><code>@certa/platform</code></p>
<pre><code class="lang-json"><span class="hljs-comment">// packages/platform/package.json</span>
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@certa/platform"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"vite"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"vite build"</span>,
    <span class="hljs-attr">"preview"</span>: <span class="hljs-string">"vite preview"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@certa/common"</span>: <span class="hljs-string">"workspace:*"</span>,
    <span class="hljs-attr">"date-fns"</span>: <span class="hljs-string">"^2.30.0"</span>,
    <span class="hljs-attr">"react"</span>: <span class="hljs-string">"^18.2.0"</span>,
    <span class="hljs-attr">"react-dom"</span>: <span class="hljs-string">"^18.2.0"</span>,
    <span class="hljs-attr">"react-intl"</span>: <span class="hljs-string">"^5.8.0"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"vite"</span>: <span class="hljs-string">"catalog:"</span>,
    <span class="hljs-attr">"@vitejs/plugin-react"</span>: <span class="hljs-string">"^4.3.4"</span>,
  }
}
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// packages/platform/src/App.js</span>
<span class="hljs-keyword">import</span> { IntlProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-intl"</span>;
<span class="hljs-keyword">import</span> { Welcome } <span class="hljs-keyword">from</span> <span class="hljs-string">"@certa/common/src/Welcome.jsx"</span>;

<span class="hljs-comment">// Translated messages in French with matching IDs to what you declared</span>
<span class="hljs-keyword">const</span> messages = {
  <span class="hljs-attr">myMessage</span>: <span class="hljs-string">"Hello world, welcome to the Certa platform!"</span>
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">IntlProvider</span> <span class="hljs-attr">messages</span>=<span class="hljs-string">{messages}</span> <span class="hljs-attr">locale</span>=<span class="hljs-string">"fr"</span> <span class="hljs-attr">defaultLocale</span>=<span class="hljs-string">"en"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Welcome</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">IntlProvider</span>&gt;</span></span>
  );
}
</code></pre>
<p>We’re using the <code>&lt;IntlProvider&gt;</code> in <code>@certa/platform</code> and importing a component from <code>@certa/common</code> that consumes that context.</p>
<p>After running <code>pnpm start</code>, we see that the app is running fine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737120892634/57e7c5a1-93f3-4361-87e9-80e57c18e4b6.png" alt class="image--center mx-auto" /></p>
<p>Nice! 😊 Now we run our CI checks and deploy it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737121125193/ab23154b-87ee-4ed7-aa3b-53311292e64a.png" alt class="image--center mx-auto" /></p>
<p><code>[React Intl] Could not find required intl object. needs to exist in the component ancestry.</code></p>
<p>Users immediately start reporting errors in our application. ❌ 😱</p>
<p><strong>Why did it work in development but fail in production?</strong> In our case, we were using Vite, which handles dependencies differently between development and production. In development mode, Vite uses a looser module resolution strategy that sometimes masks dependency conflicts. However, in production, the optimization process creates a more strictly isolated bundle that exposes these incompatibilities.</p>
<p><strong>The main issue</strong>: pnpm installs both versions of <code>react-intl</code>:</p>
<pre><code class="lang-json">node_modules/
└── .pnpm/
    ├── react-intl@<span class="hljs-number">5.25</span><span class="hljs-number">.1</span>_react@<span class="hljs-number">18.3</span><span class="hljs-number">.1</span>_typescript@<span class="hljs-number">4.9</span><span class="hljs-number">.5</span>
    └── react-intl@<span class="hljs-number">7.1</span><span class="hljs-number">.1</span>_react@<span class="hljs-number">18.3</span><span class="hljs-number">.1</span>
packages/
├── platform/
│   └── react-intl@^<span class="hljs-number">5.8</span><span class="hljs-number">.0</span> (symlink -&gt; react-intl@<span class="hljs-number">5.25</span><span class="hljs-number">.1</span>_react@<span class="hljs-number">18.3</span><span class="hljs-number">.1</span>_typescript@<span class="hljs-number">4.9</span><span class="hljs-number">.5</span>)
└── common/
    └── react-intl@^<span class="hljs-number">7.1</span><span class="hljs-number">.1</span> (symlink -&gt; react-intl@<span class="hljs-number">7.1</span><span class="hljs-number">.1</span>_react@<span class="hljs-number">18.3</span><span class="hljs-number">.1</span>)
</code></pre>
<p>Notice that there are two separate copies of <code>react-intl</code> installed. <strong>This means the</strong> <code>&lt;IntlProvider&gt;</code> <strong>context that</strong> <code>useIntl()</code> <strong>in</strong> <code>@certa/common</code> <strong>tries to access is different from the one wrapping our application in</strong> <code>@certa/platform</code><strong>.</strong> A workaround could be to re-export the <code>useIntl</code> hook from <code>@certa/platform</code> and use it across other packages in the monorepo.</p>
<p>This isn't something a developer would easily spot, so it's wise to avoid using multiple versions of the same dependency whenever possible.</p>
<p><em>The source code for this example is</em> <a target="_blank" href="https://github.com/PawanKolhe/npm-monorepo-example/tree/pnpm-multiple-instances-of-a-dependency"><em>here</em></a><em>.</em></p>
<h1 id="heading-centrally-managing-dependency-versions">🏠 Centrally Managing Dependency Versions</h1>
<p>pnpm offers a powerful feature called <a target="_blank" href="https://pnpm.io/catalogs">“Catalogs“</a> that allows you to define dependency versions centrally in the <code>pnpm-workspace.yaml</code> file. This ensures consistency across all packages in your monorepo.</p>
<p>Setting it up is straightforward—just run <code>pnpx codemod pnpm/catalog</code> to automate the process.</p>
<p>Dependency versions are defined in the <code>pnpm-workspace.yaml</code> file at the root of the workspace.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># pnpm-workspace.yaml</span>
<span class="hljs-attr">packages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">packages/*</span>

<span class="hljs-attr">catalog:</span>
  <span class="hljs-comment"># Can be referenced through "catalog"</span>
  <span class="hljs-attr">cypress:</span> <span class="hljs-string">^5.0.0</span>
  <span class="hljs-attr">date-fns:</span> <span class="hljs-string">^2.30.0</span>
  <span class="hljs-attr">react:</span> <span class="hljs-string">^18.2.0</span>
  <span class="hljs-attr">react-dom:</span> <span class="hljs-string">^18.2.0</span>
  <span class="hljs-attr">react-scripts:</span> <span class="hljs-string">^5.0.1</span>

<span class="hljs-attr">catalogs:</span>
  <span class="hljs-comment"># Can be referenced through "catalog:old"</span>
  <span class="hljs-attr">old:</span>
    <span class="hljs-attr">date-fns:</span> <span class="hljs-string">^1.30.1</span>
</code></pre>
<p>Now in your <code>package.json</code> files, you can reference these centralized versions.</p>
<p>When adding a dependency, we can use the <code>catalog:</code> protocol instead of specifying the version range directly.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@certa/platform"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@certa/common"</span>: <span class="hljs-string">"workspace:*"</span>,
    <span class="hljs-attr">"date-fns"</span>: <span class="hljs-string">"catalog:"</span>,
    <span class="hljs-attr">"react"</span>: <span class="hljs-string">"catalog:"</span>,
    <span class="hljs-attr">"react-dom"</span>: <span class="hljs-string">"catalog:"</span>,
    <span class="hljs-attr">"react-scripts"</span>: <span class="hljs-string">"catalog:"</span>
  }
}
</code></pre>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"@certa/common"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"date-fns"</span>: <span class="hljs-string">"catalog:old"</span>
  }
}
</code></pre>
<h2 id="heading-benefits-of-catalogs">📖 Benefits of Catalogs</h2>
<ul>
<li><p><strong>Faster dependency additions</strong>: Simply reference the catalog: protocol for existing dependencies</p>
</li>
<li><p><strong>Consistent versioning</strong>: Eliminate accidental version mismatches from typos</p>
</li>
<li><p><strong>Easier upgrades</strong>: Update one line in pnpm-workspace.yaml instead of modifying multiple package.json files</p>
</li>
</ul>
<p><em>The source code for this example is</em> <a target="_blank" href="https://github.com/PawanKolhe/npm-monorepo-example/tree/pnpm-catalogs"><em>here</em></a><em>.</em></p>
<h2 id="heading-enforcement">🚓 Enforcement</h2>
<p>Centrally defining dependency versions is great, but how can you ensure that specific versions aren't set in individual packages? Enter <a target="_blank" href="https://github.com/JamieMason/syncpack">syncpack</a>, a handy tool to keep everything in check!</p>
<pre><code class="lang-bash">pnpm add -wD syncpack
</code></pre>
<p>Add the following script command to the root <code>package.json</code> file:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"syncpack"</span>: <span class="hljs-string">"syncpack list-mismatches"</span>
  }
}
</code></pre>
<p>Here is how the <code>.syncpackrc</code> config file will look:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"versionGroups"</span>: [
    {
      <span class="hljs-attr">"label"</span>: <span class="hljs-string">"Use workspace protocol when developing local packages"</span>,
      <span class="hljs-attr">"dependencies"</span>: [
        <span class="hljs-string">"$LOCAL"</span>
      ],
      <span class="hljs-attr">"dependencyTypes"</span>: [
        <span class="hljs-string">"prod"</span>,
        <span class="hljs-string">"dev"</span>
      ],
      <span class="hljs-attr">"pinVersion"</span>: <span class="hljs-string">"workspace:*"</span>
    },
    {
      <span class="hljs-attr">"label"</span>: <span class="hljs-string">"Use catalog:old protocol for all dependencies"</span>,
      <span class="hljs-attr">"packages"</span>: [
        <span class="hljs-string">"@certa/common"</span>
      ],
      <span class="hljs-attr">"dependencies"</span>: [
        <span class="hljs-string">"react-intl"</span>
      ],
      <span class="hljs-attr">"dependencyTypes"</span>: [
        <span class="hljs-string">"prod"</span>,
        <span class="hljs-string">"dev"</span>
      ],
      <span class="hljs-attr">"pinVersion"</span>: <span class="hljs-string">"catalog:old"</span>
    },
    {
      <span class="hljs-attr">"label"</span>: <span class="hljs-string">"Use catalog protocol for all dependencies"</span>,
      <span class="hljs-attr">"dependencies"</span>: [
        <span class="hljs-string">"**"</span>
      ],
      <span class="hljs-attr">"dependencyTypes"</span>: [
        <span class="hljs-string">"prod"</span>,
        <span class="hljs-string">"dev"</span>
      ],
      <span class="hljs-attr">"pinVersion"</span>: <span class="hljs-string">"catalog:"</span>
    }
  ]
}
</code></pre>
<p>This setup ensures that:</p>
<ul>
<li><p>Internal packages use <code>workspace:*</code></p>
</li>
<li><p>Specific packages use <code>catalog:old</code> for designated dependencies</p>
</li>
<li><p>Everything else uses <code>catalog:</code></p>
</li>
</ul>
<p>This will ensure that your monorepo is following a single version policy. You can customize the configuration to allow different versions of a dependency if needed.</p>
<p>Running <code>pnpm syncpack</code> will flag any violations:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737286250906/dccc344e-cd4d-4782-bb74-092b2b310127.png" alt class="image--center mx-auto" /></p>
<p>Fixing the issue by moving the version to <code>pnpm-workspace.yaml</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737286416223/29311c4f-07e8-474b-ad6c-a2d4dc51ad45.png" alt class="image--center mx-auto" /></p>
<p>Run this check in your CI pipeline to maintain dependency consistency across your project.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Dependency management issues like phantom dependencies and version conflicts are common headaches in monorepo setups. By understanding these problems and implementing the solutions outlined in this post, you can take the lead in your organization and become the hero 🦸 who saves your application from dependency hell.</p>
<p>The key takeaways:</p>
<ul>
<li><p>Be aware of phantom dependencies and their risks</p>
</li>
<li><p>Consider pnpm for strict dependency management</p>
</li>
<li><p>Avoid multiple versions of the same dependency when possible</p>
</li>
<li><p>Use centralized version management with Catalogs</p>
</li>
<li><p>Enforce dependency standards with tools like syncpack</p>
</li>
</ul>
<p>If you found this post helpful, please share it with your network! 💻</p>
<h2 id="heading-were-hiring"><strong>We're hiring!</strong></h2>
<p>If solving challenging problems at scale in a fully-remote team interests you, check out our <a target="_blank" href="https://www.getcerta.com/careers"><strong>careers page</strong></a> and apply for the position that matches your skills and interests!</p>
]]></content:encoded></item><item><title><![CDATA[How We Reduced React App Load Time by 36%]]></title><description><![CDATA[Who doesn’t love blazing fast apps? At Certa, we know our customers expect nothing less than a snappy responsive UI with minimal wait times. A slow application can leave customers feeling frustrated and not wanting to come back for more.
Many blogs a...]]></description><link>https://blog.certa.dev/how-we-reduced-react-app-load-time-by-36</link><guid isPermaLink="true">https://blog.certa.dev/how-we-reduced-react-app-load-time-by-36</guid><category><![CDATA[React]]></category><category><![CDATA[performance]]></category><category><![CDATA[optimization]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Performance Optimization]]></category><dc:creator><![CDATA[Pawan Kolhe]]></dc:creator><pubDate>Thu, 26 Dec 2024 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737265574501/4896deed-abe6-4b5a-b2a2-83a8f0e62473.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Who doesn’t love blazing fast apps? At Certa, we know our customers expect nothing less than a snappy responsive UI with minimal wait times. A slow application can leave customers feeling frustrated and not wanting to come back for more.</p>
<p>Many blogs already cover classic React application optimisation techniques, like image lazy loading, route level code splitting, virtualisation, useMemo, and more. This blog digs deeper and explores lesser-known techniques that are often overlooked, but can help improve your React application’s performance. Even if you don’t use React, most of these tips are still useful.</p>
<p>Route-level code splitting is a well-known optimisation to significantly improve load times, and we already had it in our application. We needed something extra to reduce load times even further.</p>
<p>🚀 Check out the improvements we got:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=Hqi3YQXJ958">https://www.youtube.com/watch?v=Hqi3YQXJ958</a></div>
<p> </p>
<p>Certa’s platform frontend is a client-side application built using <strong>React + TypeScript + Vite</strong>. It’s a fairly large application with various third party libraries installed. If you’re using a similar stack the following tips should be very straightforward to implement.</p>
<p>Let’s dig right in!</p>
<h1 id="heading-remove-old-unused-routes">🧹 Remove Old Unused Routes</h1>
<p>Startups like ours are supposed to iterate quickly. We often leave behind a lot of unused legacy code because we're focused on building the next big thing.</p>
<p>As an application evolves, we tend to forget about old, unused routes that remain in our routing declarations. These old routes add to your application's bundle size over time, so it's important to clean them up once customers have fully migrated to the new version.</p>
<p>We're using React Router, so simply removing these routes from wherever you've declared all your routes will eliminate them from the bundle due to tree shaking.</p>
<pre><code class="lang-diff">&lt;Route path="/" element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;Home /&gt;} /&gt;
    &lt;Route path="about" element={&lt;About /&gt;} /&gt;
    &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
    &lt;Route path="insights" element={&lt;Dashboard /&gt;} /&gt;
    &lt;Route path="messages" element={&lt;Messages /&gt;} /&gt;

    {/* Old Routes */}
<span class="hljs-deletion">-   &lt;Route path="dashboard" element={&lt;DashboardOld /&gt;} /&gt;</span>
<span class="hljs-deletion">-   &lt;Route path="chat" element={&lt;Chat /&gt;} /&gt;</span>

    {/* 404 Route */}
    &lt;Route path="*" element={&lt;NotFound /&gt;} /&gt;
&lt;/Route&gt;
</code></pre>
<h3 id="heading-clean-up-the-tech-debt">Clean up the tech debt</h3>
<p>Go ahead and clean the code associated with the removed route. I’d recommend using a tool like <a target="_blank" href="https://knip.dev/">knip</a> which can find unused files, dependencies, and even exports. Best part is that it can also clean up that code automatically for you.</p>
<p>A cleanup is great for engineering productivity.</p>
<ul>
<li><p>A cleaner, more organised codebase reduces cognitive load for developers, making it easier to onboard new team members and allowing existing developers to work more efficiently.</p>
</li>
<li><p>Less code to refactoring in cases of large repo wide refactors.</p>
</li>
</ul>
<h1 id="heading-lazy-load-large-third-party-libraries">🛒 Lazy Load Large Third-Party Libraries</h1>
<p>When building a large application, it’s inevitable to use many open source libraries. Some of these libraries tend to be quite large. It makes more sense to lazy load these libraries so that they are fetched only when a component importing them is rendered.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734662566781/47f75397-7466-4b0e-8616-9e2a2a94a20e.png" alt="Bundle output" class="image--center mx-auto" /></p>
<p>Let’s take the example of <a target="_blank" href="https://github.com/vankop/jsoneditor-react">jsoneditor-react</a>. We have included it in our application for JSON editing capabilities but these capabilities are only required on certain pages and so it doesn’t make much sense to load them initially on page load. <a target="_blank" href="https://github.com/vankop/jsoneditor-react">jsoneditor-react</a> contributes around 210kB (gzipped) to the bundle size which is significant enough to lazy load.</p>
<p>Following example of lazy loading <a target="_blank" href="https://github.com/vankop/jsoneditor-react">jsoneditor-react</a> is a rather more extreme case where multiple dependent libraries also have to be loaded in order for the library to work.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> loadable <span class="hljs-keyword">from</span> <span class="hljs-string">"@loadable/component"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> {
  JsonEditor,
  JsonEditorProps <span class="hljs-keyword">as</span> JsonEditorComponentProps
} <span class="hljs-keyword">from</span> <span class="hljs-string">"jsoneditor-react"</span>;
<span class="hljs-keyword">import</span> Ajv <span class="hljs-keyword">from</span> <span class="hljs-string">"ajv"</span>;

<span class="hljs-keyword">const</span> ajv = <span class="hljs-keyword">new</span> Ajv({ allErrors: <span class="hljs-literal">true</span>, verbose: <span class="hljs-literal">true</span> });

<span class="hljs-keyword">const</span> Editor = loadable(
  <span class="hljs-function">() =&gt;</span>
    <span class="hljs-keyword">import</span>(<span class="hljs-string">"ace-builds/src-noconflict/ace"</span>)
      .then(<span class="hljs-function">() =&gt;</span>
        <span class="hljs-built_in">Promise</span>.all([
          <span class="hljs-keyword">import</span>(<span class="hljs-string">"jsoneditor-react"</span>),
          <span class="hljs-keyword">import</span>(<span class="hljs-string">"brace"</span>),
          <span class="hljs-keyword">import</span>(<span class="hljs-string">"jsoneditor-react/es/editor.min.css"</span>),
          <span class="hljs-keyword">import</span>(<span class="hljs-string">"brace/mode/json"</span>)
        ])
      )
      .then(<span class="hljs-function">(<span class="hljs-params">[{ JsonEditor }, { <span class="hljs-keyword">default</span>: ace }]</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> forwardRef&lt;JsonEditor, JsonEditorComponentProps&gt;(
          <span class="hljs-function">(<span class="hljs-params">props, ref</span>) =&gt;</span> (
            &lt;JsonEditor ace={ace} ajv={ajv} ref={ref} {...props} /&gt;
          )
        );
      }),
  {
    fallback: &lt;FallbackLoader /&gt;
  }
);

<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">const</span> MyForm = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        &lt;Editor ... /&gt;
    )
}
</code></pre>
<p>We’re using the <a target="_blank" href="https://github.com/gregberge/loadable-components">loadable-components</a> library to make lazy loading simpler. It essentially only fetches the module when the component mounts (<code>&lt;Editor&gt;</code> is this case).</p>
<p>Here is a simpler use-case where we need the <a target="_blank" href="https://www.npmjs.com/package/xlsx">xlsx</a> library capabilities to export an excel sheet.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> exportExcel = <span class="hljs-keyword">async</span> (
  data: Record&lt;<span class="hljs-built_in">any</span>, <span class="hljs-built_in">any</span>&gt;[],
  fileName: <span class="hljs-built_in">string</span>,
  extension: <span class="hljs-built_in">string</span>
) =&gt; {
  <span class="hljs-comment">// Dynamic import</span>
  <span class="hljs-keyword">const</span> XLSX = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">"xlsx"</span>);

  <span class="hljs-keyword">const</span> worksheet = XLSX.utils.json_to_sheet(data);
  <span class="hljs-keyword">const</span> workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, <span class="hljs-string">"Sheet1"</span>);
  XLSX.writeFile(workbook, <span class="hljs-string">`<span class="hljs-subst">${fileName}</span>.<span class="hljs-subst">${extension}</span>`</span>, {
    compression: <span class="hljs-literal">true</span>
  });
};

<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">const</span> MyForm = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        &lt;button onClick={<span class="hljs-function">() =&gt;</span> exportExcel(...)}&gt;
          Export <span class="hljs-keyword">as</span> excel sheet
        &lt;/button&gt;
    )
}
</code></pre>
<h1 id="heading-use-tree-shakable-alternatives">🌲 Use Tree Shakable Alternatives</h1>
<p>The <code>lodash</code> library is not tree-shakable by default. You need to import modules with specific paths like <code>lodash/map</code> so that you don’t end up importing the whole library. With a large team, it’s not always possible to follow specific import syntax patterns. One mistake can lead to an increased bundle size.</p>
<p>The <code>lodash-es</code> package on the other hand is tree-shakable by default as it uses ES modules. So you can import without accidentally including unnecessary code in the bundle.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// All lodash modules included in bundle</span>
<span class="hljs-keyword">import</span> { debounce } <span class="hljs-keyword">from</span> <span class="hljs-string">'lodash'</span>;

<span class="hljs-comment">// Only 'debounce' function included in bundle</span>
<span class="hljs-keyword">import</span> debounce <span class="hljs-keyword">from</span> <span class="hljs-string">'lodash/map'</span>;

<span class="hljs-comment">// Only 'debounce' function included in bundle</span>
<span class="hljs-keyword">import</span> { debounce } <span class="hljs-keyword">from</span> <span class="hljs-string">'lodash-es'</span>;

<span class="hljs-comment">// Only 'debounce' function included in bundle</span>
<span class="hljs-keyword">import</span> debounce <span class="hljs-keyword">from</span> <span class="hljs-string">'lodash-es/debounce'</span>;
</code></pre>
<p>If your team can’t move away from <code>lodash</code>, you can always use an ESLint rule:</p>
<pre><code class="lang-json"><span class="hljs-string">"no-restricted-imports"</span>: [
   <span class="hljs-string">"error"</span>,
   {
      <span class="hljs-attr">"paths"</span>: [
         {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"lodash"</span>,
            <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Import [module] from lodash/[module] instead"</span>
         }
      ]
   }
],
</code></pre>
<h1 id="heading-pre-connect-to-frequently-used-endpoints">🔌 Pre-connect to Frequently Used Endpoints</h1>
<p>In your main HTML file, you can define origins using <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preconnect">&lt;link rel="preconnect"&gt;</a> which will tell the browser to preemptively perform all of the handshake (DNS+TCP for HTTP, and DNS+TCP+TLS for HTTPS origins) for those origins. This will speeds up future loads from a given origin.</p>
<p>These origins should be cross-origin. It has no benefit on same-origin requests because the connection is already open.</p>
<p>These origins are usually ones that you need on page load and are used on almost all pages of your application.</p>
<p>Here is an example:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- Preconnect - Full connection including DNS, TCP, and TLS --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preconnect"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://www.gstatic.com"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preconnect"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://fonts.gstatic.com"</span> <span class="hljs-attr">crossorigin</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preconnect"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://fonts.googleapis.com"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preconnect"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://www.recaptcha.net"</span> <span class="hljs-attr">crossorigin</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"preconnect"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://api.certa.in"</span> /&gt;</span>
</code></pre>
<p>The <code>crossorigin</code> attribute is added when the server expects CORS headers in the request. If the server is cross-origin but doesn't require CORS headers (like for images or scripts that don't need CORS), you should omit the <code>crossorigin</code> attribute else the pre-connection would be of no use and the browser would be forced to establish a connection again.</p>
<p>Before preconnect:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734670772305/08f8b1b2-52d1-41f8-824d-05cae9f99092.png" alt class="image--center mx-auto" /></p>
<p>After preconnect:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734670860033/21dc65fa-2eaa-4238-8636-3c1a57cac6ae.png" alt class="image--center mx-auto" /></p>
<p>That’s around <strong>0.4s</strong> faster page loads already. 🚀</p>
<h1 id="heading-optimize-fonts">🖌 Optimize Fonts</h1>
<h2 id="heading-eliminate-extra-fonts-if-possible">Eliminate Extra Fonts (if possible)</h2>
<p>Our application used 4 different fonts before optimization. One was our primary font, others were just used at single places in the UI. We redesigned our UI in a way that eliminated the need for some most of the extra fonts. Loading fonts is blocking in nature, so lesser the fonts, the better for page loads.</p>
<h2 id="heading-preload-fonts">Preload Fonts</h2>
<p>When importing fonts in HTML, we typically import like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&amp;display=swap"</span>
/&gt;</span>
</code></pre>
<p>Adding a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload">preload tag</a> tells the browser to start downloading the asset early and treat it as high priority. It ensures that the font is available to be used before browser renders the page. This prevents UI flicker caused by loading default browser fonts and then the desired fonts.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"preload"</span>
  <span class="hljs-attr">as</span>=<span class="hljs-string">"style"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&amp;display=swap"</span>
  <span class="hljs-attr">crossorigin</span>
/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&amp;display=swap"</span>
/&gt;</span>
</code></pre>
<h1 id="heading-you-might-not-need-those-polyfills">🪑 You might not need those Polyfills</h1>
<p>A polyfill is a piece of code that provides modern functionality to older browsers that don't support it natively. For example, if you want to use <code>Array.prototype.includes()</code> (ES7 feature) in a very old browser that doesn't support it, you'd need a polyfill to replicate that functionality.</p>
<p>Polyfills do come with several downsides:</p>
<ul>
<li><p>Bundle Size: Each polyfill adds to your JavaScript bundle size, increasing download times and potentially affecting performance.</p>
</li>
<li><p>Runtime Overhead: Polyfills often can't match the performance of native implementations.</p>
</li>
<li><p>Maintenance Burden: Managing polyfills adds complexity to your build process and dependencies.</p>
</li>
</ul>
<p>Before including polyfills, ask yourself:</p>
<ul>
<li><p>What browsers do your users use?</p>
</li>
<li><p>What percentage of your users would benefit from the polyfill?</p>
</li>
<li><p>What's the performance impact of including the polyfill?</p>
</li>
</ul>
<p>It might be beneficial to drop support for old browsers and ask them to upgrade if the users using old browsers is a very small fraction.</p>
<p>At Certa, we were using polyfills for <code>string.prototype.matchall</code> and had <a target="_blank" href="https://www.npmjs.com/package/react-app-polyfill"><code>react-app-polyfill</code></a> package added to polyfill ES6 features for Edge 17 and 16 compatibility.</p>
<p>After analysing the browsers our users were using, we found that those browser are no longer used and proceeded to clean them up.</p>
<h1 id="heading-manual-chunking-with-vite">🪆 Manual Chunking with Vite</h1>
<p>While Vite provides automatic chunking out of the box for source code, there are scenarios where manual chunking gives you more control over your build output. Allow me to explain:</p>
<p>By default Vite third-party libraries and source code is bundled together in what we call the <strong>index chunk</strong>. This chunk is the first chunk to load and is always loaded no matter the route.</p>
<p>We should aim to break down large chunks in order to reduce fetching delays.</p>
<ul>
<li><p>Aim for chunks between 100-300KB</p>
</li>
<li><p>Too many small chunks can increase HTTP requests</p>
</li>
<li><p>Too few large chunks can delay initial load</p>
</li>
</ul>
<h3 id="heading-visualize-your-bundle">Visualize your Bundle</h3>
<p>Before we optimize chunking, it would be great to visualize it first so that we can observe the difference. Using <a target="_blank" href="https://www.npmjs.com/package/rollup-plugin-visualizer">rollup-plugin-visualizer</a>, we can view a neat graph of all the chunks and how much size they take up.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734688004936/04d873ad-3b90-41eb-acc1-4ed7d9fc34c2.png" alt="Sample bundle visualization" class="image--center mx-auto" /></p>
<h3 id="heading-separate-chunk-for-third-party-libraries">Separate Chunk for Third-Party Libraries</h3>
<p>Third-party packages are not dependent on any application source code, so they can all be easily separated into a separate chunk without any breakages. Let’s call this chunk the <strong>vendor chunk</strong>.</p>
<p>You can further divide the vendor chunk into multiple separate chunks. Be careful here, because third-party libraries are often inter dependent on each other, so it’s important to keep inter dependent packages in the same chunk else they might fail to load at run time.</p>
<p>Possible scenario that will lead to an error:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Will FAIL</span>

<span class="hljs-comment">// Dependency graph</span>
@lexical/core -&gt; @lexical/react -&gt; @lexical/core

<span class="hljs-comment">// Manual chunking</span>
<span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"@lexical/react"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"lexical-react"</span>;
<span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"@lexical/core"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"lexical-core"</span>;

<span class="hljs-comment">// Output chunks</span>
lexical-react-[hash].js
lexical-core-[hash].js
</code></pre>
<p>In above example, we are keeping to inter-dependent packages in separate chunks.</p>
<p>When <code>lexical-react</code> chunk is loaded and parsed, it will see that there is an import to the <code>lexical-core</code> chunk. Parsing will be paused for "lexical-react" and move to loading and parsing “lexical-core“ chunk.</p>
<p>The <code>lexical-core</code> chunk will have an import statement defined for <code>lexical-react</code>. But the <code>lexical-react</code> chunk is still not loaded yet, so we’re stuck in a deadlock and the parsing fails and error is thrown.</p>
<p>For this reason, we need to keep related packages in the same chunk.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Will SUCCEED</span>

<span class="hljs-comment">// Dependency graph</span>
@lexical/core -&gt; @lexical/react -&gt; @lexical/core

<span class="hljs-comment">// Manual chunking (will include both `@lexical/core` and `@lexical/react` in same chunk)</span>
<span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"lexical"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"lexical"</span>;

<span class="hljs-comment">// Output chunks</span>
lexical-[hash].js
</code></pre>
<p>It’s not recommended to implement manual chunking in source code due to high likelihood of creating circular dependencies that will break your app too often. The right way to create separate chunks in source code is to use dynamic imports where is makes sense. Each route should ideally be dynamically imported so that the module bundler knows to analyse the dependency graph and separate the chunks accordingly.</p>
<p>Here is the full manual configuration:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// vite.config.ts</span>
<span class="hljs-keyword">const</span> viteConfig = defineConfig(<span class="hljs-function">() =&gt;</span> ({
  build: {
    rollupOptions: {
      output: {
        manualChunks
      }
    },
    ...
  },
  ...
}))

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">manualChunks</span>(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">string</span> | <span class="hljs-title">void</span> </span>{
  <span class="hljs-keyword">const</span> isVendor = id.includes(<span class="hljs-string">"node_modules"</span>);
  <span class="hljs-keyword">if</span> (isVendor) {
    <span class="hljs-comment">// Creates manual chunks from node_module deps. Reducing the vendor chunk size.</span>
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"core-js"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"core-js"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"react-flow-renderer"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"react-flow-renderer"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"recharts"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"recharts"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"lexical"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"lexical"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"d3-"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"d3"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"lodash-es"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"lodash"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"moment"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"moment"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"dexie"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"dexie"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"date-fns"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"date-fns"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"react-popper"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"react-popper"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"handlebars"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"handlebars"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"@sentry"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"sentry"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"@radix-ui"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"radix-ui"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"@dnd-kit"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"dnd-kit"</span>;
    <span class="hljs-keyword">if</span> (id.includes(<span class="hljs-string">"jsoneditor-react"</span>) || id.includes(<span class="hljs-string">"brace"</span>))
      <span class="hljs-keyword">return</span> <span class="hljs-string">"jsoneditor"</span>;

    <span class="hljs-comment">// Default chunk name for third-party packages</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"vendor"</span>;
  }
}
</code></pre>
<p>All chunks required for initial page load are loaded in parallel. So the largest chunk can be the bottleneck, which is where manual chunking helps to break down chunk further.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734688411093/bdc3d214-e282-425b-9a37-42a30bef04b2.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Most of the above techniques are framework independent. They can be done one at a time and are fairly straightforward to implement. Enhancing the performance of your application will greatly improve user experience, leading to higher satisfaction and engagement.</p>
<p>With a combination of all these techniques, we benchmarked major improvements in metrics across our platform and saw a <strong>36%</strong> improvement in page load times on average. Let’s dig into the metrics, shall we?</p>
<ul>
<li><p><strong>57%</strong> reduction in the app size loaded on Login page</p>
</li>
<li><p>LCP (Largest Contentful Paint): <strong>27.2%</strong> faster</p>
</li>
<li><p>FCP (First Contentful Paint): <strong>28.5%</strong> faster</p>
</li>
<li><p>FMP (First Meaningful Paint): <strong>30%</strong> faster</p>
</li>
<li><p>DOMContentLoaded (DCL): <strong>41.1%</strong> faster</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734687259313/6191e3cf-60aa-45e4-bd30-ce8ff250bef6.png" alt class="image--center mx-auto" /></p>
<p>Hope you achieve similar or even better results! 🚀</p>
<h2 id="heading-were-hiring"><strong>We're hiring!</strong></h2>
<p>If solving challenging problems at scale in a fully-remote team interests you, head to our <a target="_blank" href="https://www.getcerta.com/careers"><strong>careers page</strong></a> and apply for the position of your liking!</p>
]]></content:encoded></item><item><title><![CDATA[Unraveling a Database Performance Mystery: The Tale of Unexpected Bloat and IOPS Surges]]></title><description><![CDATA[It was just another Monday at Certa, where a team of engineers had completed a significant deployment over the weekend. The mood was upbeat as everyone settled in for the start of the workweek. But the serenity was deceptive; a subtle issue brewing w...]]></description><link>https://blog.certa.dev/unraveling-a-database-performance-mystery-the-tale-of-unexpected-bloat-and-iops-surges</link><guid isPermaLink="true">https://blog.certa.dev/unraveling-a-database-performance-mystery-the-tale-of-unexpected-bloat-and-iops-surges</guid><category><![CDATA[DatabasePerformance]]></category><category><![CDATA[Autovacuum]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[techtips]]></category><category><![CDATA[databasemanagement]]></category><category><![CDATA[Performance Tuning]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Rohit Patil]]></dc:creator><pubDate>Tue, 25 Jun 2024 17:18:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719483314827/a1c16ca4-5bd4-4784-b9f0-8129141980ee.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It was just another Monday at <a target="_blank" href="https://www.getcerta.com/">Certa</a>, where a team of engineers had completed a significant deployment over the weekend. The mood was upbeat as everyone settled in for the start of the workweek. But the serenity was deceptive; a subtle issue brewing would soon disrupt operations.</p>
<h2 id="heading-the-first-signs-of-trouble"><strong>The First Signs of Trouble 📞</strong></h2>
<p>On Tuesday, we noticed a minor uptick in database activity. The <a target="_blank" href="https://www.crunchydata.com/blog/understanding-postgres-iops">Read IOPS</a> on the main database started climbing. Initially dismissed as a minor fluctuation, after monitoring it for some hours, it was clear something was wrong. The database was straining under an unexpected load, and the effects soon rippled through the system.</p>
<h2 id="heading-the-impact-spreads"><strong>The Impact Spreads 🧨</strong></h2>
<p>As the week went on, our DevOps team began receiving alerts about high-read IOPS. They sprung into action involving a few platform engineers, and promptly began the investigation. Meanwhile, several clients reported high latency across multiple features to our customer success team.</p>
<h2 id="heading-the-investigation-begins"><strong>The Investigation Begins 🕵🏽‍♂️</strong></h2>
<p>To identify the root cause, we delved into the monitoring dashboards, examining metrics and logs to understand what was driving the increased read IOPS. When we compared metrics from the last two weeks, we saw a steep rise in the read IOPS count, with no sign of decline.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719307387160/d22991ea-4f98-4ed0-a5fa-60c1b540b842.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-discovery"><strong>The Discovery 💡</strong></h2>
<p>Further investigation revealed a troubling execution plan within one of the database schemas. An <code>EXPLAIN ANALYZE</code> query showed some operations were taking far longer than expected, leading to a cascade of delays and increased read IOPS. In such cases, <code>VACUUM ANALYZE</code> always comes as a lifesaver, so we ran the command to refresh the stats and generate an optimized query plan. When we checked the dashboards for clues, we noticed that the <code>autovacuum</code> command was running continuously. We focused on it and checked what was happening. The database automatically triggers this command whenever the <a target="_blank" href="https://levelup.gitconnected.com/understanding-the-autovacuum-process-in-postgresql-everything-you-need-to-know-ab389af7719d">bloat size crosses a threshold limit</a>, and when we checked the stats, we found out that this was indeed what had happened with our database.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719307501396/318c6444-5b40-42f6-8ae3-3a099e3cd547.png" alt class="image--center mx-auto" /></p>
<p>SQL query:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> schemaname, relname, n_live_tup, n_dead_tup, trunc(<span class="hljs-number">100</span>*n_dead_tup/(n_live_tup+<span class="hljs-number">1</span>))::<span class="hljs-built_in">float</span> <span class="hljs-string">"ratio%"</span>,
to_char(last_autovacuum, <span class="hljs-string">'YYYY-MM-DD HH24:MI:SS'</span>) <span class="hljs-keyword">as</span> autovacuum_date,
to_char(last_autoanalyze, <span class="hljs-string">'YYYY-MM-DD HH24:MI:SS'</span>) <span class="hljs-keyword">as</span> autoanalyze_date
<span class="hljs-keyword">FROM</span> pg_stat_all_tables
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> n_dead_tup <span class="hljs-keyword">DESC</span>;
</code></pre>
<h3 id="heading-more-technical-details-for-those-who-are-interested">More technical details for those who are interested…</h3>
<p>Let’s take a pause and understand a few technical details before continuing the story.</p>
<ul>
<li><p><strong>Understanding dead tuples in PostgreSQL</strong></p>
<p>  When deleting rows in PostgreSQL, the database does not actually remove them but marks them as dead tuples. One might think that it would be easier to delete the row when performing the delete operation, instead of relying on something like <code>vacuum</code>, right? But there are reasons for not doing it, which are:</p>
<ol>
<li><p><strong>Recovery and rollback</strong></p>
<p> During the execution of a <code>DELETE</code> operation, the database cannot just delete the record without committing the transaction because it may fail or be aborted at any point in time, leading to the need for a rollback and restoration of a previous version of the record.</p>
</li>
<li><p><strong>Concurrency Control</strong></p>
<ul>
<li><p>PostgreSQL uses MVCC to manage concurrent transactions without locking the database tables. Instead of locking rows, it maintains multiple versions of a tuple. This allows readers to access the previous versions of the data while writers are modifying it, enabling better performance and less contention.</p>
</li>
<li><p>By marking tuples as dead rather than deleting them immediately, PostgreSQL ensures that transactions running in parallel can continue to see a consistent view of the data according to their isolation level. Deleting tuples outright could violate this consistency.</p>
</li>
<li><p>For example, consider two transactions running in parallel.</p>
<ul>
<li><p>Transaction T1 performs a <code>SELECT</code> operation on a dataset and then Transaction T2 performs <code>DELETE</code> on a few rows in the same dataset just after some time.</p>
</li>
<li><p>The view of transaction T1 is locked and does not change until it completes hence deleting those rows by another transaction would not be a good idea.</p>
</li>
<li><p>Instead, transaction T2 actively marks those rows as dead, allowing T1 to still see and return the previous copy so that the user can view a version of the rows at the time it was requested.</p>
</li>
<li><p>Now instead of returning, T1 performs any <code>UPDATE</code> operation after T2 marks it as dead, commits, and raises a serialization error indicating the record has been updated, forcing T1 to refresh the data before performing any update.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Index Maintenance</strong></p>
<p> Marking tuples as dead simplifies the handling of indexes. Deleting tuples immediately would require updating or removing corresponding index entries right away, which could lead to additional complexity and performance overhead.</p>
</li>
</ol>
</li>
<li><p><strong>What is “bloat”?</strong></p>
<p>  Database bloat refers to the excessive accumulation of "dead tuples" or obsolete data in a database. Bloat happens when dead tuples pile up faster than the database can clean them up. For example, if the application updates or deletes 1,000 records per second, but the <a target="_blank" href="https://www.postgresql.org/docs/9.6/static/routine-vacuuming.html">autovacuum daemon</a> can only remove 800 dead tuples per second, 200 dead tuples are left behind every second, causing bloat. This is just a hypothetical example—it's not possible to fine-tune the <code>autovacuum</code> daemon to such specific rates.</p>
</li>
<li><p><strong>Problems caused by database bloat</strong></p>
<p>  In Postgres, the <a target="_blank" href="https://www.postgresql.org/docs/9.6/static/planner-optimizer.html">query planner</a> decides the best way to execute a query. It looks at table statistics, indexes, and data types to recommend the most efficient execution path. When a table is bloated, <code>ANALYZE</code> in Postgres can provide inaccurate information to the query planner, causing slow queries and increased disk I/O operations due to the processing of unnecessary data.</p>
<p>  For instance, if a table has 350 million dead tuples but only 50 million active rows, the bloat score is 7 (a 7:1 ratio of dead to active rows). This high bloat score leads the query planner to make poor decisions, resulting in slow queries. During query execution, PostgreSQL loads both live and dead tuples into memory, which increases disk I/O and degrades performance because it has to process unnecessary data.</p>
</li>
<li><p>VACUUM and <a target="_blank" href="https://www.postgresql.org/docs/9.6/routine-vacuuming.html"><code>autovaccum</code></a> for cleaning bloat</p>
<p>  VACUUM command can be used to manually clean up these dead tuples and reclaim the space. PostgreSQL also provides autovacuum functionality which automates vacuum activity. By default, Postgres triggers cleanup when dead tuples make up 20% of the table. Based on your database workload and performance requirements, you have the option to adjust various <a target="_blank" href="https://www.postgresql.org/docs/9.6/runtime-config-autovacuum.html">settings</a>. However, the <code>autovacuum</code> process uses resources like CPU, memory, and disk I/O, which can affect the database’s regular operations.</p>
<p>  To learn more about this, refer to "<a target="_blank" href="https://aws.amazon.com/blogs/database/understanding-autovacuum-in-amazon-rds-for-postgresql-environments/">Understanding Autovacuum in Amazon RDS for PostgreSQL Environments</a>.”</p>
</li>
</ul>
<h2 id="heading-the-search-for-a-solution"><strong>The Search for a Solution 🔍</strong></h2>
<p>Now there were two questions that we were asking ourselves.</p>
<ol>
<li><p>Why does bloat size increase so much even though <code>autovacuum</code> is running continuously?</p>
</li>
<li><p>How could we speed up the removal of table bloat from the database?</p>
</li>
</ol>
<p>While we were still seeking answers to the first question, we didn’t want the customer experience to suffer. Therefore, we prioritized addressing the symptoms and began exploring potential solutions. Each option had advantages and disadvantages, demanding us to carefully evaluate the potential downtime and challenges, particularly in a production environment. Since finalizing a solution was going to take some time, we opted for a short-term fix by increasing provisioned IOPS to ensure a seamless customer experience.</p>
<h2 id="heading-then-another-problem-was-reported-at-the-same-time">Then, another problem was reported at the same time 😵</h2>
<p>Everything was working fine for our clients, and we were still searching for answers to our first question while preparing a solution. Then another issue arose, requiring our immediate attention. Our analytics database, which is a read-only replica of the main database, became slow, causing one of our clients to experience problems generating daily analytic reports. Our investigation revealed that our replica database was consistently running at over 98% load. We scaled up the database vertically in order to swiftly address this issue and ensure the client could continue working without experiencing slowdowns.</p>
<h2 id="heading-what-the-so-this-is-what-happened">What the… So, this is what happened… 🤔</h2>
<p>After scaling our replica database, it restarted, causing a drop in CPU load. To our surprise, the bloat size in our main database also decreased rapidly. Initially, we thought this might be because of the increased IOPS capacity. However, further investigation revealed the true cause.</p>
<p>We discovered a few queries had been running for a long time on our replica database. Upon checking the configuration, we noticed that <code>hot_standby_feedback</code> was enabled, which caused the problem. Enabling the <code>hot_standby_feedback</code> parameter on the replica instance usually prevents vacuum from removing dead row versions needed by running queries on the replica. This prevents query conflicts or cancellations and delays the cleanup of dead rows on the primary database, which can lead to table bloat. In our case, when the read replica database restarted during the instance scaling process, It canceled all active sessions. This allowed the <code>autovacuum</code> process to complete in the primary instance, reducing read IOPS.</p>
<p>While we worked on a long-term solution, the problem recurred in a few days. This time, we checked the replica database for long-running queries and discovered the same issue. We terminated those long-running queries rather than restarting the database. Within a few hours, the bloat size returned to normal, and the application performance improved.</p>
<h3 id="heading-more-technical-details">More technical details 🖥️</h3>
<p>Dead tuples are created to ensure concurrency when two transactions are running in parallel and working on the same dataset. But what if those transactions are running on two different servers, the main server and a replica? Rows are marked as dead, so they will still exist, and even after replication, the replica server will know which rows are marked as dead. However, a problem arises when <code>autovacuum</code> runs on the main server because when these dead tuples are deleted, the same will be reflected on the replica. What if there is a transaction that still uses those rows? This will cause replication conflicts.</p>
<p>To prevent such conflicts, PostgreSQL provides the <code>hot_standby_feedback</code> toggle. When enabled, the replica periodically sends information about the oldest transaction that is running. With this information, VACUUM can delay cleaning up certain rows, thus avoiding replication conflicts. However, if a query runs for a long duration or never terminates, these dead tuples will accumulate, resulting in database bloat.</p>
<h2 id="heading-lessons"><strong>Lessons 📝</strong></h2>
<ul>
<li><p>The database is the backbone of your application, so it is crucial to configure it properly to ensure optimal performance and reliability. In our case, we had <code>hot_standby_feedback</code> enabled but had improperly configured the timeout parameter to terminate long-running queries. Although we had alerts enabled, these queries went unnoticed because of the incorrectly configured time parameter. We had to reconsider various factors to determine the optimal time parameter and reconfigure it accordingly.</p>
<p>  These parameter values may vary depending on the use case, so it is always recommended to understand your specific use case before configuring them.</p>
</li>
<li><p>We use replica db to fetch analytic reports, while most queries we run there are reviewed first. Those long queries were the ones that were not audited and those ended up biting us in back. So we learned to always allow audited queries that won't cause resource starvation for other queries.</p>
</li>
<li><p>Customer experience is of utmost priority. While working on long-term solutions, ensure you provide quick resolutions that make the customer’s life easier.</p>
</li>
</ul>
<h2 id="heading-additional-information">Additional Information 📃</h2>
<p>We also explored the following options to clean up the bloat, but fortunately didn’t need to use:</p>
<ul>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/9.6/static/sql-vacuum.html">VACUUM FULL</a></p>
<ul>
<li><p><strong>Pros</strong>: It can be faster than <code>VACUUM ANALYZE</code> and will remove all bloat in the table while shrinking the table’s physical size on disk.</p>
</li>
<li><p><strong>Cons</strong>: It requires an exclusive read/write lock on the table for the operation’s duration, which can cause application outages depending on the table size.</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://github.com/reorg/pg_repack">pg_repack</a> (<a target="_blank" href="https://aws.amazon.com/blogs/database/remove-bloat-from-amazon-aurora-and-rds-for-postgresql-with-pg_repack/">reference</a>)</p>
<ul>
<li><p><strong>Pros</strong>: Very fast and doesn’t require a read/write lock on the table.</p>
</li>
<li><p><strong>Cons</strong>: It is very resource-intensive, which can degrade overall database performance. It can also cause significant replication lag and OutOfMemory errors when using replication slots.</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://github.com/dataegret/pgcompacttable">pgcompacttable</a></p>
<ul>
<li><p>It performs the same function as <code>pg_repack</code> by modifying the rows in place.</p>
</li>
<li><p>However, it uses significantly fewer resources and operates much slower, making it less problematic for replication slots.</p>
</li>
<li><p>Despite this, <code>pgcompacttable</code> was too slow to effectively remove the amount of bloat we had.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-references">References 🔗</h2>
<ol>
<li><p><a target="_blank" href="https://www.crunchydata.com/blog/understanding-postgres-iops">Understanding Postgres IOPS: Why They Matter Even When Everything Fits in Cache</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/learning-sql/analyse-and-vacuum-in-postgresql-keep-your-database-clean-a318e1c20d8f">Analyse and Vacuum in PostgreSQL: Keep Your Database Clean!</a></p>
</li>
<li><p><a target="_blank" href="https://levelup.gitconnected.com/understanding-the-autovacuum-process-in-postgresql-everything-you-need-to-know-ab389af7719d">Understanding the Autovacuum Process in PostgreSQL</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/database/remove-bloat-from-amazon-aurora-and-rds-for-postgresql-with-pg_repack/">Remove bloat from Amazon Aurora and RDS for PostgreSQL with pg_repack</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/@hnasr/the-postgres-replication-dilemma-72bef6cc4599">The Postgres replication dilemma</a></p>
</li>
<li><p><a target="_blank" href="https://www.cybertec-postgresql.com/en/what-hot_standby_feedback-in-postgresql-really-does/">WHAT HOT_STANDBY_FEEDBACK IN POSTGRESQL REALLY DOES</a></p>
</li>
</ol>
<hr />
<h2 id="heading-bonus-content">Bonus Content 🤓</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719307297190/ff204fa7-d43a-41ca-95d2-bb170cc70481.png" alt class="image--center mx-auto" /></p>
<hr />
]]></content:encoded></item><item><title><![CDATA[From Duplication to Unification: How Cloudflare Workers Helped Us Centralize Shared Logic]]></title><description><![CDATA[Problem
In our codebase, we had a very specific parser responsible for converting meta JSON created on front-end into a format ingestible by our backend. The parser was exclusively utilized by the frontend until recently. However, while we were build...]]></description><link>https://blog.certa.dev/from-duplication-to-unification-how-cloudflare-workers-helped-us-centralize-shared-logic</link><guid isPermaLink="true">https://blog.certa.dev/from-duplication-to-unification-how-cloudflare-workers-helped-us-centralize-shared-logic</guid><category><![CDATA[cloudflare-worker]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[cloudflare]]></category><dc:creator><![CDATA[Sushant Gulati]]></dc:creator><pubDate>Wed, 22 May 2024 07:42:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/40XgDxBfYXM/upload/8391588b0499bc287ce4dd60a3c871cb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-problem">Problem</h1>
<p>In our codebase, we had a very specific parser responsible for converting meta JSON created on front-end into a format ingestible by our backend. The parser was exclusively utilized by the frontend until recently. However, while we were building our AI suite, another use-case emerged, where the backend also needed to use the same utility. Since this parser undergoes frequent updates, maintaining separate copies of the same logic in different languages is a maintenance nightmare - it's error prone &amp; increases unnecessary dependencies between teams.</p>
<p>At Certa, we believe in building scalable processes - inside &amp; out. So, we decided to build a centralized solution.</p>
<h1 id="heading-the-ideal-solution">The Ideal Solution</h1>
<p>The ideal solution to this problem would have the following characteristics:</p>
<ul>
<li><p><strong>Seamless Integration:</strong> It had to integrate smoothly with our existing codebases, minimizing the need for extensive restructuring or managing multiple repositories.</p>
</li>
<li><p><strong>Maintainability:</strong> Easily maintainable and automatable, allowing for straightforward updates and integration with deployment pipelines.</p>
</li>
<li><p><strong>Scalability:</strong> The solution had to scale effortlessly to accommodate fluctuations in demand, ensuring consistent performance under varying loads.</p>
</li>
<li><p><strong>Low Latency:</strong> Ensuring low latency was paramount for optimal performance, as every millisecond counts in delivering a seamless UX. This was crucial for maintaining our high standards of customer satisfaction.</p>
</li>
<li><p><strong>Cost-Effectiveness:</strong> The solution needed to be cost-effective, providing a balance between performance and expense.</p>
</li>
</ul>
<p>With these characteristics identified and a bit of research, we arrived at 3 potential solutions:</p>
<ol>
<li><p><strong>Shared Library</strong></p>
<p> Initially, publishing a library on NPM seemed like a straightforward solution. However, our backend is driven by Django, which presented significant hurdles. While workarounds such as wrappers or transpilers exist, they introduce unnecessary complexities. Although we could automate some of this through CI/CD pipeline, but the compatibility issues remained a concern.</p>
</li>
<li><p><strong>WASM Module</strong></p>
<p> To address the language compatibility issues, we considered converting our code to AssemblyScript. We could set up a CI/CD pipeline that would allow us to compile it into a WASM module and upload it to a CDN. But this solution had a few challenges of its own. The most notable one being the latency that is introduced when passing large data from JavaScript to WASM (and vice versa). This latency would be noticeably worse when compared to function calls to the same parser, within JavaScript.</p>
</li>
<li><p><strong>Serverless Functions</strong></p>
<p> Serverless functions like <a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a> seemed to be one of the better options, compared to the other two, for our use-case. The code can exist in our frontend repository, which can be deployed whenever it is changed and pushed. The serverless functions provided several benefits like:</p>
<ul>
<li><p>Automatic Scaling based on demand</p>
</li>
<li><p>Cost-efficient operation, especially for tasks that are not constantly running</p>
</li>
<li><p>An option to deploy on edge for ultra low latency in all regions around the world</p>
</li>
</ul>
</li>
</ol>
<p>    However, there's still the issue of cold starts. A <strong>“cold start”</strong> refers to the duration required to initialize and execute a fresh instance of a serverless function. The delay caused by cold starts, especially in scenarios with infrequent invocations, could impact user experience.</p>
<p><img src="https://i.imgflip.com/8pur76.jpg" alt class="image--center mx-auto" /></p>
<h1 id="heading-solution-cloudflare-workers">Solution: Cloudflare Workers</h1>
<p>While exploring our serverless options, <a target="_blank" href="https://workers.cloudflare.com/">Cloudflare Workers</a> emerged as the ideal solution for our use case, meeting all our criteria. With its 0ms cold starts, this edge service allows our backend microservices to access the transformational code almost instantaneously. Read more on <a target="_blank" href="https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers"><strong>how Cloudflare Workers is able to eliminate cold starts</strong></a><strong>;</strong> it's inspiring to see how they've used isolates to eliminate cold-starts!</p>
<h2 id="heading-how-did-we-use-it">How did we use it?</h2>
<h3 id="heading-initial-setup">Initial Setup</h3>
<ol>
<li><p>We started off by adding a new package into our front-end monorepo as this code clearly needed to be independent of the rest our front-end logic.</p>
</li>
<li><p>Within this package, we initialized a Cloudflare Workers project and moved the parsers' logic into it, exposing an endpoint accessible to external services.</p>
</li>
<li><p>To have a cleaner and maintainable code, we used <a target="_blank" href="https://github.com/kwhitley/itty-router"><code>itty-router</code></a> library, which offered a more elegant solution compared to nested if-else statements in the Worker's fetch event handler.</p>
</li>
<li><p>Deployment was done using <code>wrangler deploy</code> command, <a target="_blank" href="https://www.npmjs.com/package/wrangler?activeTab=readme">which</a> was installed when we initialized the Cloudflare Workers project.</p>
<p> It looked something like this:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { IRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"itty-router"</span>;
 <span class="hljs-keyword">import</span> { Router, json, error, createCors } <span class="hljs-keyword">from</span> <span class="hljs-string">"itty-router"</span>;

 <span class="hljs-keyword">const</span> router = Router({ base: <span class="hljs-string">"/api"</span> });

 <span class="hljs-keyword">const</span> { preflight, corsify } = createCors({
   origins: [<span class="hljs-string">"*"</span>],
   methods: [<span class="hljs-string">"POST"</span>, <span class="hljs-string">"OPTIONS"</span>]
 });

 router
   <span class="hljs-comment">// embedding preflight upstream to handle all OPTIONS requests</span>
   .all(<span class="hljs-string">"*"</span>, preflight)
   .post(<span class="hljs-string">"/tranform/"</span>, yourTransformController)
   .all(<span class="hljs-string">"*"</span>, <span class="hljs-function">() =&gt;</span> error(<span class="hljs-number">404</span>, <span class="hljs-string">"Invalid endpoint"</span>));

 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
   fetch: <span class="hljs-function">(<span class="hljs-params">req: IRequest</span>) =&gt;</span>
     router
       .handle(req)
       .then(json)
       .catch(error)
       <span class="hljs-comment">// corsify all Responses (including errors)</span>
       .then(corsify)
 };
</code></pre>
</li>
</ol>
<h3 id="heading-benchmarks">Benchmarks</h3>
<p>After completing the initial setup and deployment of the parser on Cloudflare Workers, we conducted testing to verify its functionality. Our tests focused on assessing the round-trip time (RTT).</p>
<p>These tests were performed on a MacBook M1 Pro with a high-speed internet connection over WiFi.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Payload size</td><td>Local function invocation</td><td>Processing time (Remote)</td><td>Remote Round Trip Time</td></tr>
</thead>
<tbody>
<tr>
<td>100KB</td><td>3.04ms</td><td>3.09ms</td><td>207ms</td></tr>
<tr>
<td>1.5MB</td><td>22.06ms</td><td>21.98ms</td><td>1511ms</td></tr>
<tr>
<td>9MB</td><td>113.75ms</td><td>115.11ms</td><td>5217ms</td></tr>
</tbody>
</table>
</div><h3 id="heading-results">Results</h3>
<p>Our testing results showed that the parser was performing well within acceptable limits. Notably, the benchmarks involved a larger-than-usual payload size of <strong>1.5MB</strong>. For typical payloads (less than <strong>100KB</strong>), the round-trip time (RTT) was consistently around <strong>200 ms</strong>.</p>
<p>Given these outcomes, we decided to proceed with this solution.</p>
<h3 id="heading-finally-setting-up-cicd">Finally, setting up CI/CD</h3>
<p>The next step was to set up a CI/CD pipeline to ensure that any changes made to the parser would be automatically deployed on Cloudflare Workers. Deploying our project was straightforward using Cloudflare's GitHub Action, <a target="_blank" href="https://github.com/cloudflare/wrangler-action"><code>cloudflare/wrangler-action@v3</code></a>.</p>
<p>To optimize the deployment process, we made a key modification. We configured the GitHub Action to trigger only when changes were made to the specific folder containing our parser. This targeted approach prevents unnecessary deployments and ensures that the action only runs when there are relevant updates.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">development</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-comment"># Only run the workflow when changes are made to our code</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"src/parser/*"</span>

<span class="hljs-string">..</span>
</code></pre>
<h1 id="heading-conclusion-how-did-we-benefit-from-this"><strong>Conclusion: How did we benefit from this?</strong></h1>
<ul>
<li><p><strong>Minimal Latency:</strong> Our system consistently achieves an average round trip of around 200ms for typical use cases to transform and return data on a decent network, complemented by 0ms cold starts, ensuring consistent and fast response times for all requests.</p>
</li>
<li><p><strong>Reduced Maintenance</strong>: By utilizing a single codebase, we've significantly reduced maintenance efforts. Managing two repositories wouldn't just increase complexity but also add to management tasks without necessarily demanding more resources.</p>
</li>
<li><p><strong>Scalability</strong>: The system seamlessly handles traffic spikes, without any degradation in performance. The deployment across 200 cities ensures that we can provide consistent service to users globally.</p>
</li>
<li><p><strong>Cost Savings</strong>: In contrast to other serverless options, Cloudflare Workers charge based on CPU time instead of execution duration, which includes both active processing and idle waiting times. For our expected usage of 20 million requests and 100 million CPU milliseconds per month, the total expected monthly cost is only $9.40, significantly lower than alternative serverless options.</p>
</li>
</ul>
<h1 id="heading-we-are-hiring"><strong>We are hiring!</strong></h1>
<p>If solving challenging problems at scale in a fully remote team interests you, head to our <a target="_blank" href="https://www.getcerta.com/careers"><strong>careers page</strong></a> and apply for the position of your liking!</p>
]]></content:encoded></item><item><title><![CDATA[Beyond Pass or Fail: Diagnosing Flaky Test Cases]]></title><description><![CDATA[Background
As development teams rely on CI/CD tools and workflows more and more to deploy their applications, testing has become a key integral part of the SDLC. By using automated testing tools like CircleCI or GitHub Actions, we can ensure our code...]]></description><link>https://blog.certa.dev/beyond-pass-or-fail-diagnosing-flaky-test-cases</link><guid isPermaLink="true">https://blog.certa.dev/beyond-pass-or-fail-diagnosing-flaky-test-cases</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Mayank Jha]]></dc:creator><pubDate>Wed, 01 May 2024 07:09:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/YVv7RO-47bI/upload/4118383708a466249d1fced7ea93d75b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>As development teams rely on CI/CD tools and workflows more and more to deploy their applications, testing has become a key integral part of the SDLC. By using automated testing tools like CircleCI or GitHub Actions, we can ensure our code meets requirements, performs reliably, and is maintainable in the long run</p>
<p>However, as codebases increase in complexity and size with the addition of new features over time, so do the number of test cases that we write to ensure proper coverage and verification of our core business logic. With that, most development teams eventually run into the problem with <strong>flaky test cases</strong>.</p>
<p>In this blog, we will navigate through the various causes of flakiness within test cases and how we can aim to resolve them. For context, we will stick to python3, and Django as examples to follow along with.</p>
<h2 id="heading-how-do-we-define-flaky-test-cases">How do we define flaky test cases?</h2>
<p>Flaky test cases can be normally defined as test cases that can sometimes pass or fail without any changes to the underlying code. These test cases flake due to some form of non-determinism within the code that lead to mismatched results or undefined behavior.</p>
<p>Facing these flaky issues within CI pipelines is a growing problem for development teams of all sizes as it leads to hindrance in productivity, loss of trust in the CI tools, and frustrations all around. Flakiness is a characteristic of test suites that present themselves more within CI environments in general. The reason for this could be the differences in resources when comparing it to local development environments and the under-the-hood dependencies that may give way to conflicts.</p>
<p>Luckily, modern-day CI tools are well suited to help identify flakiness within our test suites as part of their offerings and while they may help in identifying flaky test cases, the responsibility falls upon the development teams to go in and address those issues promptly to keep the codebase clean and up to the mark.</p>
<h2 id="heading-diagnosing-the-problem">Diagnosing the problem</h2>
<p>Test cases can fail for a variety of reasons, as non-determinism can manifest from the most seemingly random or innocent places within the code. Having said that, when dealing with flaky test cases over a long period, it becomes easier to see the few patterns that emerge from test suites that help us identify root causes quickly and be more mindful about how we write our test cases.</p>
<p>Let’s dive deeper into the various causes of flaky test cases and what we can do to remedy them.</p>
<h3 id="heading-1-asynchronous-functions">1. Asynchronous Functions</h3>
<p>When dealing with test cases where the target functionality executes asynchronously, there may be scenarios where the expected data is not ready by the time we run our assertions, leading to flakiness. This scenario is one of the more common reasons why test cases flake.</p>
<p>Let’s take the example of a function that we invoke via celery.</p>
<pre><code class="lang-python"><span class="hljs-comment"># tasks.py</span>
<span class="hljs-keyword">from</span> celery <span class="hljs-keyword">import</span> shared_task
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> YourModel

<span class="hljs-meta">@shared_task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">internal_task</span>(<span class="hljs-params">model_id</span>):</span>
    model_instance = YourModel.objects.get(pk=model_id)
    <span class="hljs-comment"># Perform some operations on the model instance</span>
    model_instance.some_field = <span class="hljs-string">'modified'</span>
    model_instance.save()
</code></pre>
<p>When writing test cases, we generally go from writing unit tests —&gt; integration tests —&gt; end-to-end tests. While the core functionality of our sample function is verified within unit tests, integration tests are often found to be flakier due to the asynchronous behavior we introduce within the entire flow.</p>
<p>In other words, if we were to invoke this function within a function-based view for a Django application, we would likely do something along the lines of</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> django.shortcuts <span class="hljs-keyword">import</span> render
<span class="hljs-keyword">from</span> django.http <span class="hljs-keyword">import</span> HttpResponse
<span class="hljs-keyword">from</span> .tasks <span class="hljs-keyword">import</span> internal_task
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> YourModel

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">my_view</span>(<span class="hljs-params">request</span>):</span>
    <span class="hljs-comment"># Assume you have a model instance</span>
    model_instance = YourModel.objects.get(pk=<span class="hljs-number">1</span>)

    <span class="hljs-comment"># Call the Celery task asynchronously</span>
    internal_task.delay(model_instance.id)

    <span class="hljs-keyword">return</span> HttpResponse(<span class="hljs-string">"Task has been queued for execution."</span>)
</code></pre>
<p>When writing integration tests for this view, we might encounter flakiness if the celery task does not finish execution before our assertions are made.</p>
<p>This is where <code>Mocking</code> comes in handy!</p>
<p>Mocking allows developers to replace parts of the code with mock objects, which simulate the behavior of real objects. This is particularly useful when testing asynchronous functions because it enables us to isolate the function being tested from its dependencies. By mocking external dependencies such as database queries or API calls, we can focus solely on testing the function's logic without worrying about the behavior of other components.</p>
<pre><code class="lang-python"><span class="hljs-comment"># test_views.py</span>
<span class="hljs-keyword">from</span> django.test <span class="hljs-keyword">import</span> TestCase, Client
<span class="hljs-keyword">from</span> django.urls <span class="hljs-keyword">import</span> reverse
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> YourModel
<span class="hljs-keyword">from</span> unittest.mock <span class="hljs-keyword">import</span> patch

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyViewTest</span>(<span class="hljs-params">TestCase</span>):</span>
<span class="hljs-meta">    @patch('yourapp.tasks.internal_task.delay')</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_my_view</span>(<span class="hljs-params">self, mock_internal_task_delay</span>):</span>
        <span class="hljs-comment"># Create a model instance</span>
        model_instance = YourModel.objects.create(id=<span class="hljs-number">1</span>)

        <span class="hljs-comment"># Call the view function</span>
        client = Client()
        response = client.get(reverse(<span class="hljs-string">'my_view'</span>))

        <span class="hljs-comment"># Check if the response is successful</span>
        self.assertEqual(response.status_code, <span class="hljs-number">200</span>)

        <span class="hljs-comment"># Check if the Celery task is called with the correct arguments</span>
        mock_internal_task_delay.assert_called_once_with(model_instance.id)
</code></pre>
<p>By doing this, we can still write our integration tests by relying on the individual unit tests to verify functionality.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Mocking is also very useful when dealing with external APIs as we cannot base our tests on the reliability of a third-party source. By utilizing mocking, we can cater to testing how we want our application to perform without worrying about handling what external sources send back!</div>
</div>

<h3 id="heading-2-improper-management-of-state">2. Improper Management of State</h3>
<p>This kind of flaky test case is more subtle as during development, it can be easily missed.</p>
<p>When writing new test cases, we often only test the particular suite we are working on. However, when we push our code into our CI pipelines, we run all of our test cases at once. What ends up happening is that test cases that were invoked before the new test case that was pushed as part of PR can unintentionally alter the state of the system leading to flaky test cases.</p>
<p>When running these test cases locally, we might think that our test case is robust but a different test case that was run before yours was responsible for altering the state of the system about which you made assumptions.</p>
<p>This modification of state most commonly stems from improper management of resources and states within test suites.</p>
<p>For example, let us take a look at the following test suite.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> django.test <span class="hljs-keyword">import</span> TransactionTestCase
<span class="hljs-keyword">from</span> django.utils <span class="hljs-keyword">import</span> timezone

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestFlakyTimeZone</span>(<span class="hljs-params">TransactionTestCase</span>):</span>

<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setUpClass</span>(<span class="hljs-params">cls</span>):</span>
        super().setUpClass()
        <span class="hljs-comment"># Alter global timezone without proper teardown</span>
        timezone.activate(<span class="hljs-string">"Asia/Kolkata"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_something_with_timezone</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Test logic that relies on the altered timezone</span>
        <span class="hljs-keyword">assert</span> timezone.is_active <span class="hljs-keyword">is</span> <span class="hljs-literal">True</span>  <span class="hljs-comment"># Example assertion</span>
</code></pre>
<p>Seemingly harmless, but on closer inspection, we find out that <code>timezone.activate()</code> modifies the timezone globally and is not limited to the function scope. This introduces inadvertent flakiness to any other downstream test cases that make assumptions about the timezone and hence fail due to the unfortunate ordering of the test cases.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Playing around with the order of test executions will always give good insights to isolate issues</div>
</div>

<p>What we can do here to mitigate such scenarios is to ensure we cater for proper cleanup of resources within our test suite. For python3, when using the in-built <code>unittest</code> module, we can use the <code>tearDown()</code> or <code>tearDownClass()</code> to handle the cleanup.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pytest
<span class="hljs-keyword">from</span> django.test <span class="hljs-keyword">import</span> TransactionTestCase
<span class="hljs-keyword">from</span> django.utils <span class="hljs-keyword">import</span> timezone

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestFlakyTimeZone</span>(<span class="hljs-params">TransactionTestCase</span>):</span>
<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setUpClass</span>(<span class="hljs-params">cls</span>):</span>
        super().setUpClass()
        <span class="hljs-comment"># Alter global timezone without proper teardown</span>
        timezone.activate(<span class="hljs-string">"Asia/Kolkata"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_something_with_timezone</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Test logic that relies on the altered timezone</span>
        <span class="hljs-keyword">assert</span> timezone.is_active <span class="hljs-keyword">is</span> <span class="hljs-literal">True</span>  <span class="hljs-comment"># Example assertion</span>

<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">tearDownClass</span>(<span class="hljs-params">cls</span>):</span>
            timezone.deactivate()
</code></pre>
<p>This is just one example of where cleanup is important. However, there can be other scenarios where open files, disconnected signals, or some altered state must be reset to their original state to allow downstream tests to run smoothly.</p>
<h3 id="heading-3-parallelism-and-transactions">3. Parallelism and Transactions</h3>
<p>To expedite the execution of our test suites, it's common practice to employ various tools that facilitate the distribution of tests to run concurrently. For example, when utilizing <a target="_blank" href="https://docs.pytest.org/en/8.0.x/">pytest</a>, we frequently leverage <a target="_blank" href="https://pypi.org/project/pytest-xdist/">pytest-xdist</a> to distribute tests across multiple CPUs, significantly improving execution times.</p>
<p>While parallel test execution offers notable performance benefits, it can introduce nuanced challenges, particularly when dealing with database transactions. This is especially evident in the case of pytest-xdist, where tests are parallelized at the function level. Let's illustrate this with an example from Django's <a target="_blank" href="https://docs.djangoproject.com/en/5.0/topics/testing/tools/#transactiontestcase"><code>TransactionTestCase</code></a>.</p>
<p><code>TransactionTestCase</code> encapsulates each test method within a database transaction, ensuring that the database state is reverted to its original state upon test completion. However, a common issue arises when individual test cases attempt to manipulate shared data concurrently.</p>
<p>The crux of the problem lies in the potential for conflicts arising from simultaneous modifications to shared data. Such conflicts can lead to test flakiness, where the outcome of a test becomes unpredictable due to interference from other concurrently executing tests.</p>
<p>For example -</p>
<pre><code class="lang-python"><span class="hljs-comment"># test_example.py</span>
<span class="hljs-keyword">from</span> django.test <span class="hljs-keyword">import</span> TransactionTestCase
<span class="hljs-keyword">from</span> myapp.models <span class="hljs-keyword">import</span> ExampleObject

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleTestCase</span>(<span class="hljs-params">TransactionTestCase</span>):</span>
<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setUpClass</span>(<span class="hljs-params">cls</span>):</span>
        super().setUpClass()
        cls.example_object = ExampleObject()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_operation_1</span>(<span class="hljs-params">self</span>):</span>
        self.example_object.perform_operation_1()
        <span class="hljs-keyword">assert</span> self.example_object.get_result() == expected_result_1

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_operation_2</span>(<span class="hljs-params">self</span>):</span>
        self.example_object.perform_operation_2()
        <span class="hljs-keyword">assert</span> self.example_object.get_result() == expected_result_2
</code></pre>
<p>Let’s visualize how the two test cases interact with the shared object :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712725054427/496843c1-2460-4483-9f75-97ac145f0ec0.png" alt class="image--center mx-auto" /></p>
<p>In this example, we can observe a potential conflict that leads to flaky test cases because, in TransactionTestCase, every test method is wrapped in its transaction. However, since we defined our <code>ExampleObject</code> within the <code>setUpClass</code> method, this essentially acts as a shared resource. So, what we may run into here is an instance where while using <code>pytest-xdist</code> , the two test methods run in parallel but run into a database transaction error due to the shared resource being acted upon in one or the other method.</p>
<p>One approach to solving this would be to use the <code>setUp()</code> method instead.</p>
<pre><code class="lang-python"><span class="hljs-comment"># test_example.py</span>
<span class="hljs-keyword">import</span> pytest
<span class="hljs-keyword">from</span> django.test <span class="hljs-keyword">import</span> TransactionTestCase
<span class="hljs-keyword">from</span> myapp.models <span class="hljs-keyword">import</span> ExampleObject

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleTestCase</span>(<span class="hljs-params">TransactionTestCase</span>):</span>
<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setUp</span>(<span class="hljs-params">self</span>):</span>
        super().setUp()
        self.example_object = ExampleObject()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_operation_1</span>(<span class="hljs-params">self</span>):</span>
        self.example_object.perform_operation_1()
        <span class="hljs-keyword">assert</span> self.example_object.get_result() == expected_result_1

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_operation_2</span>(<span class="hljs-params">self</span>):</span>
        self.example_object.perform_operation_2()
        <span class="hljs-keyword">assert</span> self.example_object.get_result() == expected_result_2
</code></pre>
<p>Let’s visualize how this change affects our test case!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712725081694/63a3eee0-ec76-4f9a-b82b-8d197e012dae.png" alt class="image--center mx-auto" /></p>
<p>It is important to note that this solution is not a silver bullet by any means, and there are a few things to keep in mind when solving concurrency-related issues for flaky test cases!</p>
<ol>
<li><p>If we use <code>setUp</code> we will trade off on execution times and memory since we will instantiate the <code>ExampleObject</code> every time for every test method</p>
</li>
<li><p>There can be nuances within the code you are meaning to test that may require handling concurrency differently.</p>
</li>
</ol>
<p>To mitigate such issues, it's essential to design test cases that isolate their data dependencies and avoid modifying shared resources whenever possible. This ensures test reliability and repeatability across different execution environments.</p>
<h3 id="heading-4-improper-usage-of-assertions">4. Improper Usage of Assertions</h3>
<p>This category of flaky test cases is more of a development miss than an environmental dependency or systematic nature of execution. They can occur randomly and are a common “right-under-your-nose” error.</p>
<blockquote>
<p>This is the missing semicolon equivalent for test cases after all!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712725144214/30260c4d-52d8-4b93-946c-47b4ad67f6e7.png" alt class="image--center mx-auto" /></p>
</blockquote>
<p>When using assertions, it is important to remember the data types we are dealing with and use appropriate assert statements for varying kinds of expected values.</p>
<p>The most common example here is the use of <code>assertListEqual</code> when comparing 2 lists. Sometimes, if the order is not guaranteed for our <code>actual_value</code> , we can often run into flaky scenarios where our <code>expected_value</code> has mismatched elements.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> unittest
<span class="hljs-keyword">from</span> my_app.utils <span class="hljs-keyword">import</span> foo

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestListComparison</span>(<span class="hljs-params">unittest.TestCase</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_assert_list_equal</span>(<span class="hljs-params">self</span>):</span>
        expected_value = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]
        actual_value = foo()  <span class="hljs-comment"># Returns [1, 2, 3] in no particular order</span>
        self.assertListEqual(expected_value, actual_value)
</code></pre>
<p>In the above scenario, our test case will be flaky as <code>assertListEqual</code> is designed to match the elements in the order of our expected value. As our test case function <code>foo()</code> returns the same elements in no particular order, it is more suitable to use <code>assertCountEqual</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> unittest
<span class="hljs-keyword">from</span> my_app.utils <span class="hljs-keyword">import</span> foo

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestListComparison</span>(<span class="hljs-params">unittest.TestCase</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_assert_count_equal</span>(<span class="hljs-params">self</span>):</span>
        expected_value = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]
        actual_value = foo()  <span class="hljs-comment"># Returns [1, 2, 3] in no particular order</span>
        self.assertCountEqual(expected_value, actual_value)
</code></pre>
<h2 id="heading-5-date-time-flakiness">5. Date-time Flakiness</h2>
<p>DateTime flakiness in testing can occur due to various factors, not merely as a systemic issue. Fluctuations in system time, environment differences, and asynchronous operations are the primary reasons behind this unpredictability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712725218968/b3aea819-c552-45d6-b58a-5b7bd87f76d4.png" alt class="image--center mx-auto" /></p>
<p>Let’s take a look at a scenario where a test case measures the elapsed time between two operations:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> unittest <span class="hljs-keyword">import</span> TestCase, mock

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">is_within_business_hours</span>(<span class="hljs-params">current_time</span>):</span>
  <span class="hljs-string">"""Checks if the current time falls within business hours (9am to 5pm)."""</span>
  start_of_business = datetime.time(<span class="hljs-number">9</span>, <span class="hljs-number">0</span>)
  end_of_business = datetime.time(<span class="hljs-number">17</span>, <span class="hljs-number">0</span>)
  <span class="hljs-keyword">return</span> start_of_business &lt;= current_time &lt;= end_of_business

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DatetimeFlakinessTestCase</span>(<span class="hljs-params">TestCase</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_business_hours</span>(<span class="hljs-params">self</span>):</span>
          <span class="hljs-string">"""Tests if the current time is within business hours."""</span>
          current_time = datetime.datetime.now().time()
          <span class="hljs-keyword">assert</span> is_within_business_hours(current_time)

        <span class="hljs-comment"># This test might fail if it runs exactly at 9:00 or 5:00</span>
        <span class="hljs-comment"># due to the nature of datetime.now() capturing a specific moment.</span>
</code></pre>
<p>This test relies on <a target="_blank" href="http://datetime.now"><code>datetime.now</code></a><code>()</code> to get the current time. If the test runs exactly at 9:00 am or 5:00 pm, it might fail because <code>is_within_business_hours</code> checks for strict inequalities (<code>&lt;=</code> and <code>&gt;=</code>). A test run at 9:00 on the dot might capture a time slightly before 9:00, causing the test to fail.</p>
<p>This is a flaky test because its outcome depends on the exact moment it runs, not the functionality it tries to verify.</p>
<p>Although the above example may be the trivial and rather incorrect approach to writing any test case, we can still see the underlying patterns of flakiness emerge.</p>
<p>To address this, we can go back to the idea of mocking described earlier to address the <a target="_blank" href="http://datetime.datetime.now"><code>datetime.datetime.now</code></a><code>()</code> function using the <code>unittest.mock</code> module</p>
<p>The updated test case would look something like this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> unittest <span class="hljs-keyword">import</span> TestCase, mock

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">is_within_business_hours</span>(<span class="hljs-params">current_time</span>):</span>
    <span class="hljs-string">"""Checks if the current time falls within business hours (9am to 5pm)."""</span>
    start_of_business = datetime.time(<span class="hljs-number">9</span>, <span class="hljs-number">0</span>)
    end_of_business = datetime.time(<span class="hljs-number">17</span>, <span class="hljs-number">0</span>)
    <span class="hljs-keyword">return</span> start_of_business &lt;= current_time &lt;= end_of_business

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DatetimeFlakinessTestCase</span>(<span class="hljs-params">TestCase</span>):</span>
<span class="hljs-meta">    @patch('datetime.datetime')</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_business_hours</span>(<span class="hljs-params">self, mock_datetime</span>):</span>
        <span class="hljs-string">"""Tests if the current time is within business hours (using mock)."""</span>
        <span class="hljs-comment"># Set a fixed time within business hours</span>
        mock_datetime.now.return_value = datetime.datetime(<span class="hljs-number">2024</span>, <span class="hljs-number">4</span>, <span class="hljs-number">1</span>, <span class="hljs-number">10</span>, <span class="hljs-number">45</span>)
        current_time = datetime.datetime.now().time() <span class="hljs-comment"># Now uses the mocked time</span>
        <span class="hljs-keyword">assert</span> is_within_business_hours(current_time)
</code></pre>
<p>In this modified test case, we mock <a target="_blank" href="http://datetime.datetime.now"><code>datetime.datetime.now</code></a><code>()</code> to provide fixed timestamps for the start and end times of the operation. By controlling the timestamps, we eliminate DateTime flakiness, ensuring consistent test outcomes regardless of system time fluctuations or asynchronous operations.</p>
<h1 id="heading-what-actions-can-we-take-to-tackle-this">What actions can we take to tackle this?</h1>
<p>Since flaky test cases can occur for apparently any reason, there is no one cheat sheet available that can be used for all root causes. Although we can be more proactive and mindful about writing resilient test cases and ensuring thorough reviews, sometimes a flaky test case requires diving into nuances of our application’s core logic.</p>
<p>Some starting approaches we can take to tackle flakiness are ➖</p>
<h3 id="heading-using-tools-specifically-designed-to-address-flaky-test-cases">Using tools specifically designed to address flaky test cases</h3>
<p>It can be counterproductive to sit and rerun test case after test case waiting for something to happen. Thankfully, there are tools available that allow us to execute test cases locally in bulk so we can identify flaky test cases early and often.</p>
<p>For <code>pytest</code>, you can use <a target="_blank" href="https://pypi.org/project/pytest-flakefinder/"><code>pytest-flakefinder</code></a> . This tool allows you to multiply your tests so they run in bulk without having to restart <code>pytest</code></p>
<p>A good rule of thumb if using these tools is to run them with a combination of parameters to try and see which conditions cause breakage. An example would be when using <code>pytest-xdist</code> distributed testing alongside <code>pytest-flakefinder</code></p>
<pre><code class="lang-bash">pytest src/tests/test.py --flake-finder -n4 --flake-runs=n --reuse-db
pytest src/tests/test.py --flake-finder -n0 --flake-runs=n --reuse-db
pytest src/tests/test.py --flake-finder --flake-runs=n --reuse-db
pytest src/tests/test.py -n4 --reuse-db
pytest src/tests/test.py --reuse-db
</code></pre>
<p>where <code>n</code> in range of <code>[1,2,5,10,50,500]</code> the number of “multiplications” we want to test for.</p>
<p>This approach to testing can ensure that some of the more obvious causes of flakiness are identified early and should be adopted as a practice when raising PRs.</p>
<h3 id="heading-reproducing-ci-executions-locally">Reproducing CI executions locally</h3>
<p>More than likely, when you use a CI tool for automated tests, chances are you would be distributing test cases across multiple containers to speed up execution times.</p>
<p>As mentioned above, test cases running in isolation may not be an issue but test cases that inadvertently cause a "test-on-test dependency", and flakiness may occur.</p>
<p>For larger codebases, there can be a lot of tests that make identifying the “problem child” rather tedious. In cases like this, a good approach would be to inspect what test cases were run before your alleged flaky test case on your CI tool and pull in the same execution parameters locally.</p>
<h3 id="heading-re-runs-re-runs-and-re-runs">Re-runs, Re-runs, and Re-runs!</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712725284234/3ef6a948-63f7-4926-9629-090e45db34da.png" alt class="image--center mx-auto" /></p>
<p>Believe it or not, sometimes a good old-fashioned re-run is all it takes. Having a retry mechanism with your test cases can be beneficial to reduce failing builds for your application and improve productivity. Various tools out there can be utilized for this such as <a target="_blank" href="https://pypi.org/project/pytest-rerunfailures/"><strong>pytest-rerunfailures</strong></a></p>
<p>While this approach may result in frequently failing builds, we should aim to limit the number of retries to avoid flaky tests to continue living on in the system. Finding the right balance of retries can vary but a good starting point that has worked for us is no more than 3.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Flaky test cases are elusive. More often than not they can slip through the cracks of development and reviews, causing undue frustrations for everyone else down the line. As developers, we should adopt a proactive approach to resolving flakiness in test cases.</p>
<p>Sometimes it may seem easier to quarantine or skip the flakes as we find them but having them end up in a bucket that no one addresses can lead to an unstable platform that hasn’t addressed its core issues.</p>
<p>It is important to note that there is never a one-size-fits-all solution to resolving these things and more often than not, the real root causes can be more nuanced and hint towards deeper issues within the system. However, by being mindful of the different caveats of our tests during development and being proactive and prompt with resolving flaky test cases as they creep into our CI builds, we can ensure the reliability of our systems and restore trust in our external tools.</p>
]]></content:encoded></item><item><title><![CDATA[Graph Databases: To Use or not to use?]]></title><description><![CDATA[Introduction
In today's interconnected world, data isn't just about rows and columns—it's about relationships. From social networks connecting friends and family to recommendation engines suggesting our next favorite movie, understanding these intric...]]></description><link>https://blog.certa.dev/graph-databases-to-use-or-not-to-use</link><guid isPermaLink="true">https://blog.certa.dev/graph-databases-to-use-or-not-to-use</guid><category><![CDATA[graph database]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[graphdatabaserelationships]]></category><category><![CDATA[cypher]]></category><category><![CDATA[gremlin]]></category><category><![CDATA[Databases]]></category><category><![CDATA[aql]]></category><category><![CDATA[gql]]></category><category><![CDATA[apacheage]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[pg routing]]></category><category><![CDATA[cache]]></category><category><![CDATA[Redis]]></category><dc:creator><![CDATA[Sachin Harpalani]]></dc:creator><pubDate>Tue, 16 Apr 2024 05:33:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712557088788/3073ed8b-88d4-4ce1-ae6e-27599b9401e9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction"><strong>Introduction</strong></h1>
<p>In today's interconnected world, data isn't just about rows and columns—it's about relationships. From social networks connecting friends and family to recommendation engines suggesting our next favorite movie, understanding these intricate connections is essential for unlocking the full potential of our data.</p>
<p>Enter graph databases—the specialized database designed to do just that.</p>
<p>In this blog post, we'll delve into the world of graph databases, exploring their unique features, typical use cases, and alternatives, to answer the question: To use or not to use?</p>
<h1 id="heading-understanding-graph-databases">Understanding Graph Databases</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712552363613/8c4861aa-d450-4990-9c07-3ddf4a4a62c5.jpeg?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If this is you, don’t worry, we’ve got you covered. Let’s dive into understanding the basics.</p>
<h2 id="heading-fundamentals">Fundamentals</h2>
<p>A graph database is a specialized, single-purpose platform used to create and manipulate data of an associative and contextual nature. The graph itself contains nodes, edges, and properties that come together to allow users to represent and store data in a way that relational databases aren’t equipped to do.</p>
<p>The main concept of a graph database system is a relationship. Relationships are defined as first-class citizens — this means everything you can do with all other elements can be done with a relationship. Data is related together in a graph to store a collection of nodes and edges, where the edges represent the relationship between nodes.</p>
<p>Relationships allow data within the system to be linked together directly. Querying relationships in a graph database is fast since they’re stored in a way that doesn’t change. You may also visualize them, which makes them great for deriving insights for heavily interconnected data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712552428982/eafefd5d-38f5-402c-aadb-7a003600457c.png?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>In this example: <code>User</code> are nodes that have properties <code>name</code>, <code>surname</code> and <code>FOLLOWS</code> is the relationship between the nodes which can have properties (like <code>since</code> ) of it’s own.</p>
<p>Now that we are done with the basics, let’s move on to the next segment</p>
<h2 id="heading-use-cases-of-graph-databases">Use Cases of Graph Databases</h2>
<p><strong>1. Social Networks:</strong> Graph databases are ideal for modeling social networks, where users, friendships, likes, and interactions form a complex web of relationships. By representing users as nodes and connections between them as edges, graph databases facilitate efficient querying for friend recommendations, community detection, and content personalization.</p>
<p><strong>2. Recommendation Engines:</strong> Graph databases power recommendation engines by analyzing user behavior, preferences, and item similarities to generate personalized recommendations. By modeling users, items, and their interactions as nodes and edges, graph databases enable efficient recommendation algorithms that can scale to millions of users and items.</p>
<p><strong>3. Fraud Detection:</strong> Graph databases play a crucial role in fraud detection by identifying patterns of suspicious behavior within networks of transactions, users, and entities. By analyzing the flow of money and connections between accounts, graph databases can uncover fraudulent activities such as money laundering, identity theft, and insider fraud.</p>
<p><strong>4. Network Analysis:</strong> Graph databases are widely used in network analysis applications, including transportation networks, telecommunications networks, and biological networks. By representing nodes as network elements and connections as edges, graph databases enable advanced analytics such as route optimization, network visualization, and gene pathway analysis.</p>
<p><strong>5. Features involving Hierarchical data:</strong> In addition to the standard use cases mentioned earlier, graph databases offer flexibility for implementing custom business logic involving hierarchical relationships. Whether managing organizational charts, product categorizations, or any other nested data models, graph databases excel at representing and querying hierarchical relationships efficiently. This versatility allows businesses to implement bespoke solutions tailored to their unique requirements, empowering them to derive insights and make informed decisions based on the underlying data relationships.</p>
<h2 id="heading-commonly-used-graph-query-languages">Commonly used graph query languages</h2>
<p>As graph databases gain traction, the number of graph query languages has exploded. This abundance of options can be quite daunting for developers. Navigating this ever-growing landscape can be challenging.</p>
<p>Let’s narrow those and take a quick look at some of the most commonly used graph query languages out there:</p>
<p><strong>Cypher</strong></p>
<p>Cypher is an open-source declarative query language developed by <a target="_blank" href="https://linkurious.com/neo4j/"><strong>Neo4j</strong></a> for querying graph databases. It is part of the openCypher project, an open standard that aims to make Cypher available for use in various graph database systems, which has led to it being one of the most widely adopted query languages. Queries in Cypher are typically straightforward, beginning with the specification of a pattern to match and then permitting further refinements through filtering, aggregation, and other operations.</p>
<p><strong>Gremlin</strong></p>
<p>Developed as part of the Apache TinkerPop graph computing framework, Gremlin is notable for its language agnosticism, meaning it is not bound to a particular graph database system. Instead, Gremlin is compatible with various graph databases, making it a suitable choice for developers who require flexibility and want to work with multiple data sources.</p>
<p><strong>GraphQL</strong></p>
<p>GraphQL is a query language developed internally by Facebook in 2012 before being publicly released in 2015. It has gained significant popularity as an alternative to REST APIs. Unlike the other graph query languages discussed, GraphQL was designed for clients to query data from APIs and servers, not traditional graph databases. However, it shares similarities in its graph-structured queries.</p>
<p>Apart from these, there are more vendor-specific graph languages like AQL(<a target="_blank" href="https://arangodb.com/"><strong>ArrangoDB</strong></a>), GSQL(<a target="_blank" href="https://www.tigergraph.com/"><strong>TigerGraph</strong></a>), nGQL(<a target="_blank" href="https://www.nebula-graph.io/"><strong>Nebula Graph</strong></a>) and many more.</p>
<h1 id="heading-alternatives-to-graph-databases"><strong>Alternatives to Graph Databases</strong></h1>
<p>Even though graph databases excel at managing interconnected data, their implementation and maintenance can be a time-consuming and expensive endeavor. However, they're not the only game in town.</p>
<p>Several alternative approaches exist, each with its strengths and weaknesses. Let's explore some of these alternatives and how they stack up against graph databases.</p>
<p><strong>1. Recursive Common Table Expressions (CTEs) in Relational Databases:</strong> Relational databases, such as PostgreSQL and SQL Server, offer support for recursive common table expressions (CTEs). This feature allows for recursive queries that can traverse hierarchical or graph-like structures within relational data models. By recursively joining tables with themselves, developers can perform graph traversals and pathfinding operations directly within SQL queries.</p>
<p><strong>2. Caching Solutions like Redis or Memcached:</strong> Another approach to handling graph-like data involves leveraging caching solutions like Redis or Memcached. These in-memory key-value stores excel at storing and retrieving data with low latency, making them ideal for implementing algorithms like breadth-first search (BFS) on graph-like data structures. By caching intermediate results, developers can improve the performance of graph traversal operations, especially in scenarios with high read/write ratios or frequently changing data.</p>
<p><strong>3. Apache Age:</strong> Apache Age is a distributed graph database built on top of PostgreSQL. It leverages the capabilities of PostgreSQL as a relational database while providing native support for graph data structures and operations. With Apache Age, users can store and query graph data using familiar SQL syntax, making it a compelling alternative for organizations already invested in PostgreSQL infrastructure.</p>
<p><strong>4. pgRouting:</strong> pgRouting is an extension for PostgreSQL that adds routing functionality to the database, enabling users to perform complex routing and pathfinding operations on spatial data. While not a full-fledged graph database, pgRouting can be used to solve graph-related problems such as finding the shortest path between two points in a network. It offers a range of routing algorithms and functions, making it a valuable tool for applications requiring geospatial analysis and routing.</p>
<h1 id="heading-trade-offs-and-considerations"><strong>Trade-offs and Considerations</strong></h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712552674997/9dc34415-e2cb-49a8-a298-41b584aaba1b.jpeg?auto=compress,format&amp;format=webp" alt class="image--center mx-auto" /></p>
<p>If you’re in this same dilemma, here are several factors to consider:</p>
<ul>
<li><p><strong>Performance:</strong> Graph databases are optimized for querying and traversing graph-like data structures, offering efficient graph algorithms and indexing mechanisms out of the box. In contrast, recursive CTEs in relational databases may suffer from performance limitations as the size of the dataset grows, while caching solutions may introduce overhead due to cache invalidation and synchronization.</p>
</li>
<li><p><strong>Scalability:</strong> Graph databases are designed to scale horizontally to handle large and highly interconnected datasets. However, they may require specialized infrastructure and tuning to achieve optimal performance at scale. On the other hand, relational databases and caching solutions can also scale horizontally, but may face limitations in handling complex graph traversals efficiently.</p>
</li>
<li><p><strong>Ease of Use:</strong> Graph databases typically offer high-level query languages and intuitive data modeling tools tailored specifically for graph data. This makes them easy to use for developers familiar with graph concepts. In contrast, leveraging recursive CTEs or caching solutions may require more advanced SQL skills or custom code to implement and maintain.</p>
</li>
<li><p><strong>Pricing:</strong> Graph databases often come with pricing models that consider factors such as the volume of data stored, the number of transactions processed, and the level of support provided. While some graph databases offer community editions or open-source options with no upfront costs, others may require subscription-based licensing or usage-based pricing models. In contrast, relational databases and caching solutions may have different pricing structures, such as per-instance fees or pay-as-you-go pricing for cloud-based deployments. It's essential to consider the total cost of ownership, including licensing, infrastructure, and ongoing maintenance, when evaluating the pricing of graph databases and alternatives.</p>
</li>
<li><p><strong>Documentation</strong>: While graph database vendors strive to provide comprehensive documentation, the quality and coverage may vary depending on the specific vendor. In contrast, alternatives such as relational databases often offer consistently well-established documentation that covers a wide range of use cases. The extensive documentation, backed by a large community, makes it easy for users to find resources and support for their projects.</p>
</li>
<li><p><strong>Community Support</strong>: While graph databases have an active &amp; growing community, they may not be as extensive as those surrounding alternatives like relational databases. Relational databases have been around for more years and are more mature, resulting in a larger and more established user base. This broad user base provides robust support, forums, and resources for users, ensuring a wealth of knowledge and assistance is readily available.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In the ever-evolving landscape of data management, the choice between graph databases and alternative approaches is not always straightforward. Each option offers its own set of advantages, trade-offs, and pricing considerations, making it essential to carefully evaluate your specific requirements and constraints before making a decision.</p>
<p>Graph databases shine in scenarios where complex relationships and graph-like data structures are prevalent, offering intuitive query languages, efficient graph traversal algorithms, and scalable infrastructure. However, it's important to note that they may come with significant upfront costs, making them a substantial investment for organizations. Additionally, utilizing graph databases effectively often requires specialized expertise, further adding to the overall cost of implementation and maintenance.</p>
<p>On the other hand, alternatives like recursive CTEs in relational databases and caching solutions like Redis or Memcached provide viable options for certain use cases, offering familiar SQL-based querying capabilities and low-latency data access. However, they may face performance limitations or scalability challenges when dealing with highly interconnected datasets.</p>
<p>When making your decision, consider factors such as performance, scalability, ease of use, and pricing, weighing the trade-offs against your specific application requirements. Whether you opt for the flexibility of graph databases, the familiarity of relational databases, or the speed of caching solutions, remember that the ultimate goal is to empower your organization with the right tools to derive insights and drive innovation from your data.</p>
<p>In the end, the best approach is one that aligns closely with your business objectives and enables you to extract maximum value from your data assets. So, choose wisely, and embark on your data journey with confidence, knowing that you've selected the optimal solution for your needs.</p>
<h1 id="heading-references">References</h1>
<p><a target="_blank" href="https://www.dylanpaulus.com/posts/postgres-is-a-graph-database/"><strong>https://www.dylanpaulus.com/posts/postgres-is-a-graph-database/</strong></a></p>
<p><a target="_blank" href="https://www.linkedin.com/pulse/you-dont-need-graph-database-modeling-graphs-trees-viktor-qvarfordt-efzof"><strong>https://www.linkedin.com/pulse/you-dont-need-graph-database-modeling-graphs-trees-viktor-qvarfordt-efzof</strong></a></p>
<p><a target="_blank" href="https://www.datacamp.com/blog/what-is-a-graph-database"><strong>https://www.datacamp.com/blog/what-is-a-graph-database</strong></a></p>
<p><a target="_blank" href="https://linkurious.com/graph-query-languages/"><strong>https://linkurious.com/graph-query-languages/</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[Using SVG Icons with Pixi.JS]]></title><description><![CDATA[📚 Pre-requisites
1. Basic understanding of PixiJS
PixiJS is an open-source, web-based rendering system that provides blazing-fast performance for graphics-intensive projects.
This blog assumes the user has set up a basic Pixi.js application. If not,...]]></description><link>https://blog.certa.dev/using-svg-with-webgl-pixijs</link><guid isPermaLink="true">https://blog.certa.dev/using-svg-with-webgl-pixijs</guid><category><![CDATA[Pixi]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[icon]]></category><category><![CDATA[SVG]]></category><category><![CDATA[WebGL]]></category><dc:creator><![CDATA[Abir Pal]]></dc:creator><pubDate>Wed, 29 Nov 2023 13:47:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/_zKxPsGOGKg/upload/991068d44e91d76025c641ad13bb8d3a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-pre-requisites">📚 Pre-requisites</h2>
<p><strong>1. Basic understanding of PixiJS</strong></p>
<p><a target="_blank" href="https://pixijs.com/">PixiJS</a> is an open-source, web-based rendering system that provides blazing-fast performance for graphics-intensive projects.</p>
<p>This blog assumes the user has set up a basic Pixi.js application. If not, it's highly recommended to go through the <a target="_blank" href="https://pixijs.com/tutorial#1">official Pixi.js tutorials</a></p>
<p><strong>2. SVG</strong></p>
<p><strong>SVG</strong> is an XML-based vector image format for defining two-dimensional graphics, having support for interactivity and animation.</p>
<ul>
<li><p><strong>Basic understanding of</strong> <code>&lt;symbol&gt;</code> <strong>and</strong> <code>&lt;use&gt;</code> <strong>element</strong></p>
<p>  The <code>&lt;symbol&gt;</code> element is used to define <mark>graphical template objects</mark> which can be instantiated by a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use"><code>&lt;use&gt;</code></a> element. Refer to <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol">MDN</a> for reference.</p>
</li>
</ul>
<p><strong>3. How to use spritesheet</strong></p>
<p>In layman's terms, <a target="_blank" href="https://en.wikipedia.org/wiki/Texture_atlas">spritesheet</a> is a collection of small images, like icons in this case. Instead of keeping each picture separately, you tile them on a single large document. To view an image from this spritesheet, you just need the position of the image.</p>
<h2 id="heading-the-challenge">⚔️ The Challenge</h2>
<p>If you directly use SVG icons within Pixi JS, they get "Rasterized" (meaning they lose their vector data). Which means if you scale them, they get blurred out. To avoid this, we can:</p>
<ol>
<li><p>Either convert them into native Pixi Objects using something like <a target="_blank" href="https://github.com/bigtimebuddy/pixi-svg">pixi-svg</a> library.</p>
<ul>
<li>The problem with this approach is that these conversions don't translate 1:1 to original SVG due to lack of certain features. So this option can be used if your SVG is not using any of the unsupported features listed by the library.</li>
</ul>
</li>
<li><p>Or we can set the SVG scale beforehand so we can rasterize them at higher resolution, before they get converted to textures.</p>
</li>
</ol>
<p>For the scope of this blog post, we will be implementing the solution based on the second approach, while making sure that we have:</p>
<ul>
<li><p><strong>Efficient Icon Rendering Performance</strong> - Icons are loaded quickly and rendered without any hassle.</p>
</li>
<li><p><strong>Smoother Icon Management</strong> - Adding new icons should not require significant extra effort</p>
</li>
</ul>
<h2 id="heading-the-action-plan">🔖 The Action Plan</h2>
<p>Let's get started, with our action plan.</p>
<ol>
<li><p><strong>NodeJS Script to generate Spritesheet</strong></p>
<ol>
<li><p>The first step is to create a spritesheet from available SVG icons in our workspace. The advantage of this tooling is rich DX and a better way to manage SVG icons.</p>
</li>
<li><p>Assumption is that we already have a set of SVG icons with us</p>
</li>
<li><p>The script will read these icons and compile them into a single <code>spritesheet.json</code> file, which we'll use within our code.</p>
</li>
<li><p>The script itself will be responsible for:</p>
<ol>
<li><p>loading SVG icons as strings</p>
</li>
<li><p>Wrapping <code>&lt;symbol&gt;</code> around them so they become "reusable".</p>
</li>
<li><p>Finally, using them by <code>&lt;use&gt;</code> and laying them horizontally. (with the assumption that every icon is of the same size and aspect ratio)</p>
</li>
<li><p>Following will be format for <code>spritesheet.json</code></p>
<pre><code class="lang-javascript">   <span class="hljs-comment">// Spritesheet.json</span>
   {
       <span class="hljs-string">"spriteSheetSVGData"</span>: <span class="hljs-string">"..."</span> <span class="hljs-comment">// Entire SVG as string</span>
       <span class="hljs-string">"iconSequence"</span>:[] <span class="hljs-comment">// sequence of icons in the spritesheet.</span>
   }
</code></pre>
</li>
</ol>
</li>
</ol>
</li>
<li><p><strong>Using SVGs in Pixi.JS</strong></p>
<ol>
<li><p>Now coming to our main logic, we will use the generated spritesheet JSON to:</p>
</li>
<li><p>Create texture from the spritesheet</p>
</li>
<li><p>Create a function to crop the spritesheet based on the provided <code>iconName</code> and return the resultant sprite.</p>
</li>
<li><p>Render the icon in a PixiJS Stage.</p>
</li>
</ol>
</li>
</ol>
<h2 id="heading-code-walkthroughs">🚀 Code Walkthroughs</h2>
<p><strong>🧑🏽‍💻 Spritesheet generation</strong></p>
<ul>
<li><p><strong>spritesheet.json</strong> consists of the following properties</p>
<ul>
<li><p><code>spritesheetSVGData</code><strong>:</strong> stringified SVG Data of the spritesheet.</p>
</li>
<li><p><code>iconSequence</code> : An ordered array of distinct <code>svg-ids</code> used in the spritesheet. This is a critical attribute, whose index will be used to fetch an Icon, which is going to be used in Pixi.js app.</p>
</li>
</ul>
</li>
</ul>
<pre><code class="lang-javascript">      <span class="hljs-comment">/**
       * Note:
       * The script runs in node-js environment.
       */</span>

      <span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;
      <span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;

      <span class="hljs-comment">/**
       * Converts string to PascalCase
       */</span>

      <span class="hljs-keyword">const</span> toPascalCase = <span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${str}</span>`</span>
          .replace(<span class="hljs-regexp">/[-_]+/g</span>, <span class="hljs-string">" "</span>)
          .replace(<span class="hljs-regexp">/[^\w\s]/g</span>, <span class="hljs-string">""</span>)
          .replace(
            <span class="hljs-regexp">/\s+(.)(\w*)/g</span>,
            <span class="hljs-function">(<span class="hljs-params">$<span class="hljs-number">1</span>, $<span class="hljs-number">2</span>, $<span class="hljs-number">3</span></span>) =&gt;</span> <span class="hljs-string">`<span class="hljs-subst">${$<span class="hljs-number">2.</span>toUpperCase() + $<span class="hljs-number">3.</span>toLowerCase()}</span>`</span>,
          )
          .replace(<span class="hljs-regexp">/\w/</span>, <span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.toUpperCase());
      };

      <span class="hljs-comment">// Directory where spritesheet.json will be generated</span>
      <span class="hljs-keyword">const</span> spriteSheetDirectoryPath = <span class="hljs-string">""</span>;

      <span class="hljs-comment">// spritesheet object, to be loaded in JSON.</span>
      <span class="hljs-keyword">const</span> spriteSheet = {
        <span class="hljs-attr">spriteSheetSVGData</span>: <span class="hljs-string">""</span>,
        <span class="hljs-attr">iconSequence</span>: [],
      };

      <span class="hljs-comment">// Utility function to replace SVG with &lt;symbol&gt;</span>
      <span class="hljs-keyword">const</span> replaceSvgWithSymbol = <span class="hljs-function">(<span class="hljs-params">svg, id</span>) =&gt;</span> {
        <span class="hljs-comment">// Replace &lt;svg&gt; opening tag with &lt;symbol&gt; opening tag</span>
        <span class="hljs-keyword">let</span> modifiedSvg = svg.replace(<span class="hljs-regexp">/&lt;svg([^&gt;]*)&gt;/</span>, <span class="hljs-string">`&lt;symbol$1 id="<span class="hljs-subst">${id}</span>"&gt;`</span>);
        <span class="hljs-comment">// Replace &lt;/svg&gt; closing tag with &lt;/symbol&gt; closing tag</span>
        modifiedSvg = modifiedSvg.replace(<span class="hljs-string">"&lt;/svg&gt;"</span>, <span class="hljs-string">"&lt;/symbol&gt;"</span>);
        <span class="hljs-comment">// Remove xmlns property from &lt;symbol&gt;</span>
        modifiedSvg = modifiedSvg.replace(<span class="hljs-string">' xmlns="http://www.w3.org/2000/svg"'</span>, <span class="hljs-string">""</span>);

        <span class="hljs-keyword">return</span> modifiedSvg;
      };

      <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createSpriteSheet = <span class="hljs-function">(<span class="hljs-params">svgList</span>) =&gt;</span> {
        <span class="hljs-comment">/* 
           Used to store &lt;symbol&gt;(s). 
           Every root &lt;symbol&gt; represents a svg icon.
           For example:
           &lt;symbol id="iconName"&gt;...&lt;/symbol&gt;
        */</span>
        <span class="hljs-keyword">let</span> symbols = <span class="hljs-string">""</span>;

        <span class="hljs-comment">// conversion of SVG to &lt;symbol&gt;, and</span>
        <span class="hljs-comment">// updating IconSequence in spritesheet</span>
        svgList.forEach(<span class="hljs-function">(<span class="hljs-params">svg</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> svgCode = svg.data;
          <span class="hljs-keyword">const</span> iconName = toPascalCase(svg.name);
          <span class="hljs-keyword">const</span> symbolCode = replaceSvgWithSymbol(svgCode, iconName);
          symbols += symbolCode;
          spriteSheet.iconSequence.push(iconName);
        });

        <span class="hljs-comment">/* 
           Create SpriteSheet SVG Data
           Our icons are of size: 16 X 16

           A linear spritesheet having a
           height: 16 units (based on icon height).
           width: 16 * total icons
           gap between icons: 8px

          The data part of spritesheet svg
          We use entity reference method, to put images in the spritesheet.
          &lt;use href="symbol-id" y=0 x={index * 24}/&gt;

          Finally write the spriteSheet to the respective JSON file

        */</span>

        spriteSheet.spriteSheetSVGData = <span class="hljs-string">`&lt;svg xmlns="http://www.w3.org/2000/svg" 
      height="16" width="<span class="hljs-subst">${<span class="hljs-number">16</span> * svgList.length}</span>" viewbox="0 0 <span class="hljs-subst">${
          <span class="hljs-number">16</span> * svgList.length
        }</span> 16"&gt;<span class="hljs-subst">${symbols}</span><span class="hljs-subst">${spriteSheet.iconSequence
          .map((symbolID, index) =&gt; {
            <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;use href="#<span class="hljs-subst">${symbolID}</span>" x="<span class="hljs-subst">${index * <span class="hljs-number">24</span>}</span>" y="0"/&gt;`</span>;
          }</span>)
          .join(" ")}&lt;/svg&gt;`</span>;

        <span class="hljs-comment">// Create Spritesheet.json</span>
        fs.writeFileSync(
          path.resolve(spriteSheetDirectoryPath, <span class="hljs-string">"spritesheet.json"</span>),
          <span class="hljs-built_in">JSON</span>.stringify(spriteSheet),
        );
        <span class="hljs-comment">// Create Spritesheet.svg</span>
        fs.writeFileSync(
          path.resolve(spriteSheetDirectoryPath, <span class="hljs-string">"spritesheet.svg"</span>),
          <span class="hljs-built_in">JSON</span>.stringify(spriteSheet.spriteSheetSVGData),
        );
      };
</code></pre>
<p><strong>⚙️ Using SVGs in Pixi.JS</strong></p>
<ol>
<li><p><strong>Assumptions</strong></p>
<ul>
<li><p><code>spritesheet.svg</code> is generated from the above script.</p>
</li>
<li><p><code>iconSequence</code> is extracted from the <code>spriteSheet.json</code></p>
</li>
</ul>
</li>
<li><p><strong>Example App Structure</strong></p>
<ol>
<li><p>We will be using Vanilla Javascript as an example for simplicity and easier for understanding</p>
<pre><code class="lang-plaintext">    app/
     ├─ index.html
     │─ js/
     │  ├─ app.js
     ├─ static/
     │  ├─spritesheet.json
</code></pre>
</li>
</ol>
</li>
<li><p>As we are using vanilla javascript. We will be using the native DOM to load the <code>Pixi.JS</code>. Henceforth, here is what <code>index.html</code> looks like.</p>
<ul>
<li><strong>Note</strong>: The script, <code>app.js</code> is loaded as a module. This is intentionally done to prevent scope ambiguity issues with global context. Henceforth, we have to explicitly mention what variables we want to add to the global context. For example: <code>globalThis.__PIXI_APP__ = app;</code></li>
</ul>
</li>
</ol>
<pre><code class="lang-xml">    <span class="hljs-comment">&lt;!--index.html--&gt;</span>
    <span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"X-UA-Compatible"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"ie=edge"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>PixiJS and Icons Integration<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/pixi.js@6.x/dist/browser/pixi.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"js/app.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<ol>
<li><p><code>app.js</code> performs the following operations.</p>
<ol>
<li><p>Initialize PixiJS</p>
</li>
<li><p>Load SVG Resource for spritesheet</p>
</li>
<li><p>Create texture from the SVG resource.</p>
</li>
<li><p>A function that takes input as icon name, and returns the relevant icon sprite.</p>
</li>
<li><p>Create a sprite from the texture.</p>
</li>
<li><p>Crop the sprite generated, based on the required index found in <code>iconSequence</code></p>
</li>
<li><p>If no such icon exists, return <code>undefined</code>.</p>
<pre><code class="lang-javascript"> <span class="hljs-comment">// App.js</span>
 <span class="hljs-comment">// File where our svg is stored.</span>
 <span class="hljs-keyword">const</span> SVG_URL = <span class="hljs-string">"/images/spriteSheet.svg"</span>;
 <span class="hljs-keyword">const</span> ICON_SEQUENCE = [<span class="hljs-string">"House"</span>, <span class="hljs-string">"Tasks"</span>, <span class="hljs-string">"Chart"</span>];

 <span class="hljs-comment">// Initializing the PIXIJS</span>
 <span class="hljs-keyword">let</span> app = <span class="hljs-keyword">new</span> PIXI.Application({
   <span class="hljs-attr">width</span>: <span class="hljs-number">400</span>,
   <span class="hljs-attr">height</span>: <span class="hljs-number">300</span>,
   <span class="hljs-attr">resolution</span>: <span class="hljs-built_in">window</span>.devicePixelRatio,
   <span class="hljs-attr">antialias</span>: <span class="hljs-literal">true</span>,
   <span class="hljs-attr">backgroundColor</span>: <span class="hljs-number">0xeeeeee</span>,
   <span class="hljs-attr">autoDensity</span>: <span class="hljs-literal">true</span>
 });
 globalThis.__PIXI_APP__ = app;
 <span class="hljs-built_in">document</span>.body.appendChild(app.view);

 <span class="hljs-comment">// Created SVG Resource from the SVG</span>
 <span class="hljs-keyword">const</span> svgRes = <span class="hljs-keyword">new</span> PIXI.SVGResource(SVG_URL, { <span class="hljs-attr">scale</span>: <span class="hljs-number">3</span> });

 <span class="hljs-comment">// Created Texture from the SVG Resource</span>
 <span class="hljs-keyword">const</span> texture = PIXI.Texture.from(svgRes);

 <span class="hljs-comment">/* Returns the relevant Icon Sprite
  * @param iconName: string
  * @param options: {x:number, y:number, height:number, width:number}
  */</span>
 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getIcon</span>(<span class="hljs-params">iconName, options</span>) </span>{
   <span class="hljs-keyword">const</span> sprite = <span class="hljs-keyword">new</span> PIXI.Sprite(texture);

   <span class="hljs-keyword">const</span> iconIndex = ICON_SEQUENCE.findIndex(
                     <span class="hljs-function">(<span class="hljs-params">iName</span>) =&gt;</span> iName === iconName);

   <span class="hljs-keyword">if</span> (iconIndex &lt; <span class="hljs-number">0</span>) {
     <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"No icon found with name:"</span>, iconName);
     <span class="hljs-keyword">return</span> <span class="hljs-literal">undefined</span>;
   }

   texture.on(<span class="hljs-string">"update"</span>, <span class="hljs-function">() =&gt;</span> {
     <span class="hljs-keyword">if</span> (texture.valid) {
       texture.frame = <span class="hljs-keyword">new</span> PIXI.Rectangle(<span class="hljs-number">70</span> * iconIndex, <span class="hljs-number">0</span>, <span class="hljs-number">50</span>, <span class="hljs-number">48</span>);
       sprite.width = options?.width || <span class="hljs-number">16</span>;
       sprite.height = options?.height || <span class="hljs-number">16</span>;
       sprite.x = options?.x || <span class="hljs-number">175</span>;
       sprite.y = options?.y || <span class="hljs-number">125</span>;
     }
   });

   <span class="hljs-keyword">return</span> sprite;
 }
 <span class="hljs-keyword">const</span> iconSprite = getIcon(<span class="hljs-string">"Tasks"</span>, { <span class="hljs-attr">height</span>: <span class="hljs-number">30</span>, <span class="hljs-attr">width</span>: <span class="hljs-number">30</span>, <span class="hljs-attr">x</span>: <span class="hljs-number">25</span>, <span class="hljs-attr">y</span>: <span class="hljs-number">25</span> });
 app.stage.addChild(iconSprite);
</code></pre>
</li>
</ol>
</li>
</ol>
<h1 id="heading-codesandbox">👨🏼‍💻 CodeSandbox</h1>
<p>Here's a live example of our above implementation.</p>
<ul>
<li><div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/s/icons-integration-with-pixi-js-t55fdn?file=/js/app.js">https://codesandbox.io/s/icons-integration-with-pixi-js-t55fdn?file=/js/app.js</a></div>
 </li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>So let's look at the aspects we discussed before starting the blog.</p>
<ol>
<li><p>Icon Rendering Performance</p>
<ul>
<li>Now, we do not calculate textures for every icon. Instead, we calculate it once for the spritesheet, and then we crop the spritesheet based on the icon we need.</li>
</ul>
</li>
<li><p>Icon Management</p>
<ul>
<li><p>Spritesheets can be now easily generated and updated with new icons from our new script.</p>
</li>
<li><p>And every generated spritesheet, can be versioned via git.</p>
</li>
<li><p>In the end, the whole spritesheet is just a big SVG file, and we need not worry about having multiple files for multiple SVG icons, providing a smoother icon management experience, without any overhead of fetching individual resources.</p>
</li>
</ul>
</li>
</ol>
<p>I hope you found the blog helpful and got to learn something new today. Plus give a read to our detailed blog on building a layout engine in Pixi.JS <a target="_blank" href="https://blog.certa.dev/building-a-layout-engine-for-webgl">here</a></p>
<h1 id="heading-we-are-hiring">We are hiring! 🎉</h1>
<p>Solving challenging problems at scale in a fully remote team interests you, head to our <a target="_blank" href="https://www.getcerta.com/careers"><strong>careers page</strong></a> and apply for the position of your liking!</p>
]]></content:encoded></item><item><title><![CDATA[Third-Party Cookie Restrictions for Iframes in Safari]]></title><description><![CDATA[What do you need to know before?

What is an iframe?

What are cookies?

What is a third-party cookie?


Introduction
Have you ever wondered why some websites ask for storage permissions or why some features don't work in certain browsers? Let's dive...]]></description><link>https://blog.certa.dev/third-party-cookie-restrictions-for-iframes-in-safari</link><guid isPermaLink="true">https://blog.certa.dev/third-party-cookie-restrictions-for-iframes-in-safari</guid><category><![CDATA[iframe]]></category><category><![CDATA[WebKit]]></category><category><![CDATA[safari]]></category><category><![CDATA[Third Party Cookies]]></category><category><![CDATA[Intelligent Tracking Prevention]]></category><dc:creator><![CDATA[Malav Shah]]></dc:creator><pubDate>Tue, 17 Oct 2023 07:12:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/3_Xwxya43hE/upload/884bd60aca3c09bf1ecfd18a28ec335c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-do-you-need-to-know-before">What do you need to know before?</h2>
<ol>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe">What is an <code>iframe</code>?</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">What are cookies?</a></p>
</li>
<li><p><a target="_blank" href="https://www.techtarget.com/whatis/definition/third-party-cookie">What is a third-party cookie?</a></p>
</li>
</ol>
<h2 id="heading-introduction">Introduction</h2>
<p>Have you ever wondered why some websites ask for storage permissions or why some features don't work in certain browsers? Let's dive into the intricacies of third-party cookie restrictions in Safari and how we tackled them!</p>
<p><strong>Safari</strong>, Apple's flagship browser, has always been a pioneer in the quest for user <strong>privacy and security</strong>. In recent years, it has made significant strides in protecting its users from the prying eyes of online trackers by <mark>restricting third-party cookies</mark>.</p>
<p>In the following discussion, we will delve into a specific challenge encountered within this privacy-focused paradigm. This blog aims to unravel the complexities of seeking storage permission in Safari, particularly when our content is loaded within an iframe and relies on third-party cookies.</p>
<p>The predicament arose as we endeavored to integrate our React-based web app into an iframe hosted on a different domain. This intersection of domains posed a unique challenge, prompting us to explore innovative solutions to seamlessly navigate the intricacies of Safari's third-party cookie restrictions.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><a target="_blank" href="https://webkit.org/">WebKit</a> is an open-source web browser engine developed by Apple Inc. It is primarily used as the rendering engine for Apple's Safari web browser. In the next few sections, we have added links to Webkit's website.</div>
</div>

<h2 id="heading-problem">Problem</h2>
<p>Our application uses cookies for authentication. In one of the use cases, it was being rendered inside an iframe. The parent HTML document that was rendering the iframe was being hosted on an entirely different domain.</p>
<p>In other browsers, our application within the iframe was able to access the cookies but not in Safari. Safari uses <a target="_blank" href="https://webkit.org/tracking-prevention/">Intelligent Tracking Prevention(ITP</a>) to control the access of third-party cookies.</p>
<p>ITP aims to prevent third-party cookies, making them inaccessible in iframes unless <strong>certain conditions</strong> are met. These conditions can be found in the <a target="_blank" href="https://webkit.org/blog/11545/updates-to-the-storage-access-api/">Webkit's official announcement</a>.</p>
<h2 id="heading-storage-access-apis">Storage Access APIs</h2>
<p>As per ITP</p>
<blockquote>
<p>"Third-party cookie access can only be granted through the Storage Access API."</p>
</blockquote>
<p>Let's look at parts of this API that concern our problem:</p>
<ol>
<li><p><code>document.hasStorageAccess</code> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess">API Doc</a></p>
<p> This API is used to check cookie storage access. This will return <code>false</code> for third-party cookies in the case of the Safari browser.</p>
</li>
<li><p><code>document.requestStorageAccess</code> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess">API Doc</a></p>
<p> This API is used to ask for third-party storage(cookie) access explicitly from the user.</p>
</li>
</ol>
<p>Both of the above APIs are available in Safari as well as in <a target="_blank" href="https://caniuse.com/?search=storageAccess">other browsers</a>.</p>
<p><a target="_blank" href="https://webkit.org/blog/11545/updates-to-the-storage-access-api/">Webkit's official documentation</a> explains the steps to use these APIs &amp; the rest of the user flow(which is the basis for the following solution). We recommend giving it a read before moving ahead with this post.</p>
<h2 id="heading-solution">Solution</h2>
<p>The solution described below is not the only one but will help you in designing solutions for your use cases. You can also design a solution as per your needs by following the guide mentioned <a target="_blank" href="https://webkit.org/blog/11545/updates-to-the-storage-access-api/">here</a>.</p>
<p>We are using react so the above-mentioned solution is written in the concepts of react.</p>
<ol>
<li><p>We have created separate utility functions in the helper file.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isSafari</span>(<span class="hljs-params"></span>): <span class="hljs-title">boolean</span></span>{
   <span class="hljs-keyword">const</span> userAgent = navigator.userAgent.toLowerCase();
   <span class="hljs-keyword">return</span> (
     userAgent.indexOf(<span class="hljs-string">"safari"</span>) !== <span class="hljs-number">-1</span> &amp;&amp; userAgent.indexOf(<span class="hljs-string">"chrome"</span>) === <span class="hljs-number">-1</span>
   );
 };

 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">supportStorageAccessApi</span>(<span class="hljs-params"></span>): <span class="hljs-title">boolean</span> </span>{
   <span class="hljs-keyword">return</span> <span class="hljs-string">"hasStorageAccess"</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">document</span> &amp;&amp; <span class="hljs-string">"requestStorageAccess"</span> <span class="hljs-keyword">in</span> <span class="hljs-built_in">document</span>;
 }

 <span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hasStorageAccess</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">boolean</span>&gt; </span>{
   <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.hasStorageAccess();
 }

 <span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">requestStorageAccess</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
   <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.requestStorageAccess();
 }

 <span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">requiresStoragePermissions</span>(<span class="hljs-params"></span>): <span class="hljs-title">boolean</span> </span>{
   <span class="hljs-keyword">return</span> isSafari() &amp;&amp; supportStorageAccessApi();
 }
</code></pre>
<p> The above code is to make the browser API abstract from the actual implementation. These functions are what we are going to call.</p>
</li>
<li><p>Then after, we created a new file named <code>useStoragePermissions.tsx</code> and added the below-mentioned code.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useStoragePermissions = (): {
   needPermission: <span class="hljs-built_in">boolean</span>;
   askForPermission: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
   haveCheckedPermission: <span class="hljs-built_in">boolean</span>;
 } =&gt; {
   <span class="hljs-keyword">const</span> [needPermission, setNeedPermission] = React.useState(
     requiresStoragePermissions() ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>
   );
   <span class="hljs-keyword">const</span> [haveCheckedPermission, setHaveCheckedPermission] =
     React.useState(<span class="hljs-literal">false</span>);

   <span class="hljs-keyword">const</span> isHavingPermissionFn = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
     <span class="hljs-keyword">try</span> {
       <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> hasStorageAccess();
     } <span class="hljs-keyword">catch</span> (e: <span class="hljs-built_in">any</span>) {
       <span class="hljs-comment">// Handle error gracefully and show user some message</span>
       <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
     }
   }, []);

   <span class="hljs-keyword">const</span> checkPermission = useCallback(<span class="hljs-function">() =&gt;</span> {
     isHavingPermissionFn().then(<span class="hljs-function">(<span class="hljs-params">isHavingPerm: <span class="hljs-built_in">boolean</span></span>) =&gt;</span> {
       setNeedPermission(!isHavingPerm);
       setHaveCheckedPermission(<span class="hljs-literal">true</span>);
     });
   }, [isHavingPermissionFn]);

   <span class="hljs-keyword">const</span> askForPermission = useCallback(<span class="hljs-keyword">async</span> () =&gt; {
     <span class="hljs-keyword">try</span> {
       <span class="hljs-keyword">await</span> requestStorageAccess();
       checkPermission();
     } <span class="hljs-keyword">catch</span> (e: <span class="hljs-built_in">any</span>) {
       <span class="hljs-comment">// Handle error gracefully and show user some message</span>
     }
   }, [checkPermission]);

   React.useEffect(<span class="hljs-function">() =&gt;</span> {
     <span class="hljs-keyword">if</span> (requiresStoragePermissions()) {
       checkPermission();
     }
   }, [checkPermission]);

   <span class="hljs-keyword">return</span> {
     needPermission,
     askForPermission: requiresStoragePermissions()
       ? askForPermission
       : <span class="hljs-function">() =&gt;</span> {},
     haveCheckedPermission
   };
 };
</code></pre>
<p> Using the above hook we have exposed below three states:</p>
<ol>
<li><p><code>needPermission</code>: This will be true when the browser is Safari and it has support for <code>hasStorageAccess</code> and <code>requestStroageAccess</code></p>
<p> <strong>Hint:</strong> Use this boolean while consuming this hook to decide when to call <code>askForPermission</code> and <code>haveCheckedPermission</code> .</p>
</li>
<li><p><code>askForPermission</code>: This is the function that the consumer could call to request the user to give storage access permission</p>
</li>
<li><p><code>haveCheckedPermission</code> : This is a boolean which will be true after calling <code>askForPermission</code> in the case of <code>needPermission</code> is true initially.</p>
</li>
</ol>
</li>
<li><p>To consume the above hook, we have followed the below-mentioned steps:</p>
<ol>
<li><p>We have mounted and created a hook in #2 at the initialization part of the app. Use the <code>needPermission</code> state from it and proceed ahead as normal when <code>needPermission</code> is <code>false</code>.</p>
</li>
<li><p>We created some other routes <code>user-access-flow</code> that show a button with some text like <code>Set Cookie</code> . And <code>onClick</code> of it, we set the cookie.</p>
<ol>
<li><p>This is where we are actually calling authentication-related APIs and then the server is setting up cookies.</p>
</li>
<li><p>Once this cookie is set, close this tab using <code>window.close()</code></p>
</li>
</ol>
</li>
<li><p>Now when, <code>needPermission</code> is <code>true</code></p>
<ol>
<li><p>We show the user some text like, "<strong>Click here and click on the Set-Cookie button on the newly opened tab</strong>" and on click of it, redirect the user to the route created in above step 2.</p>
</li>
<li><p>Now, when a user comes back from that route, call <code>askForPermission</code> on click of some button. Which should ask the user to give storage permission.</p>
<p> Ex:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695902863506/ffa22f10-6ffa-493a-8803-68769a1bd8e1.png" alt="Storage access permission ask popup in safari" /></p>
</li>
<li><p>Once the user clicks on <code>Allow</code> the button, your website is authorized to store and access third-party cookies and now you can continue with business logic.</p>
</li>
<li><p>We have also kept this thing in mind that this consent will be revoked if the user cleans up the browser history and does not visit that domain for 7 days. These constraints are already mentioned <a target="_blank" href="https://webkit.org/blog/9521/intelligent-tracking-prevention-2-3/">here</a></p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you have faced such issues while developing or browsing such issues, please share those in the comments. We will be more than happy to read and comment more on those.</p>
<h2 id="heading-we-are-hiring">We are hiring!</h2>
<p>If solving challenging problems at scale in a fully remote team interests you, head to our <a target="_blank" href="https://www.getcerta.com/careers"><strong>careers page</strong></a> and apply for the position of your liking!</p>
]]></content:encoded></item><item><title><![CDATA[Simplifying WebGL: Building an Effective Layout Engine]]></title><description><![CDATA[As front-end engineers, we often don't have to think about everything that the browser is doing behind the scenes to make our lives easier.

You put two div together, they get stacked automatically.

They grow in size as you add content to them.

You...]]></description><link>https://blog.certa.dev/building-a-layout-engine-for-webgl</link><guid isPermaLink="true">https://blog.certa.dev/building-a-layout-engine-for-webgl</guid><category><![CDATA[WebGL]]></category><category><![CDATA[Layout engine ]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[yoga-layout]]></category><dc:creator><![CDATA[Abhinav Dabral]]></dc:creator><pubDate>Wed, 09 Aug 2023 08:30:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/geNNFqfvw48/upload/35f6652e39e63d2e6bb1562565b7c15f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As front-end engineers, we often don't have to think about everything that the browser is doing behind the scenes to make our lives easier.</p>
<ul>
<li><p>You put two <code>div</code> together, they get stacked automatically.</p>
</li>
<li><p>They grow in size as you add content to them.</p>
</li>
<li><p>You can style them, change alignments and whatnot.</p>
</li>
</ul>
<p>The logic that's handling all this is your browser's layout engine (along with a lot of other things).</p>
<p>But when we're using WebGL, everything has to be individually positioned and their dimensions have to be predefined as well. So, if we want to get an HTML-like experience within WebGL, that is, relative positioning between objects; we require a layout engine. A layout engine will help in dynamically allocating and calculating the position and dimensions of relatively positioned objects within WebGL.</p>
<h3 id="heading-what-is-a-layout-engine">What is a Layout Engine?</h3>
<p>In this context, we're referring to a piece of logic that is responsible for handling the logistics of where something should be positioned, relative to the position of others.</p>
<p>Even this practical guide is using WebGL as a most common use case but the approach is not specific to WebGL. It can be used in other use cases with similar requirements.</p>
<h3 id="heading-option-1-find-something-that-just-works">Option 1 - Find something that "just works"</h3>
<p>Your best bet is to find something purpose-built for this exact use case. How well it integrates with your specific project, is another thing.</p>
<p>Here are a couple of options:</p>
<ul>
<li><p>Yoga layout - <a target="_blank" href="https://github.com/facebook/yoga">https://github.com/facebook/yoga</a> (C++)</p>
<p>  <em>This is what React Native uses internally. That's how you can write CSS-like styles and they work nearly identically on iOS and Android.</em></p>
</li>
<li><p>Stretch layout - <a target="_blank" href="https://github.com/vislyhq/stretch">https://github.com/vislyhq/stretch</a> (Rust)</p>
</li>
<li><p>Pixi Layout - <a target="_blank" href="https://github.com/pixijs/layout">https://github.com/pixijs/layout</a> (JS)</p>
</li>
<li><p>And a few others (which have not been updated for a while)</p>
<ul>
<li><p><a target="_blank" href="https://github.com/lynaghk/subform-layout">https://github.com/lynaghk/subform-layout</a> (Abandoned, also only minified code is available)</p>
</li>
<li><p><a target="_blank" href="https://github.com/randrew/layout">https://github.com/randrew/layout</a> (C++)</p>
</li>
</ul>
</li>
</ul>
<p>Unfortunately, for our use case (and without getting into details), none of those worked as we expected.</p>
<p>Maybe you're in the same situation as us, or you just want something small and don't want to use these big libraries, either way, I hope you will find something useful in this article.</p>
<h3 id="heading-option-2-learn-how-a-layout-engine-works">Option 2 - Learn how a layout engine works</h3>
<p>There's no point in putting these details here, especially because Matt Brubeck has done a far better job at highlighting all the important bits that someone building a layout engine should know.</p>
<p>Here's the link - <a target="_blank" href="https://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html">https://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html</a></p>
<p>While his approach was more towards building an HTML rendering engine, the core design of the layout engine stays more or less the same.</p>
<hr />
<h1 id="heading-building-a-layout-engine-in-javascript">Building a layout engine in Javascript</h1>
<p>Please note that this is not a one-size-fits-all solution. This is more of a "proof-of-concept" than anything else. Here, we're merely trying to highlight the process that goes into building a very basic layout engine, so that one can learn from it and build their implementation, for their specific use-case. You can make this as simple as needed or as complicated as needed.</p>
<h2 id="heading-1-plan">1. Plan</h2>
<p>Before we start the implementation, let's just go over what we plan to achieve and how we plan to achieve it.</p>
<p>The process from start to finish goes somewhat like this:</p>
<ol>
<li><p><strong>Define</strong> - Have all the nodes with styles in them, arranged in a proper hierarchy. This will be the tree structure and the top-mode node will be called the <code>rootNode</code></p>
</li>
<li><p><strong>Compute</strong> - Start computing layouts from the <code>rootNode</code>, and recursively calculate for the entire tree.</p>
</li>
<li><p><strong>Paint</strong> - Start from the <code>rootNode</code> and recursively go over all children.</p>
</li>
</ol>
<h2 id="heading-2-implementation">2. Implementation</h2>
<p>These are the items we'll be implementing here</p>
<ul>
<li><p><strong>Allocator (class)</strong></p>
<p>  An Allocator is a singleton, responsible for handling all the operations of adding/removing nodes.</p>
</li>
<li><p><strong>Box (class)</strong></p>
<p>  The Box will be a visual element. It'll just use the calculated data from the <code>node</code> to position and shape itself accordingly.</p>
</li>
<li><p><strong>Test environment</strong></p>
<p>  To test everything.</p>
</li>
<li><p><strong>Layout calculator (function)</strong></p>
<p>  A recursive function that will calculate the layout for the given node (and its children) recursively.</p>
</li>
</ul>
<h3 id="heading-21-allocator">2.1 - Allocator</h3>
<h3 id="heading-211-define-the-shape-of-node">2.1.1 - Define the shape of Node</h3>
<p>Nodes are a logical structure that we will use to represent any element during the entire process. Node doesn't need to know about what kind of element it is representing. An element can be whatever you want it to be - Box, Text, anything. But they all still have to be linked to their own individual "node" which will represent them during the layout process.</p>
<p>A node can contain information like:</p>
<ul>
<li><p>Externally supplied style to this element</p>
</li>
<li><p>Layout parameters of the element</p>
</li>
</ul>
<p>For our use case, let's say that the node is defined by the following TypeScript types:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// layoutEngine.ts</span>

<span class="hljs-keyword">type</span> NodeLayout = {
  id: <span class="hljs-built_in">string</span>; <span class="hljs-comment">// unique ID</span>
  width: <span class="hljs-built_in">number</span>; <span class="hljs-comment">// computed</span>
  height: <span class="hljs-built_in">number</span>; <span class="hljs-comment">// computed</span>
  x: <span class="hljs-built_in">number</span>; <span class="hljs-comment">// computed</span>
  y: <span class="hljs-built_in">number</span>; <span class="hljs-comment">// computed</span>
  style: NodeStyle; <span class="hljs-comment">// externally supplied styles</span>
  children: <span class="hljs-built_in">Array</span>&lt;NodeLayout&gt;; <span class="hljs-comment">// in order</span>
  parentNode: NodeLayout | <span class="hljs-literal">null</span>; <span class="hljs-comment">// because root will have `null`</span>
}

<span class="hljs-keyword">type</span> NodeStyle = {
  width?: <span class="hljs-built_in">number</span>; 
  height?: <span class="hljs-built_in">number</span>;
  layoutMode: LayoutMode
}

<span class="hljs-built_in">enum</span> LayoutMode {
  HORIZONTAL = <span class="hljs-string">"horizontal"</span>,
  VERTICAL = <span class="hljs-string">"vertical"</span>
}
</code></pre>
<h3 id="heading-212-create-the-allocator">2.1.2 - Create the Allocator</h3>
<p>An Allocator singleton class will be responsible for managing all the operations of adding nodes to other nodes and forming a tree.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// allocator.ts</span>

<span class="hljs-keyword">class</span> Allocator {
  <span class="hljs-keyword">public</span> rootNode = createNode(); <span class="hljs-comment">// Wait what?</span>
  <span class="hljs-comment">// .. other methods will go here</span>
}
</code></pre>
<p>Oh yes, we also need a function that will help us create a node initializer. Nothing too complicated.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// allocator.ts</span>

<span class="hljs-keyword">const</span> defaultStyle: NodeStyle = {
  layoutMode: LayoutMode.VERTICAL,
  backgroundColor: <span class="hljs-number">0x000000</span>
};

<span class="hljs-comment">/**
 * Creates and returns a NodeLayout object with 
 * default values assigned to it.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createNode = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> newNode: NodeLayout = {
    id: uuid(),
    width: <span class="hljs-number">0</span>,
    height: <span class="hljs-number">0</span>,
    x: <span class="hljs-number">0</span>,
    y: <span class="hljs-number">0</span>,
    style: defaultStyle,
    children: [],
    parentNode: <span class="hljs-literal">null</span>
  };
  <span class="hljs-keyword">return</span> newNode;
};
</code></pre>
<p>Now that, we've taken care of that, let's get back to our singleton.</p>
<p>Now we need a way to:</p>
<ul>
<li><p>Attach new nodes to existing nodes</p>
<pre><code class="lang-typescript">  attachChild(child: NodeLayout, parent?: NodeLayout) {
    <span class="hljs-keyword">const</span> finalParent = parent || <span class="hljs-built_in">this</span>.rootNode;
    finalParent.children.push(child);
    child.parentNode = finalParent;
  }
</code></pre>
</li>
<li><p>Detach nodes from existing nodes</p>
<pre><code class="lang-typescript">  detachChild(child: NodeLayout) {
    child.parentNode.children = child.parentNode.children.filter(
      <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.id !== child.id
    );
    child.parentNode = <span class="hljs-literal">null</span>;
  }
</code></pre>
</li>
<li><p>Move nodes between nodes</p>
<pre><code class="lang-typescript">  moveChild(child: NodeLayout, parent: NodeLayout) {
    <span class="hljs-built_in">this</span>.detachChild(child);
    <span class="hljs-built_in">this</span>.attachChild(child, parent);
  }
</code></pre>
</li>
<li><p>and Destroy the nodes, recursively.</p>
<pre><code class="lang-typescript">  destroy(node: NodeLayout) {
    node.children.forEach(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> <span class="hljs-built_in">this</span>.destroy(child));
    <span class="hljs-built_in">this</span>.detachChild(node);
  }
</code></pre>
</li>
</ul>
<p>That's it. Now the only thing left to do is, actually instantiate it as a singleton and export it.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// allocator.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> defaultAllocator = <span class="hljs-keyword">new</span> Allocator();
</code></pre>
<h3 id="heading-22-box-the-visual-component">2.2 - Box, the visual component</h3>
<p>Again, this is just a POC, so we'll go as simple as possible. For any element, in this case, Box, we'll just have a class that will contain an instance of a NodeLayout, and then the rest of the implementation is only around how the box renders and everything else involved with it.</p>
<p>For the sake of our example, we're going with Pixi JS to create this box. But this can be whatever else it needs to be. The important part is how we consume the data of the <code>node</code> to render the box.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Box.ts</span>

<span class="hljs-keyword">import</span> { createNode } <span class="hljs-keyword">from</span> <span class="hljs-string">"./allocator"</span>;
<span class="hljs-keyword">import</span> { NodeLayout, NodeStyle } <span class="hljs-keyword">from</span> <span class="hljs-string">"./layoutEngine"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> PIXI <span class="hljs-keyword">from</span> <span class="hljs-string">"pixi.js"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Box {
  node: NodeLayout = createNode();
  container: PIXI.Graphics = <span class="hljs-keyword">new</span> PIXI.Graphics();

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">style?: Partial&lt;NodeStyle&gt;</span>) {
    <span class="hljs-built_in">this</span>.node.style = { ...this.node.style, ...style };
  }

  render() {
    <span class="hljs-built_in">this</span>.container.clear();
    <span class="hljs-built_in">this</span>.container.beginFill(<span class="hljs-built_in">this</span>.node.style.backgroundColor);

    <span class="hljs-built_in">this</span>.container.lineStyle(<span class="hljs-number">1</span>, <span class="hljs-number">0x000000</span>);
    <span class="hljs-built_in">this</span>.container.drawRect(
      <span class="hljs-built_in">this</span>.node.x,
      <span class="hljs-built_in">this</span>.node.y,
      <span class="hljs-built_in">Number</span>(<span class="hljs-built_in">this</span>.node.width),
      <span class="hljs-built_in">Number</span>(<span class="hljs-built_in">this</span>.node.height)
    );

    <span class="hljs-built_in">this</span>.container.endFill();
  }
}
</code></pre>
<h3 id="heading-23-sandbox">2.3 - Sandbox</h3>
<p>Just to check if everything is in order so far, we'll just create a sandbox environment to test things out. We'll be using Pixi JS here.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app.ts or index.ts</span>

<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> PIXI <span class="hljs-keyword">from</span> <span class="hljs-string">"pixi.js"</span>;
<span class="hljs-keyword">import</span> { defaultAllocator } <span class="hljs-keyword">from</span> <span class="hljs-string">"./allocator"</span>;
<span class="hljs-keyword">import</span> { Box } <span class="hljs-keyword">from</span> <span class="hljs-string">"./Box"</span>;
<span class="hljs-keyword">import</span> { calculateLayout, LayoutMode } <span class="hljs-keyword">from</span> <span class="hljs-string">"./layoutEngine"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initApp</span>(<span class="hljs-params">container: HTMLElement</span>) </span>{
  <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> PIXI.Application({
    resizeTo: container
  });

  container.appendChild((app.view <span class="hljs-keyword">as</span> unknown) <span class="hljs-keyword">as</span> HTMLCanvasElement);

  <span class="hljs-comment">//// Sample code Start ////</span>

  <span class="hljs-comment">// initialize a Box with Red background</span>
  <span class="hljs-keyword">const</span> b1 = <span class="hljs-keyword">new</span> Box({
    backgroundColor: <span class="hljs-number">0xff0000</span>
  });

  <span class="hljs-comment">// we need to add the PIXI component to the main stage</span>
  app.stage.addChild(b1.container);

  <span class="hljs-comment">// And let's just assign some values to node</span>
  <span class="hljs-comment">// these values will actually be computed automatically</span>
  <span class="hljs-comment">// when we have calculateLayout in place</span>
  b1.node = {
    ...b1.node,
    x: <span class="hljs-number">20</span>,
    y: <span class="hljs-number">20</span>,
    width: <span class="hljs-number">100</span>,
    height: <span class="hljs-number">100</span>
  };

  <span class="hljs-comment">// finally call the render method to pain the box</span>
  b1.render();
  <span class="hljs-comment">//// Sample code End ////</span>

}

<span class="hljs-keyword">const</span> htmlContainer = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"app"</span>);
<span class="hljs-keyword">if</span> (htmlContainer) {
  initApp(htmlContainer);
}
</code></pre>
<p>At this stage, you should get something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689880306443/85bcaf82-533d-4995-b3cb-eeaf3e25ecd0.png" alt class="image--center mx-auto" /></p>
<p>Yay, a red rectangle on the screen.</p>
<p>The setup is all complete and only the last piece remains, which is what binds this whole thing together (and the whole point of this article). It was necessary to have everything else in place because layout computation is something that you'd likely want to experiment around and experimentation is fun when you can visually see the change happening as you do it.</p>
<h3 id="heading-24-layout-computation">2.4 - Layout computation</h3>
<p>When we talk about layout engines, it mostly comes down to calculating the <code>x, y, width and height</code> of a node. It sounds simple enough but the key is doing calculations in a particular sequence to get it right.</p>
<p><strong>Layout calculation process and implementation</strong></p>
<ol>
<li><p>Computation starts at the given node (<code>rootNode</code> for the very first iteration)</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// layoutEngine.ts</span>

 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateLayout</span>(<span class="hljs-params">node: NodeLayout</span>) </span>{
   <span class="hljs-keyword">const</span> parent = node.parentNode;
   <span class="hljs-comment">// what now?</span>
 }
</code></pre>
</li>
<li><p>Calculate <code>width</code>, <code>x</code> and <code>y</code> of this node</p>
<ol>
<li><p>Let's check if the parent is not there. This means this is likely the root node. so we just define some variables as defaults for the root node.</p>
<pre><code class="lang-typescript">   ...
   <span class="hljs-keyword">if</span> (!parent) {
     <span class="hljs-comment">// this is a root node</span>
     node.width = originLayout.width;
     node.x = originLayout.x;
     node.y = originLayout.y;
   }
</code></pre>
</li>
<li><p>If a parent is present, then we need to check for whether the parent is laying the children Horizontally or Vertically.</p>
<pre><code class="lang-typescript"> ...
 <span class="hljs-keyword">else</span> {
     <span class="hljs-keyword">const</span> currentNodeIndex = parent.children.findIndex(
       <span class="hljs-function">(<span class="hljs-params">n</span>) =&gt;</span> n.id === node.id
     );

     <span class="hljs-comment">// Somethings are dictated by previous sibling,</span>
     <span class="hljs-comment">// we let's have it ready</span>
     <span class="hljs-keyword">const</span> previousSibling =
       currentNodeIndex &gt; <span class="hljs-number">0</span>
         ? parent.children[currentNodeIndex - <span class="hljs-number">1</span>]
         : <span class="hljs-literal">null</span>;

     <span class="hljs-comment">// If the parent is laying out components vertically</span>
     <span class="hljs-keyword">if</span> (parent.style.layoutMode === LayoutMode.VERTICAL) {
       node.width = parent.width; <span class="hljs-comment">// deduct padding/margin here</span>
       node.x = parent.x; <span class="hljs-comment">// add padding/margin here</span>

       <span class="hljs-keyword">const</span> siblingBottom = previousSibling
         ? previousSibling.y + previousSibling.height
         : <span class="hljs-number">0</span>;

       <span class="hljs-keyword">if</span> (siblingBottom &amp;&amp; previousSibling) {
         <span class="hljs-comment">// if sibling starts at 0, ends at 200,</span>
         <span class="hljs-comment">// this starts at 201.</span>
         node.y = siblingBottom + <span class="hljs-number">1</span>; 
         <span class="hljs-comment">// Also consider adding margins here.</span>
       } <span class="hljs-keyword">else</span> {
         node.y = parent.y; <span class="hljs-comment">// add margins here</span>
       }
     }

     <span class="hljs-comment">// If the parent is layout out components horizontally</span>
     <span class="hljs-keyword">else</span> {
       <span class="hljs-keyword">const</span> availableWidth = parent.width; <span class="hljs-comment">// deduct paddings</span>

       <span class="hljs-comment">// we're going lazy here but ideally you can have the</span>
       <span class="hljs-comment">// logic to propotionally divide the available width among</span>
       <span class="hljs-comment">// the children.</span>
       node.width = availableWidth / parent.children.length;

       node.y = parent.y; <span class="hljs-comment">// Addings padding/margins here</span>

       <span class="hljs-keyword">const</span> siblingLeft = previousSibling
         ? previousSibling.x + previousSibling.width
         : <span class="hljs-number">0</span>;

       <span class="hljs-keyword">if</span> (previousSibling &amp;&amp; siblingLeft) {
         node.x = siblingLeft + <span class="hljs-number">1</span>; <span class="hljs-comment">// add margins</span>
       } <span class="hljs-keyword">else</span> {
         node.x = parent.x; <span class="hljs-comment">// add padding/margins here</span>
       }
     }
   }
</code></pre>
</li>
<li><p>Finally, let's just set the width equal to that from the <code>style</code>, if it is configured</p>
<pre><code class="lang-typescript">   <span class="hljs-keyword">if</span> (node.style.width) {
     node.width = node.style.width;
   }
</code></pre>
</li>
</ol>
</li>
<li><p>Start calculating the layout for the children, recursively</p>
<p> <em>(i.e. start from #1 of this process)</em></p>
<pre><code class="lang-typescript"> node.children.forEach(calculateLayout);
</code></pre>
</li>
<li><p>Calculate <code>height</code> of this node<br /> <em>(This is done after computing the layout for the children because height is affected by the children)</em></p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// Case 1: When height is already defined</span>
   <span class="hljs-keyword">if</span> (node.style.height) {
     node.height = node.style.height;
   }
   <span class="hljs-comment">// Case 2: If the current node is laying the children vertically</span>
   <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (node.style.layoutMode === LayoutMode.VERTICAL) {
     <span class="hljs-comment">// then the total height is just the total height of all</span>
     <span class="hljs-comment">// the children (and their margins/paddings)</span>

     <span class="hljs-comment">/**
      * get the maximum height by computing difference between y of
      * first node and the y of last node, and add the height of
      * the last node.
      * Also add the padding of the parent node and margins of
      * first and last child
      */</span>

     <span class="hljs-keyword">if</span> (node.children.length &gt;= <span class="hljs-number">1</span>) {
       <span class="hljs-keyword">const</span> firstChild = node.children[<span class="hljs-number">0</span>];
       <span class="hljs-keyword">const</span> lastChild = node.children[node.children.length - <span class="hljs-number">1</span>];

       node.height = lastChild.y + lastChild.height - firstChild.y;
     } <span class="hljs-keyword">else</span> {
       node.height = <span class="hljs-number">0</span>;
     }
   }
   <span class="hljs-comment">// Case 3: If the current node is laying children horizontally</span>
   <span class="hljs-keyword">else</span> {
     <span class="hljs-comment">// Just find the node that has the largest height</span>
     node.height = <span class="hljs-built_in">Math</span>.max(...node.children.map(<span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.height));
   }
</code></pre>
</li>
<li><p>Done</p>
</li>
</ol>
<h3 id="heading-25-lets-try-it">2.5 - Let's try it</h3>
<p>To test this out, we just need to go back to the sandbox environment and set up a set of Box components.</p>
<p>For this test, we'll do something like this</p>
<pre><code class="lang-plaintext">b1 (default vertical mode)
|- b11 (horizontal mode) - Yellow
|  |- b111 - Red
|  |- b112 - Orange
|  |- b113 - Magenta
|- b12 (vertical mode) - Aqua
|  |- b121 - Green
|  |- b122 - Dark green
|  |- b123 - Dark blue
</code></pre>
<p>To implement this, let's go back to our sandbox, replace all the sample code and implement this as follows:</p>
<ol>
<li><p>Add the box b1</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> b1 = <span class="hljs-keyword">new</span> Box();
</code></pre>
</li>
<li><p>Add 2 boxes b11 and b12</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> b11 = <span class="hljs-keyword">new</span> Box({
     layoutMode: LayoutMode.HORIZONTAL,
     backgroundColor: <span class="hljs-number">0xffff00</span> <span class="hljs-comment">// yellow</span>
   });

   <span class="hljs-keyword">const</span> b12 = <span class="hljs-keyword">new</span> Box({
     backgroundColor: <span class="hljs-number">0x00ffff</span> <span class="hljs-comment">// aqua</span>
   });
</code></pre>
</li>
<li><p>Add 6 boxes (3 for b11 and 3 for b12)</p>
<pre><code class="lang-typescript">
 <span class="hljs-comment">// FOR b11</span>
   <span class="hljs-keyword">const</span> b111 = <span class="hljs-keyword">new</span> Box({
     width: <span class="hljs-number">100</span>,
     height: <span class="hljs-number">100</span>,
     backgroundColor: <span class="hljs-number">0xff0000</span> <span class="hljs-comment">// red</span>
   });
   <span class="hljs-keyword">const</span> b112 = <span class="hljs-keyword">new</span> Box({
     width: <span class="hljs-number">120</span>,
     height: <span class="hljs-number">120</span>,
     backgroundColor: <span class="hljs-number">0xffaa00</span> <span class="hljs-comment">// orange</span>
   });
   <span class="hljs-keyword">const</span> b113 = <span class="hljs-keyword">new</span> Box({
     width: <span class="hljs-number">80</span>,
     height: <span class="hljs-number">80</span>,
     backgroundColor: <span class="hljs-number">0xcc00ff</span> <span class="hljs-comment">// magenta</span>
   });

 <span class="hljs-comment">// FOR b12</span>
   <span class="hljs-keyword">const</span> b121 = <span class="hljs-keyword">new</span> Box({
     height: <span class="hljs-number">100</span>,
     backgroundColor: <span class="hljs-number">0x00ff00</span> <span class="hljs-comment">// green</span>
   });
   <span class="hljs-keyword">const</span> b122 = <span class="hljs-keyword">new</span> Box({
     width: <span class="hljs-number">200</span>,
     height: <span class="hljs-number">120</span>,
     backgroundColor: <span class="hljs-number">0x00cc00</span> <span class="hljs-comment">// dark green</span>
   });
   <span class="hljs-keyword">const</span> b123 = <span class="hljs-keyword">new</span> Box({
     height: <span class="hljs-number">80</span>,
     backgroundColor: <span class="hljs-number">0x0000cc</span> <span class="hljs-comment">// dark blue</span>
   });
</code></pre>
</li>
<li><p>Put everything within the allocator so that it forms a tree</p>
<pre><code class="lang-typescript">   <span class="hljs-comment">// Add b1 to the root</span>
   defaultAllocator.attachChild(b1.node, defaultAllocator.rootNode);

   <span class="hljs-comment">// Add b11 and b12 as b1's children</span>
   defaultAllocator.attachChild(b11.node, b1.node);
   defaultAllocator.attachChild(b12.node, b1.node);

   <span class="hljs-comment">// add 3 children b111, b112, b113 within b11</span>
   defaultAllocator.attachChild(b111.node, b11.node);
   defaultAllocator.attachChild(b112.node, b11.node);
   defaultAllocator.attachChild(b113.node, b11.node);

   <span class="hljs-comment">// add 3 children b121, b122, b123 within b12</span>
   defaultAllocator.attachChild(b121.node, b12.node);
   defaultAllocator.attachChild(b122.node, b12.node);
   defaultAllocator.attachChild(b123.node, b12.node);
</code></pre>
</li>
<li><p>Create an array of all the elements and just attach them to the Pixi Stage.<br /> Still, nothing will be rendered at this point.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> components = [            b1,
                       b11,               b12,
                 b111, b112, b113,  b121, b122, b123
 ];

 components.forEach(<span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> app.stage.addChild(c.container));
</code></pre>
</li>
<li><p>Compute the layout and render the nodes</p>
<pre><code class="lang-typescript"> calculateLayout(defaultAllocator.rootNode);

 components.forEach(<span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
   c.render();
 });
</code></pre>
</li>
<li><p>Done. If everything so far was correctly done, you should see something like this.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689883007945/c7eca76a-db3c-48a6-a269-a45f6ab41e32.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h1 id="heading-wait-its-not-over">Wait ... it's not over</h1>
<p>The article was more aimed at keeping things as simple as possible to understand the basics of a layout engine. But there's no need to stop here. This can be taken as an opportunity to challenge yourself and implement more things on top of this implementation, such as:</p>
<ul>
<li><p>Alignment (both horizontal and vertical)</p>
</li>
<li><p>Paddings and Margins</p>
</li>
<li><p>Flexible widths of children based on proportions</p>
</li>
</ul>
<h3 id="heading-codesandbox">CodeSandbox</h3>
<p>This entire example is also available on CodeSandbox if you'd like to fork it there and experiment with it - <a target="_blank" href="https://codesandbox.io/s/layout-engine-demo-3y3hjf">https://codesandbox.io/s/layout-engine-demo-3y3hjf</a></p>
<h1 id="heading-join-us">Join us</h1>
<p>We're always looking to expand and welcome talented members to our team. And the best part, it's all remote! You can work from wherever you are. Head over to our <a target="_blank" href="https://www.getcerta.com/careers">careers page</a> and apply to any of the available positions that seem right for you.</p>
]]></content:encoded></item><item><title><![CDATA[Build React Forms using JSON]]></title><description><![CDATA[Struggling to manage forms in your project? What if you could generate your forms from a JSON schema?
Every web application makes use of forms at some point as they are vital for information gathering. These forms can get very large and complex depen...]]></description><link>https://blog.certa.dev/build-react-forms-using-json</link><guid isPermaLink="true">https://blog.certa.dev/build-react-forms-using-json</guid><category><![CDATA[React]]></category><category><![CDATA[forms]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Pawan Kolhe]]></dc:creator><pubDate>Mon, 15 May 2023 04:39:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649080318430/wGLnjeWwc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Struggling to manage forms in your project? What if you could generate your forms from a JSON schema?</p>
<p>Every web application makes use of forms at some point as they are vital for information gathering. These forms can get very large and complex depending on your use case. Ideally, we want to be able to create forms in the simplest way possible.</p>
<h2 id="heading-the-problem">🚁 The Problem</h2>
<p>The React ecosystem already has some very popular libraries to manage forms such as React Hook Form, React Final Form, Formik, and many more. All these libraries provide validation, conditional logic, form submission handling, and everything you need to create complex forms. Although, all of them require a fair amount of JSX to be written which becomes verbose and repetitive, especially with long forms and multiple sections. If your forms are large, they often need to be divided into multiple React component files for better maintainability. I think we can all agree that doing so will be very tedious and time-consuming.</p>
<h2 id="heading-the-solution">🚀 The Solution</h2>
<p>Wouldn't it be great to abstract away repetitive and verbose JSX code required to render forms? Just think about it, forms do very standard and predictable tasks. Generally, we need the following:</p>
<ul>
<li><p>Validation - Is a field required or not, Regex, etc? What error message to display if validation fails?</p>
</li>
<li><p>Layout - Whether to place fields horizontally or vertically. Nesting fields under collapse sections.</p>
</li>
<li><p>Conditions/Dependencies - Whether to hide or show a field/section when a certain condition is true. Deriving field A value from field B.</p>
</li>
<li><p>Form state - Providing the initial state of the form. Perform an action on submitting.</p>
</li>
<li><p>Field metadata - Name, label and description of the field. Component to render (TextField, NumberField, Switch, custom component..).</p>
</li>
</ul>
<p>Creating a form should be as simple as defining the structure and properties of the form in JSON format and passing that to a React component that will render the form with all the complex validation, layout, and conditions for us. Data Driven Forms is a React library that enables us to do just that.</p>
<p><a target="_blank" href="https://data-driven-forms.org/">Data Driven Forms</a>, as the name suggests, creates React forms using JSON data. Once we set up the library in our React app, all we need to do is define a JSON definition to create a unique form with its validation, layout, fields, conditions, and structure. It can be thought of as a declarative way to build forms. We don't need to specify how to build the form, but only what the user should see and how it should behave.</p>
<p>Data Driven Forms library internally makes use of <a target="_blank" href="https://github.com/final-form/react-final-form">React Final Form</a> for managing the state of the form state. Although this dependency is likely to be <a target="_blank" href="https://github.com/data-driven-forms/react-forms/issues/935">removed</a> in the next version of the library.</p>
<h2 id="heading-benefits-of-data-driven-approach">⭐️ Benefits of Data Driven approach</h2>
<ul>
<li><p>Build new forms significantly faster</p>
</li>
<li><p>Less source code</p>
</li>
<li><p>More readable and easy to tell what the form is doing</p>
</li>
<li><p>A well-structured and consistent way to create forms</p>
</li>
</ul>
<p>In the future if you choose to adopt a different framework like Svelte, the JSON form definitions can still be used. You would however need to create a component that would be able to render the JSON definition in the new framework as Data Driven Form library only has support for React at the time of writing this blog.</p>
<p>Here is an example of how a JSON definition/<a target="_blank" href="https://data-driven-forms.org/components/renderer#schema">schema</a> is structured.</p>
<pre><code class="lang-json">const exampleForm = {
    fields: [
        {
              name: <span class="hljs-string">"firstName"</span>,
              component: <span class="hljs-string">"text-field"</span>,
              label: <span class="hljs-string">"First Name"</span>,
              validate: ...,
              condition: ...,
        },
        ...
    ]
}
</code></pre>
<h2 id="heading-using-data-driven-forms">🧑🏼‍💻 Using Data Driven Forms</h2>
<p>Here is a <a target="_blank" href="https://codesandbox.io/s/data-driven-forms-d6jur?file=/src/SchemaForm.jsx">CodeSandbox</a> for you to view the source code and run it yourself:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/data-driven-forms-d6jur?expanddevtools=1&amp;fontsize=14&amp;hidenavigation=1&amp;module=%2Fsrc%2FSchemaForm.jsx&amp;theme=dark&amp;view=editor">https://codesandbox.io/embed/data-driven-forms-d6jur?expanddevtools=1&amp;fontsize=14&amp;hidenavigation=1&amp;module=%2Fsrc%2FSchemaForm.jsx&amp;theme=dark&amp;view=editor</a></div>
<p> </p>
<h3 id="heading-install-library">Install library</h3>
<p>To get started, install the required npm package.</p>
<pre><code class="lang-bash">yarn add @data-driven-forms/react-form-renderer
</code></pre>
<p>The <code>@data-driven-forms/react-form-renderer</code> package contains the <strong>FormRenderer</strong> component that is responsible for rendering the form based on the schema it is passed.</p>
<p>We'll also need a <strong>component mapper</strong>. The <code>@data-driven-forms/ant-component-mapper</code> provides a Component Mapper that maps string literals to Ant Design components. The mapper is passed to FormRenderer as a prop.</p>
<pre><code class="lang-bash">yarn add @data-driven-forms/ant-component-mapper antd
</code></pre>
<p>You could install one of the <a target="_blank" href="https://data-driven-forms.org/installation#mappers">other component mapper</a> available too or create your own custom mapper which would allow you to make use of React components already available in your project. At <a target="_blank" href="https://www.getcerta.com/">Certa</a>, we have mapped our design system components to build a custom mapper.</p>
<h3 id="heading-formrenderer">FormRenderer</h3>
<p>The FormRender is a React component that contains all the logic for rendering a React form according to the schema and the configuration provided to it via props.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// SchemaForm.jsx</span>
<span class="hljs-keyword">import</span> FormRenderer <span class="hljs-keyword">from</span> <span class="hljs-string">"@data-driven-forms/react-form-renderer/form-renderer"</span>;
<span class="hljs-keyword">import</span> FormTemplate <span class="hljs-keyword">from</span> <span class="hljs-string">"@data-driven-forms/ant-component-mapper/form-template"</span>;
<span class="hljs-keyword">import</span> componentMapper <span class="hljs-keyword">from</span> <span class="hljs-string">"@data-driven-forms/ant-component-mapper/component-mapper"</span>;
<span class="hljs-keyword">import</span> { schema } <span class="hljs-keyword">from</span> <span class="hljs-string">"./schema"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> SchemaForm = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">FormRenderer</span>
      <span class="hljs-attr">schema</span>=<span class="hljs-string">{schema}</span>
      <span class="hljs-attr">componentMapper</span>=<span class="hljs-string">{componentMapper}</span>
      <span class="hljs-attr">FormTemplate</span>=<span class="hljs-string">{FormTemplate}</span>
      <span class="hljs-attr">initialValues</span>=<span class="hljs-string">{{}}</span>
      <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{(values)</span> =&gt;</span> console.log(values)}
    /&gt;</span>
  );
};
</code></pre>
<p>The <code>schema</code> property takes the form JSON definition.</p>
<p>The <code>FormTemplate</code> property defines a template of the form. This is a component that</p>
<p>The <code>initialValues</code> property allows you to set the initial values of the fields in the form.</p>
<p>All the available props can be viewed <a target="_blank" href="https://data-driven-forms.org/components/renderer#formrenderer">here</a>.</p>
<h2 id="heading-json-definition">📜 JSON Definition</h2>
<p>Here is what a typical form schema might look like.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// schema.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> schema = {
  <span class="hljs-attr">fields</span>: [
    {
      <span class="hljs-attr">component</span>: componentTypes.TEXT_FIELD,
      <span class="hljs-attr">name</span>: <span class="hljs-string">"name"</span>,
      <span class="hljs-attr">label</span>: <span class="hljs-string">"Your name"</span>,
      <span class="hljs-attr">isRequired</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">validate</span>: [{ <span class="hljs-attr">type</span>: validatorTypes.REQUIRED }]
    },
    {
      <span class="hljs-attr">component</span>: componentTypes.TEXT_FIELD,
      <span class="hljs-attr">name</span>: <span class="hljs-string">"email"</span>,
      <span class="hljs-attr">label</span>: <span class="hljs-string">"Email"</span>,
      <span class="hljs-attr">isRequired</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">validate</span>: [
        {
          <span class="hljs-attr">type</span>: validatorTypes.PATTERN,
          <span class="hljs-attr">pattern</span>: <span class="hljs-string">"[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}$"</span>,
          <span class="hljs-attr">message</span>: <span class="hljs-string">"Not valid email"</span>
        }
      ]
    },
    {
      <span class="hljs-attr">component</span>: componentTypes.TEXT_FIELD,
      <span class="hljs-attr">name</span>: <span class="hljs-string">"confirm-email"</span>,
      <span class="hljs-attr">label</span>: <span class="hljs-string">"Confirm email"</span>,
      <span class="hljs-attr">type</span>: <span class="hljs-string">"email"</span>,
      <span class="hljs-attr">isRequired</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">validate</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"same-email"</span> }]
    },
    {
      <span class="hljs-attr">component</span>: componentTypes.TEXT_FIELD,
      <span class="hljs-attr">name</span>: <span class="hljs-string">"address.street"</span>,
      <span class="hljs-attr">label</span>: <span class="hljs-string">"Street"</span>
    },
    {
      <span class="hljs-attr">component</span>: componentTypes.SELECT,
      <span class="hljs-attr">name</span>: <span class="hljs-string">"address.state"</span>,
      <span class="hljs-attr">label</span>: <span class="hljs-string">"State"</span>,
      <span class="hljs-attr">options</span>: [
        { <span class="hljs-attr">label</span>: <span class="hljs-string">"Delhi"</span>, <span class="hljs-attr">value</span>: <span class="hljs-string">"delhi"</span> },
        { <span class="hljs-attr">label</span>: <span class="hljs-string">"Goa"</span>, <span class="hljs-attr">value</span>: <span class="hljs-string">"goa"</span> },
        { <span class="hljs-attr">label</span>: <span class="hljs-string">"Maharashtra"</span>, <span class="hljs-attr">value</span>: <span class="hljs-string">"maharashtra"</span> }
      ]
    },
    {
      <span class="hljs-attr">component</span>: componentTypes.CHECKBOX,
      <span class="hljs-attr">name</span>: <span class="hljs-string">"newsletters"</span>,
      <span class="hljs-attr">label</span>: <span class="hljs-string">"I want to receive newsletter"</span>
    }
  ]
};
</code></pre>
<p>The <code>name</code> property defines the path where the field data needs to be mapped.</p>
<p>The <code>component</code> property defines the component to be used to render the field from the componentMapper.</p>
<p>The <code>label</code> property is just the label text of that field.</p>
<p>The <code>validate</code> property defines the validation parameters on the field. Regex is also supported.</p>
<p>Depending on the component property, you might need to pass in more properties. For example, the select component also needs dropdown options.</p>
<h2 id="heading-conditions">🎛 Conditions</h2>
<p>Forms fields might need to conditionally be hidden or visible if they satisfy a certain condition. With Data Driven Forms you can easily declare this inside the field schema.</p>
<p>Let's take an example to show visibility constraint in action.</p>
<pre><code class="lang-plaintext">{
    name: "whereInGoa",
    component: "text-field",
    label: "Where in Goa?",
    condition: {
        when: "state",
        is: "goa"
    }
}
</code></pre>
<p>Initially this field would be hidden. Only when the field with name <code>"state"</code> has a value of <code>"goa"</code>, would this field be visible.</p>
<p>This was a simple example. Data Driven Forms allows for more complex <a target="_blank" href="https://data-driven-forms.org/schema/condition-schema">conditions</a> where you can define multiple rules in <code>not</code>, <code>and</code>, or <code>or</code> logic. You can even change the value of the field if the condition satisfies.</p>
<h2 id="heading-validators">🛡 Validators</h2>
<p>Validation are important to prevent users from submitting incorrect data.</p>
<p>A common way to validate a field is to use regex. You can use the <code>"pattern"</code> validator to achieve this.</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">name</span>: <span class="hljs-string">"foo"</span>,
    ...
    validate: [
        {
            <span class="hljs-attr">type</span>: validatorTypes.PATTERN,
            <span class="hljs-attr">pattern</span>: <span class="hljs-regexp">/^Foo$/i</span>,
            message: <span class="hljs-string">"This field doesn't match the required format"</span>
        },
    ]
}
</code></pre>
<blockquote>
<p><code>message</code> property specifies a custom error message for when the validation fails.</p>
</blockquote>
<p>Following are all the <a target="_blank" href="https://data-driven-forms.org/schema/introduction#validate">validators</a> provided by the library:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> validatorTypes = {
  <span class="hljs-attr">REQUIRED</span>: <span class="hljs-string">'required'</span>;
  MIN_LENGTH: <span class="hljs-string">'min-length'</span>;
  MAX_LENGTH: <span class="hljs-string">'max-length'</span>;
  EXACT_LENGTH: <span class="hljs-string">'exact-length'</span>;
  MIN_ITEMS: <span class="hljs-string">'min-items'</span>;
  MIN_NUMBER_VALUE: <span class="hljs-string">'min-number-value'</span>;
  MAX_NUMBER_VALUE: <span class="hljs-string">'max-number-value'</span>;
  PATTERN: <span class="hljs-string">'pattern'</span>; <span class="hljs-comment">// regex</span>
  URL: <span class="hljs-string">'url'</span>;
}
</code></pre>
<p>You can define your custom validator too which is essentially a function that receives the value of the field as the first argument and is expected to return the error message string if validation fails or undefined if the validation passes.</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">name</span>: <span class="hljs-string">"foo"</span>,
    ...
    validate:  [<span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> (value === <span class="hljs-string">"Pawan"</span> ? <span class="hljs-string">'"Pawan is not valid" : undefined)]
}</span>
</code></pre>
<h2 id="heading-custom-component-mapper">🗃 Custom Component Mapper</h2>
<p>Using a predefined component mapper could be a great way to get started, but at some point, we want to use custom components that are already in our project.</p>
<p>Here is how we define a custom component mapper:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// customComponentMapper.js</span>
<span class="hljs-keyword">import</span> { TextField } <span class="hljs-keyword">from</span> <span class="hljs-string">"./TextField"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> customComponentMapper = {
  <span class="hljs-string">"text-field"</span>: TextField
};
</code></pre>
<p>Here the TextField component would contain the custom component prop mapping.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// TextField.jsx</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Form } <span class="hljs-keyword">from</span> <span class="hljs-string">"antd"</span>;
<span class="hljs-keyword">import</span> { useFieldApi } <span class="hljs-keyword">from</span> <span class="hljs-string">"@data-driven-forms/react-form-renderer"</span>;
<span class="hljs-keyword">import</span> { validationError } <span class="hljs-keyword">from</span> <span class="hljs-string">"@data-driven-forms/ant-component-mapper"</span>;
<span class="hljs-keyword">import</span> { Input } <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/Input"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TextField = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> {
    input,
    isReadOnly,
    isDisabled,
    isRequired,
    label,
    helperText,
    description,
    validateOnMount,
    meta,
    ...rest
  } = useFieldApi(props);

  <span class="hljs-keyword">const</span> invalid = validationError(meta, validateOnMount);
  <span class="hljs-keyword">const</span> warning = (meta.touched || validateOnMount) &amp;&amp; meta.warning;
  <span class="hljs-keyword">const</span> help = invalid || warning || helperText || description;

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Form.Item</span>
      <span class="hljs-attr">validateStatus</span>=<span class="hljs-string">{!invalid</span> ? (<span class="hljs-attr">warning</span> ? "<span class="hljs-attr">warning</span>" <span class="hljs-attr">:</span> "") <span class="hljs-attr">:</span> "<span class="hljs-attr">error</span>"}
      <span class="hljs-attr">help</span>=<span class="hljs-string">{help}</span>
      <span class="hljs-attr">label</span>=<span class="hljs-string">{label}</span>
      <span class="hljs-attr">required</span>=<span class="hljs-string">{isRequired}</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Input</span>
        {<span class="hljs-attr">...input</span>}
        <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> input.onChange(e.target.value)}
        defaultValue={input.value}
        disabled={isDisabled || isReadOnly}
        {...rest}
      /&gt;
    <span class="hljs-tag">&lt;/<span class="hljs-name">Form.Item</span>&gt;</span></span>
  );
};
</code></pre>
<blockquote>
<p>The useFieldApi hook is a wrapper around React Final Form <a target="_blank" href="https://final-form.org/docs/react-final-form/api/useField">useField</a> hook.</p>
</blockquote>
<p>In this case, <strong>Input</strong> component would be the custom component that you want to use as a text field.</p>
<p>If the component itself doesn't have a way to show the required symbol, label or description of a field, a wrapper component like Form.Item from ant design can be used.</p>
<p>We can now spread the <code>customComponentMapper</code> object while passing it into FormRenderer. This would override the predefined component mappers with our custom ones.</p>
<pre><code class="lang-plaintext">&lt;FormRenderer
    ...
    componentMapper={{ ...componentMapper, ...customComponentMapper }}
    ...
/&gt;
</code></pre>
<h2 id="heading-formtemplate">📐 FormTemplate</h2>
<p>The available component mapper packages offer a predefined <a target="_blank" href="https://data-driven-forms.org/components/form-template#formtemplate#formtemplate">FormTemplate</a> component to fit the design language of their respective Design Systems. At Certa, we have our own design system and needed a way to customize the button styles, placements and styling of the container form component. The best way to do this is to define our own custom FormTemplate component.</p>
<p>A very basic custom FormTemplate would look like this:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> FormTemplate = <span class="hljs-function">(<span class="hljs-params">{ schema, formFields }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { handleSubmit } = useFormApi();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{handleSubmit}</span>&gt;</span>
      { formFields }
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span></span>
  )
}
</code></pre>
<h2 id="heading-final-words">👋 Final Words</h2>
<p>There is some initial heavy lifting required to setup Data Driven Forms, especially if you have your own design system components. But once the custom component mappers, validators, and FormTemplate are good to go, the form-building effort is significantly low and would save a lot of developer effort and time in the long run.</p>
<p>Hope the Data Driven approach helps you and your team focus less on forms and more on the business logic of your product.</p>
<h2 id="heading-were-hiring"><strong>We're hiring!</strong></h2>
<p>If solving challenging problems at scale in a fully-remote team interests you, head to our <a target="_blank" href="https://www.getcerta.com/careers"><strong>careers page</strong></a> and apply for the position of your liking!</p>
]]></content:encoded></item><item><title><![CDATA[The Ultimate Hack for Supercharging Your CircleCI Pipeline!]]></title><description><![CDATA[🚀 Prerequisites

An existing CircleCI pipeline.

Basic knowledge of Pytest.

Basic knowledge of YAML files.


🌅 Background
We have a couple thousand test cases as the test suite of the Django app powering Certa. To ensure CI, we are utilizing Circl...]]></description><link>https://blog.certa.dev/the-ultimate-hack-for-supercharging-your-circleci-pipeline</link><guid isPermaLink="true">https://blog.certa.dev/the-ultimate-hack-for-supercharging-your-circleci-pipeline</guid><category><![CDATA[optimization]]></category><category><![CDATA[CircleCI]]></category><category><![CDATA[pytest]]></category><category><![CDATA[CI/CD]]></category><category><![CDATA[optimising-circleci]]></category><dc:creator><![CDATA[Martin Siby]]></dc:creator><pubDate>Mon, 06 Mar 2023 19:01:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677908310639/9956dbc5-d019-465a-a270-abdd32366495.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-prerequisites">🚀 Prerequisites</h2>
<ul>
<li><p>An existing CircleCI pipeline.</p>
</li>
<li><p>Basic knowledge of Pytest.</p>
</li>
<li><p>Basic knowledge of YAML files.</p>
</li>
</ul>
<h2 id="heading-background">🌅 Background</h2>
<p>We have a couple thousand test cases as the test suite of the Django app powering <a target="_blank" href="https://www.getcerta.com/">Certa</a>. To ensure CI, we are utilizing CircleCI to validate the PRs before merging them onto the repository's development branch.</p>
<h2 id="heading-the-problem">😬 The problem</h2>
<p>Due to the reasons listed below, developers were being slowed down and had to monitor and re-run test cases when merging their PR constantly.</p>
<ul>
<li><p>The total duration of the test run before optimization was ~28 min.</p>
</li>
<li><p>Due to the size &amp; history of the test suite, flaky test cases used to be a frequent occurrence.</p>
</li>
</ul>
<h2 id="heading-solutions">🤔 Solutions</h2>
<ul>
<li><h3 id="heading-distributing-test-cases-based-on-timing">📊 Distributing test cases based on timing</h3>
<ul>
<li><p>By default, test cases are distributed among containers based on the file names, which can lead to skewed timings like this:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678027426850/ffd48541-4309-4f53-b3fa-51faf8b7ec2b.png" alt class="image--center mx-auto" /></p>
<p>  Splitting based on timings would shave out much of the test duration for no extra cost.</p>
<p>  <strong>Steps:</strong></p>
<ol>
<li><p><strong>Upload test run report</strong></p>
<p> After a test run to get the test cases to generate a report once their run is completed</p>
<pre><code class="lang-yaml"> <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># this will create the dir to store the reports</span>
     <span class="hljs-attr">name:</span> <span class="hljs-string">Make</span> <span class="hljs-string">test</span> <span class="hljs-string">report</span> <span class="hljs-string">dir</span>
     <span class="hljs-attr">command:</span> <span class="hljs-string">mkdir</span> <span class="hljs-string">-p</span> <span class="hljs-string">test-results/reports</span>
 <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># we split the test cases using timing and generates report at the end of test run</span>
     <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span> <span class="hljs-string">and</span> <span class="hljs-string">create</span> <span class="hljs-string">report</span>
     <span class="hljs-attr">command:</span> <span class="hljs-string">|
       TESTFILES="$(circleci tests glob "&lt;test dir path&gt;/**/test_*.py" | circleci tests split --split-by=timings)
       pytest ${TESTFILES} --junitxml=test-results/reports/junit.xml
</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">store_test_results:</span> <span class="hljs-comment"># this will ensure test results are stored in CircleCI for timing data for later runs.</span>
     <span class="hljs-attr">path:</span> <span class="hljs-string">test-results</span>
</code></pre>
<p> <strong>Note</strong>: By default, the reports we generate use <code>xunit2</code> format to store test results that do not contain the file paths and hence can't be used to generate timing data at the time of writing this blog. A workaround we used is to specify the following in the <code>setup.cfg</code> the file of the Django project:</p>
<pre><code class="lang-bash"> [tool:pytest]
 python_files = <span class="hljs-built_in">test</span>*.py
 junit_family = xunit1
</code></pre>
</li>
<li><p><strong>Split test cases based on timings</strong></p>
<p> Once we have the timing data available on CircleCI, then we can add split-by-timing to the YAML file as follows(re-iterating):</p>
<pre><code class="lang-yaml"> <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># we split the test cases using glob and generates report at the end of test run</span>
     <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span> <span class="hljs-string">and</span> <span class="hljs-string">create</span> <span class="hljs-string">report</span>
     <span class="hljs-attr">command:</span> <span class="hljs-string">|
       TESTFILES="$(circleci tests glob "&lt;test dir path&gt;/**/test_*.py" | circleci tests split --split-by=timings)
       pytest ${TESTFILES} -n 4  --reuse-db --junitxml=test-results/reports/junit.xml</span>
</code></pre>
<p> The <code>run</code> command uses two things, the first is to get all the test files using <a target="_blank" href="https://circleci.com/docs/use-the-circleci-cli-to-split-tests/#glob-test-files">glob</a> and then to initiate the test run by specifying to <a target="_blank" href="https://circleci.com/docs/use-the-circleci-cli-to-split-tests/#split-by-timing-data">use timing as the split criteria.</a></p>
</li>
<li><p><strong>Set timing-type</strong></p>
<p> Now that we have timing data, we can set the granularity. CircleCI supports 4 different choices: <code>filename</code> , <code>classname</code>, <code>testname</code> and <code>autodetect</code>. This choice will depend on your needs but <code>classname</code> worked best for our case.</p>
</li>
</ol>
</li>
<li><p><strong>Cost vs Impact</strong></p>
<ul>
<li>No additional cost was associated with this change, but it significantly improved the timings, reducing the overall duration by 36%.</li>
</ul>
</li>
<li><p><strong>Caveats</strong></p>
<ul>
<li>The <code>store_test_results</code> command needs a directory as input. The test results need to be stored inside this directory.</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="heading-increasing-container-count">📈 Increasing container count</h3>
<p>  This is an obvious option available to you, but the problem is that it will incur higher credit usage. This particular option varies based on projects, we found that 8 containers of the large class are the ideal configuration for our use case, which saves time while not burning away credits. Below are the YAML file changes:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/repo</span> 
  <span class="hljs-attr">resource_class:</span> <span class="hljs-string">large</span> 
  <span class="hljs-attr">parallelism:</span> <span class="hljs-number">8</span> 
  <span class="hljs-attr">steps:</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-attr">attach_workspace:</span> 
             <span class="hljs-attr">at:</span> <span class="hljs-string">~/repo</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># we split the test cases using glob and generate the report at the end of the test run </span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span> <span class="hljs-string">and</span> <span class="hljs-string">create</span> <span class="hljs-string">report</span> 
        <span class="hljs-attr">command:</span> <span class="hljs-string">|</span> <span class="hljs-string">TESTFILES="$(circleci</span> <span class="hljs-string">tests</span> <span class="hljs-string">glob</span> <span class="hljs-string">"&lt;test dir path&gt;/**/test_*.py"</span> <span class="hljs-string">|</span> <span class="hljs-string">circleci</span> <span class="hljs-string">tests</span> <span class="hljs-string">split</span> <span class="hljs-string">--split-by=timings)</span> <span class="hljs-string">pytest</span> <span class="hljs-string">${TESTFILES}</span> <span class="hljs-string">-n</span> <span class="hljs-number">4</span> <span class="hljs-string">--reuse-db</span> <span class="hljs-string">--junitxml=test-results/reports/junit.xml</span>
</code></pre>
<ul>
<li><p><strong>Cost vs Impact</strong></p>
<ul>
<li>The Addon cost was around 100k credits per month. It reduced the timings by around 28% and also consumed a lot of time to find the sweet spot since each time after changing the number, we had to run the pipeline completely to get the impact.</li>
</ul>
</li>
<li><p><strong>Caveats</strong></p>
<ul>
<li><p>This will be time-consuming; what we have done to make it easier is to push several commits with only the job container count changed and run the pipelines in parallel manually. By default, it cancels a job when a new commit is pushed, so you must manually rerun the suite.</p>
</li>
<li><p>Increasing parallelism can also lead to an increase in flaky test cases, depending on your test suite. There can be multiple reasons for this - race conditions, resource contention, or synchronization issues. Read on to learn about the solutions that worked for us.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="heading-parallel-test-runs-with-a-job">🏎️ Parallel test runs with a job</h3>
<p>  If you already have parallel containers running the jobs, then one thing to keep track of is the resource usage section of the test run. Below is the screenshot of an unoptimized job.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678783264918/cfdf5582-bb75-4851-bd6a-c5f80e309c61.png" alt class="image--center mx-auto" /></p>
<p>  As the screenshot shows, jobs were utilizing at most 50% of the CPU. An easy remedy we applied is to utilize the number of parallel pytest instances running in a container. Below is the config change for increasing the instances to 4:</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/repo</span>
     <span class="hljs-attr">resource_class:</span> <span class="hljs-string">large</span>
     <span class="hljs-attr">parallelism:</span> <span class="hljs-number">8</span>
     <span class="hljs-attr">steps:</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">attach_workspace:</span>
             <span class="hljs-attr">at:</span> <span class="hljs-string">~/repo</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># we split the test cases using glob and generate the report at the end of the test run </span>
         <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span> <span class="hljs-string">and</span> <span class="hljs-string">create</span> <span class="hljs-string">report</span>
         <span class="hljs-attr">command:</span> <span class="hljs-string">|
           TESTFILES="$(circleci tests glob "&lt;test dir path&gt;/**/test_*.py" | circleci tests split --split-by=timings)
           pytest ${TESTFILES} -n 4  --reuse-db --junitxml=test-results/reports/junit.xml</span>
</code></pre>
<p>  <strong>Note</strong>: Low CPU resource usage is expected if your test suite is IO-heavy. In such cases lowering the resource type can yield some savings on credits.</p>
<ul>
<li><p><strong>Cost vs Impact</strong></p>
<ul>
<li>No additional cost was associated with this change, and the impact was not great either. On average, it saved about 1-2 mins (3.8%) per test run.</li>
</ul>
</li>
<li><p><strong>Caveats</strong></p>
<ul>
<li>When you optimize this value, if you don't see at least a 15% improvement compared to the previous value, then it is time to stop since, post that, your cost increase will probably be unjustifiable.</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="heading-using-cache-and-workspace-storage">💾 Using cache and workspace storage</h3>
<p>  Projects that utilize external libraries or files that remain relatively constant between multiple test runs can be cached and used to save a significant amount of time. We used:</p>
<ul>
<li><p>cache to store our python libraries by generating a key based on the hash of the requirement files</p>
</li>
<li><p>workspaces cache to create a common test env from which different test jobs start.</p>
<p>The config is as follows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">prep-test-env:</span>
   <span class="hljs-attr">parameters:</span>
     <span class="hljs-attr">resource_class:</span>
       <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
       <span class="hljs-attr">description:</span> <span class="hljs-string">CircleCI</span> <span class="hljs-string">resource</span> <span class="hljs-string">class</span>
       <span class="hljs-attr">default:</span> <span class="hljs-string">medium</span>
   <span class="hljs-attr">docker:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">cimg/python:3.9.16</span>
   <span class="hljs-attr">working_directory:</span> <span class="hljs-string">~/repo</span>
   <span class="hljs-attr">resource_class:</span> <span class="hljs-string">large</span>
   <span class="hljs-attr">steps:</span>
     <span class="hljs-bullet">-</span> <span class="hljs-string">checkout</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># Generate a hash based on requirements</span>
         <span class="hljs-attr">name:</span> <span class="hljs-string">Check</span> <span class="hljs-string">if</span> <span class="hljs-string">requirements</span> <span class="hljs-string">changed</span>
         <span class="hljs-attr">command:</span> <span class="hljs-string">|
           echo $(find ./requirements -type f -exec md5sum {} \; | md5sum | cut -d' ' -f1)  &gt;&gt; REQUIREMENTS_CACHE_KEY
</span>     <span class="hljs-bullet">-</span> <span class="hljs-attr">restore_cache:</span>
         <span class="hljs-attr">keys:</span>
           <span class="hljs-bullet">-</span> <span class="hljs-string">dependencies-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"REQUIREMENTS_CACHE_KEY"</span> <span class="hljs-string">}}</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># this will install any new dependency added</span>
         <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">python</span> <span class="hljs-string">dependencies</span>
         <span class="hljs-attr">command:</span> <span class="hljs-string">|
           python -m venv venv
           . venv/bin/activate
           pip install --upgrade setuptools
           pip install -e .
           for file in requirements/*.txt; do pip install -r "$file"; done
</span>     <span class="hljs-bullet">-</span> <span class="hljs-attr">save_cache:</span>
         <span class="hljs-attr">paths:</span>
           <span class="hljs-bullet">-</span> <span class="hljs-string">venv</span>
         <span class="hljs-attr">key:</span> <span class="hljs-string">dependencies-{{</span> <span class="hljs-string">checksum</span> <span class="hljs-string">"REQUIREMENTS_CACHE_KEY"</span> <span class="hljs-string">}}</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">persist_to_workspace:</span>
         <span class="hljs-attr">root:</span> <span class="hljs-string">~/repo</span>
         <span class="hljs-attr">paths:</span> <span class="hljs-string">./</span>
</code></pre>
<p>In our job for the test run, we will attach this saved workspace as follows:</p>
<pre><code class="lang-yaml">    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">attach_workspace:</span>
            <span class="hljs-attr">at:</span> <span class="hljs-string">~/repo</span>
</code></pre>
<p>To specify the order in which we define the following in the workflows section</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">workflows:</span>
    <span class="hljs-attr">version:</span> <span class="hljs-number">2</span>
    <span class="hljs-attr">app-tests:</span>
      <span class="hljs-attr">jobs:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">prep-test-env:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">Prepare</span> <span class="hljs-string">the</span> <span class="hljs-string">test</span> <span class="hljs-string">environment</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">run-test:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">run</span>
            <span class="hljs-attr">requires:</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">Prepare</span> <span class="hljs-string">the</span> <span class="hljs-string">test</span> <span class="hljs-string">environment</span>
</code></pre>
<p>This will ensure the <code>prep-test-env</code> runs before the test cases are run.</p>
</li>
<li><p><strong>Cost vs Impact</strong></p>
<ul>
<li>This caused an additional cost of ~3k credits per month, including caches and workspace storage. This saved about 40 seconds from each test run job, which is significant since each parallel job consumes credits independently.</li>
</ul>
</li>
<li><p><strong>Caveats</strong></p>
<ul>
<li><p>Do make sure to set the usage policy to avoid storing irrelevant data. We have set it as follows:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678082116444/bbac1a43-731d-447a-8900-cdce0b3237e8.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li><h3 id="heading-auto-rerun-failed-test-cases">🔄 Auto-rerun failed test cases</h3>
<p>  Once the optimizations were done, flaky test cases became the bane of our existence. Since this would be counter-productive to the main objective of lowering the load on developers. We utilized a library called <a target="_blank" href="https://pypi.org/project/pytest-rerunfailures/">pytest-rerunfailures</a>, which, as the name suggests, re-runs failed test cases. But the problem is that this is not a library required for production and is not maintained/secured to be installed in production. Hence it was added in a test.txt file in the requirements directory, and since we utilize <code>for file in requirements/*.txt; do pip install -r "$file"; done</code> it will be installed during test runs and not in any other server.</p>
</li>
<li><pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-comment"># we split the test cases using glob and generate the report at the end of the test run </span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span> <span class="hljs-string">and</span> <span class="hljs-string">create</span> <span class="hljs-string">report</span> 
          <span class="hljs-attr">command:</span> <span class="hljs-string">|</span> <span class="hljs-string">pytest</span> <span class="hljs-string">${TESTFILES}</span> <span class="hljs-string">-n</span> <span class="hljs-number">4</span> <span class="hljs-string">--reuse-db</span> <span class="hljs-string">--junitxml=test-results/reports/junit.xml</span> <span class="hljs-string">--reruns</span> <span class="hljs-number">3</span> <span class="hljs-string">-x</span>
</code></pre>
<p>  <code>--reruns 3</code> reruns the failed test case up to 3 times, and if one of them passes, it goes ahead with the rest.</p>
<p>  <code>-x</code> will exit immediately after a test case fails, even after 3 reruns</p>
<ul>
<li><p><strong>Cost vs Impact</strong></p>
<ul>
<li>No cost add-on for this change, but it caused the flaky test cases to be reduced significantly from 2-3 flaky test cases failures per job run to 1 flaky failure in every 10-15 test runs.</li>
</ul>
</li>
<li><p><strong>Caveats</strong></p>
<ul>
<li><p>Make sure to include <code>-x</code> in the command, if your pipeline is configured to fail even if one test case fails since that will save some credits.</p>
<p>  You could also set it to <a target="_blank" href="https://pypi.org/project/pytest-rerunfailures/#re-run-all-failures-matching-certain-expressions">only rerun test cases with a specific type of failure</a> like this: <code>--only-rerun AssertionError</code> this will only rerun test cases that have failed due to AssertionError.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="heading-were-hiring">We're hiring!</h1>
<p>If solving challenging problems at scale in a fully-remote team interests you, head to our <a target="_blank" href="https://www.getcerta.com/careers">careers page</a> and apply for the position of your liking!</p>
]]></content:encoded></item><item><title><![CDATA[Generating React Icon Components from Figma]]></title><description><![CDATA[Maintaining icons in a React project can be a mess. Some amount of automation can always make the lives of Frontend devs on your team simpler and save precious time.

At Certa, our designer maintains over 150 SVG icons on a Figma board, with new ones...]]></description><link>https://blog.certa.dev/generating-react-icon-components-from-figma</link><guid isPermaLink="true">https://blog.certa.dev/generating-react-icon-components-from-figma</guid><category><![CDATA[Design]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[frontend]]></category><category><![CDATA[icon]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Pawan Kolhe]]></dc:creator><pubDate>Wed, 12 Jan 2022 12:32:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1641554337432/0Dnm_5QFx.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Maintaining icons in a React project can be a mess. Some amount of automation can always make the lives of Frontend devs on your team simpler and save precious time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641219632489/SnBYpjvnp.png" alt="Figma canvas containing icons" /></p>
<p>At Certa, our designer maintains <strong>over 150 SVG icons</strong> on a Figma board, with new ones coming every now and then. Previously we had to go through the following tedious and manual process of adding an icon from Figma to our React project:</p>
<ol>
<li>Exporting the icon as SVG format from Figma</li>
<li>Creating a React component file and appropriately naming it</li>
<li>Copying and pasting the SVG code into the component file</li>
<li>Exposing props that parent component can pass (color, size, etc)</li>
<li>Modifying SVG attributes (e.g. setting fill to currentColor)</li>
<li>Clean unnecessary attributes</li>
<li>Exporting the React icon component from the index file</li>
</ol>
<p>This was a slow, repetitive, and error-prone process. Certainly, we could write some scripts here to automate much of the process. So we did. But first, let us explore why the above process can be a problem.</p>
<h2 id="heading-the-problem">🔎 The Problem</h2>
<h3 id="heading-problem-1">Problem 1</h3>
<p>Everyone has their own way of performing manual work. Not every developer on the team will follow a consistent way of performing the above steps.</p>
<p>Let us say Alice downloaded <code>loader.svg</code> icon from Figma and created a component called <code>Spinner.tsx</code> in the React project because that name seems to make sense to Alice. Bob comes along and is searching for the loader icon by searching for <code>Loader.tsx</code> file assuming that icons are probably named according to their names on Figma. But Bob doesn't find the icon, so he creates a new icon component name <code>Loader.tsx</code> from <code>loader.svg</code>. </p>
<p>Now we have two icon components named differently, but having the same code. This will lead to <strong>code duplication</strong> unknowingly, which is never good.</p>
<h3 id="heading-problem-2">Problem 2</h3>
<p>Another issue was that of <strong>inconsistent props</strong> of icon components. In some components, certain props ( e.g. color and size) were added, and in some, it wasn't. In some components, the default value of a prop (e.g. color) was set to a hex value and in others set to <code>currentColor</code>. This had led to unpredictable behavior when using icons and <strong>unexpected bugs</strong> started haunting the UI.</p>
<h2 id="heading-the-plan">🚀 The Plan</h2>
<p>Our frontend codebase is a monorepo housing various packages each having its own special purpose. One such package is called blocks (<code>@certa/blocks</code>). It houses the design system/building block components of our application UI such as the button, menu, tooptip, etc. Previously it housed the icons too, but it didn't quite feel like the right home for it. </p>
<p>The proposed idea was to create a new package (<code>@certa/icons</code>) in the monorepo specifically catering to icons and automating as much of the icon component generation process as possible. </p>
<p>Maybe not all the icons you need in your project are on Figma. Not a problem, we can also load SVGs icons into the script from a specific folder as an additional source.</p>
<p>The new package would contain a script that would:</p>
<ol>
<li>Fetch SVG icons from Figma</li>
<li>Load SVG icons added manually into a specific folder as an additional source</li>
<li>Clean and optimize the SVG code</li>
<li>Generate React components for each icon which expose common props with the same defaults</li>
<li>Create an index file that exports all the icon components</li>
</ol>
<h2 id="heading-the-script">🧑‍💻 The Script</h2>
<p>Here is a <a target="_blank" href="https://codesandbox.io/s/icons-generator-jn248?file=/src/generate.ts">CodeSandbox</a> for you to view the source code and run it yourself:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/icons-generator-jn248?expanddevtools=1&amp;fontsize=14&amp;hidenavigation=1&amp;module=%2Fsrc%2Fgenerate.ts&amp;theme=dark&amp;view=editor">https://codesandbox.io/embed/icons-generator-jn248?expanddevtools=1&amp;fontsize=14&amp;hidenavigation=1&amp;module=%2Fsrc%2Fgenerate.ts&amp;theme=dark&amp;view=editor</a></div>
<p>The script has been implemented using Node.js with TypeScript and it will make use of the following libraries:</p>
<ul>
<li><strong><a target="_blank" href="https://www.npmjs.com/package/figma-api-exporter">figma-api-exporter</a></strong> - for fetching icons data from Figma</li>
<li><strong><a target="_blank" href="https://react-svgr.com/">@svgr/core</a></strong> - transforming SVG code into React components</li>
<li><strong><a target="_blank" href="https://www.npmjs.com/package/ts-node">ts-node</a></strong> - enables executing TypeScript on Node.js without precompiling</li>
<li><strong><a target="_blank" href="https://github.com/jprichardson/node-fs-extra">fs-extra</a></strong> - adds file system methods that aren't included in the native <code>fs</code> module</li>
<li><strong><a target="_blank" href="https://www.npmjs.com/package/axios">axios</a></strong> - for downloading SVGs from Figma</li>
<li><strong><a target="_blank" href="https://www.npmjs.com/package/dotenv">dotenv</a></strong> - loading environment variables into the script</li>
<li><strong><a target="_blank" href="https://www.npmjs.com/package/chalk">chalk</a></strong> - printing colored output to the terminal</li>
</ul>
<p>Firstly, we need to install the required libraries:</p>
<pre><code class="lang-bash">yarn add react figma-api-exporter fs-extra axios dotenv @svgr/core@5.5.0 @svgr/plugin-prettier@5.5.0 @svgr/plugin-svgo@5.5.0 chalk@4.1.2

yarn add --dev typescript ts-node @types/react @types/node @types/fs-extra
</code></pre>
<h3 id="heading-folder-structure">Folder Structure</h3>
<p>We'll be using the directory structure given below to organize our code:</p>
<pre><code>icons<span class="hljs-operator">-</span>generator<span class="hljs-operator">/</span>
├─ src<span class="hljs-operator">/</span>
│  ├─ icons<span class="hljs-operator">/</span>
│  │  ├─ components<span class="hljs-operator">/</span> (all generated React icon components)
│  │  │  ├─ Bell.tsx
│  │  │  ├─ Gear.tsx
│  │  │  ├─ Home.tsx
│  │  ├─ svgs<span class="hljs-operator">/</span> (manually added SVGs)
│  │  │  ├─ Gear.svg
│  │  ├─ index.tsx
│  ├─ templates<span class="hljs-operator">/</span>
│  ├─ generate.ts
│  ├─ types.ts
│  ├─ utils.ts
│  ├─ index.tsx
├─ .env
├─ svgr.config.js
├─ package.json
</code></pre><p><code>src/generate.ts</code> file is the primary script where a majority of our logic would be.</p>
<p><code>svgr.config.js</code> is the SVGR library configuration file</p>
<p><code>src/icons/components/</code> will contain all our generated react component files</p>
<p><code>src/icons/svgs/</code> contains SVG files that are not in Figma but also need to be converted to React components.</p>
<p><code>src/icons/index.tsx</code> index file that exports all icon react components from components/ directory</p>
<h3 id="heading-setup">Setup</h3>
<p>The first step in the script would be to fetch the metadata of icons from Figma. The metadata will contain S3 URLs of all the icons which can be used to download their corresponding SVG code. We will use <a target="_blank" href="https://www.npmjs.com/package/figma-api-exporter">figma-api-exporter</a> for this purpose.</p>
<p>To use this library, we would need to set up our <em>Figma Personal Access token</em> that the library can use to access our Figma account. Secondly, we need the <em>Figma file id</em> of the Figma board containing the icons. And lastly, the <em>canvas</em> name is required to locate the icons.</p>
<p><strong>Getting Figma Personal Access Token, File ID, and Canvas</strong></p>
<ol>
<li><p>Go to your <strong><a target="_blank" href="https://www.figma.com/">Figma</a></strong> dashboard and open Settings from the top right dropdown menu.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641219980652/O9PMv0LsA.png" alt="68747470733a2f2f692e6962622e636f2f717072626637562f53637265656e73686f742d323032312d30392d33302d61742d352d32382d34342d504d2e706e67.png" /></p>
</li>
<li><p>Scroll down to the "Personal access tokens" section and create an API token. Keep the API token handy, we'll be adding it to the <code>.env</code> file soon.</p>
</li>
<li><p>Now to get the canvas name, look at the sidebar on Figma. The canvas is the name of the page on a Figma board where you kept the icons. In my case, the canvas is named "Icons".
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641220100490/zMI7kVW2C.png" alt="Screenshot 2021-12-06 at 9.37.19 AM.png" /></p>
</li>
<li><p>One more thing we need is the <em>Figma file id</em>. Go to the Figma board where your icons reside. Look for the file id in the URL, it'll be the 12 character alphanumeric text.</p>
<pre><code class="lang-markdown">https://www.figma.com/file/atT09besy7MsrjeyUsJLfP/Example-Board
</code></pre>
</li>
<li><p>Anyone who has your personal access token can get full access to your Figma account. It is best to save these values to a <code>.env</code> file so that we don't accidentally commit these values to version control. In the <code>.env</code> file, add the API token and the file id as key-value pairs.</p>
<pre><code> FIGMA_API_TOKEN<span class="hljs-operator">=</span><span class="hljs-operator">&lt;</span>YOUR_FIGMA_API_TOKEN<span class="hljs-operator">&gt;</span>
 FIGMA_FILE_ID<span class="hljs-operator">=</span><span class="hljs-operator">&lt;</span>FIGMA_FILE_ID<span class="hljs-operator">&gt;</span>
 FIGMA_CANVAS<span class="hljs-operator">=</span>Icons
</code></pre></li>
<li><p>Open the package.json file and add the following entry in the scripts section:</p>
<pre><code class="lang-tsx"> "icons": "ts-node ./src/scripts/generate.ts",
</code></pre>
<p> Now we can just use the <code>yarn icons</code> command to run the script.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641986660184/WdqQUQE8J.png" alt="yarn icons output" /></p>
</li>
</ol>
<h3 id="heading-code">Code</h3>
<p>Enough talk, let's dive into the <a target="_blank" href="https://codesandbox.io/s/icons-generator-jn248?file=/src/generate.ts">code</a>.</p>
<ol>
<li><p>Firstly, we load the environment variables.</p>
<pre><code class="lang-ts"> <span class="hljs-comment">// generate.ts</span>

 <span class="hljs-comment">// Loads environment variables from .env</span>
 dotenv.config();

 <span class="hljs-comment">// 1. Retrieve Figma Access Token, File ID and Canvas from .env file</span>
 <span class="hljs-keyword">const</span> FIGMA_API_TOKEN = process.env.FIGMA_API_TOKEN;
 <span class="hljs-keyword">const</span> FIGMA_FILE_ID = process.env.FIGMA_FILE_ID;
 <span class="hljs-keyword">const</span> FIGMA_CANVAS = process.env.FIGMA_CANVAS;
</code></pre>
</li>
<li><p>Then we use figma-api-exporter library to fetch metadata about the icons from Figma, providing it with the token, file id and canvas name. </p>
<pre><code class="lang-ts"> <span class="hljs-comment">// 2. Fetch icons metadata from Figma</span>

 <span class="hljs-comment">// generate.ts</span>
 <span class="hljs-keyword">const</span> exporter = figmaApiExporter(FIGMA_API_TOKEN);
 exporter
   .getSvgs({
     fileId: FIGMA_FILE_ID,
     canvas: FIGMA_CANVAS
   })
   .then(<span class="hljs-keyword">async</span> svgsData =&gt; {
     <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'SVGs DATA:'</span>, svgsData);
   })
   .catch(<span class="hljs-function">(<span class="hljs-params">err: unknown</span>) =&gt;</span> {
     process.exit(<span class="hljs-number">1</span>);
   });
</code></pre>
<p> The console.log would output the following object structure:</p>
<pre><code class="lang-json"> {
     <span class="hljs-attr">"svgs"</span>: [
         {
             id: <span class="hljs-string">"1:43"</span>,
             url: <span class="hljs-string">"https://s3-us-west-2.amazonaws.com/..."</span>,
             name: <span class="hljs-string">"House"</span>
         },
         {
             id: <span class="hljs-string">"2:86"</span>,
             url: <span class="hljs-string">"https://s3-us-west-2.amazonaws.com/..."</span>,
             name: <span class="hljs-string">"Chevron down"</span>
         },
         ...
     ],
     <span class="hljs-attr">"lastModified"</span>: <span class="hljs-string">"2021-11-30T19:19:08Z"</span>
 }
</code></pre>
<p> The most interesting properties here are <code>url</code> and <code>name</code>.</p>
</li>
<li><p>We can use the <code>url</code> property of each icon to download the SVG code using the axios library. A simple get request will do the trick.</p>
<pre><code class="lang-ts"> <span class="hljs-comment">// utils.ts</span>
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> downloadSVGsData = <span class="hljs-keyword">async</span> &lt;T <span class="hljs-keyword">extends</span> {}&gt;(
   data: <span class="hljs-function">(<span class="hljs-params">{ url: <span class="hljs-built_in">string</span> } &amp; T</span>)[]
 ) =&gt;</span> {
   <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.all(
     data.map(<span class="hljs-keyword">async</span> dataItem =&gt; {
       <span class="hljs-keyword">const</span> downloadedSvg = <span class="hljs-keyword">await</span> axios.get&lt;<span class="hljs-built_in">string</span>&gt;(dataItem.url);
       <span class="hljs-keyword">return</span> {
         ...dataItem,
         data: downloadedSvg.data
       };
     })
   );
 };

 <span class="hljs-comment">// generate.ts</span>
 <span class="hljs-comment">// 3. Download SVG files from Figma</span>
 <span class="hljs-keyword">const</span> downloadedSVGsData = <span class="hljs-keyword">await</span> downloadSVGsData(svgsData.svgs);
</code></pre>
</li>
<li><p>Not all your icons might be in Figma. Having a second source for adding SVG icons would be great to have. So next, we can load the manually added SVG files located in <code>src/icons/svgs/</code> and combine them with the ones downloaded from Figma into a single array named <code>allSvgs</code>. The <code>fs.readdirSync</code> would come in handy, allowing us to retrieve files in a certain directory.</p>
<pre><code class="lang-ts"> <span class="hljs-comment">// generate.ts</span>

 <span class="hljs-comment">// 4. Read manually added SVGs data</span>
 <span class="hljs-keyword">let</span> manuallyAddedSvgs: { data: <span class="hljs-built_in">string</span>; name: <span class="hljs-built_in">string</span> }[] = [];
 <span class="hljs-keyword">const</span> svgFiles = fs
   .readdirSync(SVG_DIRECTORY_PATH)
   <span class="hljs-comment">// Filter out hidden files (e.g. .DS_STORE)</span>
   .filter(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> !<span class="hljs-regexp">/(^|\/)\.[^/.]/g</span>.test(item));
 svgFiles.forEach(<span class="hljs-function">(<span class="hljs-params">fileName</span>) =&gt;</span> {
   <span class="hljs-keyword">const</span> svgData = fs.readFileSync(
     path.resolve(SVG_DIRECTORY_PATH, fileName),
     <span class="hljs-string">"utf-8"</span>
   );
   manuallyAddedSvgs.push({
     data: svgData,
     name: toPascalCase(fileName.replace(<span class="hljs-regexp">/svg/i</span>, <span class="hljs-string">""</span>))
   });
 });
 <span class="hljs-keyword">const</span> allSVGs = [...downloadedSVGsData, ...manuallyAddedSvgs];
</code></pre>
</li>
<li><p>Once we have all the SVG code, it is time to convert them into React components and write the files to the <code>src/icons/components/</code> directory.</p>
<pre><code class="lang-ts"> <span class="hljs-comment">// generate.ts</span>

 <span class="hljs-comment">// 5. Convert SVG to React Components</span>
 allSVGs.forEach(<span class="hljs-function"><span class="hljs-params">svg</span> =&gt;</span> {
   <span class="hljs-keyword">const</span> svgCode = svg.data;
   <span class="hljs-keyword">const</span> componentName = toPascalCase(svg.name);
   <span class="hljs-keyword">const</span> componentFileName = <span class="hljs-string">`<span class="hljs-subst">${componentName}</span>.tsx`</span>;

   <span class="hljs-comment">// Converts SVG code into React code using SVGR library</span>
   <span class="hljs-keyword">const</span> componentCode = svgr.sync(svgCode, svgrConfig, { componentName });

   <span class="hljs-comment">// 6. Write generated component to file system</span>
   fs.outputFileSync(
     path.resolve(ICONS_DIRECTORY_PATH, componentFileName),
     componentCode
   );
});
</code></pre>
<p> The <code>svgr.sync</code> function is performing some magic here, converting  SVG code to React component. We will dive deeper into how it works in a later section.</p>
</li>
<li><p>After the React components are created, the process of <strong>generating an index file</strong> can begin. The index file would export all the React components, so they can be used outside our current package. Have to say, the final generated index file looks oddly satisfying. 😄
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641226869443/OgSsuKYcc.png" alt="index file" /></p>
<pre><code class="lang-js"> <span class="hljs-comment">// utils.ts</span>
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createIndex = <span class="hljs-function">(<span class="hljs-params">{
   componentsDirectoryPath,
   indexDirectoryPath,
   indexFileName
 }: IndexConfigProps</span>) =&gt;</span> {
   <span class="hljs-keyword">let</span> indexContent = <span class="hljs-string">""</span>;
   fs.readdirSync(componentsDirectoryPath).forEach(<span class="hljs-function"><span class="hljs-params">componentFileName</span> =&gt;</span> {
     <span class="hljs-comment">// Convert name to pascal case</span>
     <span class="hljs-keyword">const</span> componentName = toPascalCase(
       componentFileName.substr(<span class="hljs-number">0</span>, componentFileName.indexOf(<span class="hljs-string">"."</span>)) ||
         componentFileName
     );

     <span class="hljs-comment">// Compute relative path from index file to component file</span>
     <span class="hljs-keyword">const</span> relativePathToComponent = path.relative(
       indexDirectoryPath,
       path.resolve(componentsDirectoryPath, componentName)
     );

     <span class="hljs-comment">// Export statement</span>
     <span class="hljs-keyword">const</span> componentExport = <span class="hljs-string">`export { default as <span class="hljs-subst">${componentName}</span> } from "./<span class="hljs-subst">${relativePathToComponent}</span>";`</span>;

     indexContent += componentExport + os.EOL;
   });

     <span class="hljs-comment">// Write the content to file system</span>
   fs.writeFileSync(path.resolve(indexDirectoryPath, indexFileName), indexContent);
 };

 <span class="hljs-comment">// generate.ts</span>
 <span class="hljs-comment">// 7. Generate index.ts</span>
 createIndex({
   <span class="hljs-attr">componentsDirectoryPath</span>: ICONS_DIRECTORY_PATH,
   <span class="hljs-attr">indexDirectoryPath</span>: INDEX_DIRECTORY_PATH,
   <span class="hljs-attr">indexFileName</span>: <span class="hljs-string">"index.tsx"</span>
 });
</code></pre>
<p> The above function is simply looping over all the icon components, converting the filename to a component name, computing the relative path of the component file, forming an export style string, and concatenating it to a string that then gets written to the <code>index.tsx</code> file. </p>
</li>
</ol>
<h2 id="heading-svg-to-react-components">⚙️ SVG to React Components</h2>
<p>When it comes to raw SVG to React conversion, the SVGR library gets the job done. It provides additional plugins like SVGO that are able to shave off some excess SVG styling and help with size reduction.</p>
<p>I provided SVGR with a config where I customized the output React component code as per my requirements. The <code>svgProps</code> property is used to set the SVG attribute values. <code>replaceAttrValues</code> will replace an existing attribute value with a new one, very useful for changing a hexcode color for fill attribute to currentColor which essentially tells the SVG to inherit the color property value from a parent element.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// svgr.config.js</span>
<span class="hljs-keyword">const</span> componentTemplate = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./src/templates/componentTemplate"</span>);

<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">typescript</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">icon</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">svgProps</span>: {
    <span class="hljs-attr">width</span>: <span class="hljs-string">"inherit"</span>,
    <span class="hljs-attr">height</span>: <span class="hljs-string">"inherit"</span>
  },
  <span class="hljs-attr">replaceAttrValues</span>: {
    <span class="hljs-string">"#00164E"</span>: <span class="hljs-string">"currentColor"</span>
  },
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-comment">// Clean SVG files using SVGO</span>
    <span class="hljs-string">"@svgr/plugin-svgo"</span>,
    <span class="hljs-comment">// Generate JSX</span>
    <span class="hljs-string">"@svgr/plugin-jsx"</span>,
    <span class="hljs-comment">// Format the result using Prettier</span>
    <span class="hljs-string">"@svgr/plugin-prettier"</span>
  ],
  <span class="hljs-attr">svgoConfig</span>: {},
  <span class="hljs-attr">template</span>: componentTemplate
};
</code></pre>
<p>This is cool and all, but you might be wondering how we can customize the React component code that SVGR pushes out. Here is where templates come in. SVGR allows us to pass it a template function that is internally executed by the babel plugin <code>babel-plugin-transform-svg-component</code> and expects Babel AST (Abstract Syntax Tree) to be returned from the function.</p>
<p>AST is essentially a tree representation of program source code, and in our case represents the React component source code.</p>
<pre><code class="lang-js"><span class="hljs-comment">// componentTemplate.js</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">componentTemplate</span>(<span class="hljs-params">
  { template },
  opts,
  { imports, componentName, props, jsx, exports }
</span>) </span>{
  <span class="hljs-keyword">const</span> code = <span class="hljs-string">`
    %%NEWLINE%%
    %%NEWLINE%%

    import * as React from 'react';
    import { IconProps } from '../../types';
    import { IconWrapper } from '../IconWrapper';

    %%NEWLINE%%

    const %%COMPONENT_NAME%% = (allProps: IconProps) =&gt; {
      const { svgProps: props, ...restProps } = allProps;
      return &lt;IconWrapper icon={%%JSX%%} {...restProps} /&gt;
    };

    %%EXPORTS%%
  `</span>;

  <span class="hljs-keyword">const</span> mapping = {
    <span class="hljs-attr">COMPONENT_NAME</span>: componentName,
    <span class="hljs-attr">JSX</span>: jsx,
    <span class="hljs-attr">EXPORTS</span>: <span class="hljs-built_in">exports</span>,
    <span class="hljs-attr">NEWLINE</span>: <span class="hljs-string">"\n"</span>
  };

  <span class="hljs-comment">/**
   * API Docs: https://babeljs.io/docs/en/babel-template#api
   */</span>
  <span class="hljs-keyword">const</span> typeScriptTpl = template(code, {
    <span class="hljs-attr">plugins</span>: [<span class="hljs-string">"jsx"</span>, <span class="hljs-string">"typescript"</span>],
    <span class="hljs-attr">preserveComments</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">syntacticPlaceholders</span>: <span class="hljs-literal">true</span>
  });

  <span class="hljs-keyword">return</span> typeScriptTpl(mapping);
}

<span class="hljs-built_in">module</span>.exports = componentTemplate;
</code></pre>
<p>The template API from <code>@babel/template</code> is provided as part of the props. It is responsible for converting our code from a string format to AST format. We pass the mappings to the placeholders in our code when invoking the function returned by the template function.</p>
<p>If you've noticed, in the custom template we have an <strong>IconWrapper</strong> component that gets passed the SVG code via props. The SVG code is rendered between a span. The wrapper has the benefit of being able to modify the props' behavior in one place as opposed to keeping the logic in each generated React component.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// IconWrapper.tsx</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { IconProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> IconWrapper: React.FC&lt;{ <span class="hljs-attr">icon</span>: React.ReactNode } &amp; IconProps&gt; = <span class="hljs-function">(<span class="hljs-params">{
  icon,
  color: colorProp,
  size: sizeProp,
  autoSize,
  ...restProps
}</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> color = colorProp ? colorProp : <span class="hljs-string">"currentColor"</span>;
  <span class="hljs-keyword">const</span> size = sizeProp ? <span class="hljs-string">`<span class="hljs-subst">${sizeProp}</span>px`</span> : autoSize ? <span class="hljs-string">"1em"</span> : <span class="hljs-string">"16px"</span>;
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span>
      <span class="hljs-attr">role</span>=<span class="hljs-string">"img"</span>
      <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>
      <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">color:</span> <span class="hljs-attr">color</span>,
        <span class="hljs-attr">width:</span> <span class="hljs-attr">size</span>,
        <span class="hljs-attr">height:</span> <span class="hljs-attr">size</span>,
        <span class="hljs-attr">display:</span> "<span class="hljs-attr">inline-flex</span>",
        <span class="hljs-attr">fontSize:</span> "<span class="hljs-attr">inherit</span>"
      }}
      {<span class="hljs-attr">...restProps</span>}
    &gt;</span>
      {icon}
    <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
  );
};
</code></pre>
<h2 id="heading-make-it-pretty">🎨 Make it Pretty</h2>
<p>Once the script is ready, you can improve it by adding error handling and descriptive messages. We at Certa have also abstracted away a lot of dynamic values like the index file directory, index file name,  icons component directory, etc. into a separate config file to be able to easily configure changes.</p>
<p>If you love good design like me, you might want to use additional libraries like <a target="_blank" href="https://github.com/sindresorhus/ora">ora</a> and <a target="_blank" href="https://github.com/chalk/chalk">chalk</a> to make the developer experience awesome while waiting for the icons to be generated. Ora is what adds the cool-looking spinners to the terminal while something is loading and Chalk allows you to bring the terminal to life with colors. </p>
<p>Look how beautiful the final CLI tool looks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641374813331/TFUQ-k6eL.gif" alt="Icon Generator Demo GIF" /></p>
<h2 id="heading-final-words">👋 Final Words</h2>
<p>It seems like a lot of work to get the script set up, but once it works, you gain complete control over your icons. Plus it saves a ton of time and makes your code more reliable.</p>
]]></content:encoded></item></channel></rss>