<?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[Borris Trendy Wiria Blog]]></title><description><![CDATA[Sharing is caring, and I want to give to the community and share my knowledge around as a remainder that I learnt from the community as well.]]></description><link>https://blog.borristw.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 17:41:23 GMT</lastBuildDate><atom:link href="https://blog.borristw.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Top 5 Opensource Git-based CMS for your NextJS Project]]></title><description><![CDATA[We will be comparing 5 Popular Git-based CMS that can be worked with your NextJS Project.

DecapCMS - Github Link

TinaCMS - Github Link

SveltiaCMS - Github Link

Outstatic - Github Link

Keystatic - Github Link


Core Features Comparison




Featur...]]></description><link>https://blog.borristw.com/top-5-opensource-git-based-cms-for-your-nextjs-project</link><guid isPermaLink="true">https://blog.borristw.com/top-5-opensource-git-based-cms-for-your-nextjs-project</guid><category><![CDATA[headless cms]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Fri, 06 Dec 2024 04:42:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733460077017/c0a220c4-bb96-4e97-b749-5dc86fa62edf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We will be comparing 5 Popular Git-based CMS that can be worked with your NextJS Project.</p>
<ul>
<li><p>DecapCMS - <a target="_blank" href="https://github.com/decaporg/decap-cms">Github Link</a></p>
</li>
<li><p>TinaCMS - <a target="_blank" href="https://github.com/tinacms/tinacms">Github Link</a></p>
</li>
<li><p>SveltiaCMS - <a target="_blank" href="https://github.com/sveltia/sveltia-cms">Github Link</a></p>
</li>
<li><p>Outstatic - <a target="_blank" href="https://github.com/avitorio/outstatic">Github Link</a></p>
</li>
<li><p>Keystatic - <a target="_blank" href="https://github.com/thinkmill/keystatic">Github Link</a></p>
</li>
</ul>
<h2 id="heading-core-features-comparison">Core Features Comparison</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>DecapCMS</td><td>TinaCMS</td><td>SveltiaCMS</td><td>KeystaticCMS</td><td>OutstaticCMS</td></tr>
</thead>
<tbody>
<tr>
<td>Framework Support</td><td>Framework agnostic</td><td>React-focused</td><td>Svelte-focused</td><td>React-focused</td><td>Next.js focused</td></tr>
<tr>
<td>Open Source</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr>
<td>Self-hosting</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr>
<td>Git Provider Support</td><td>GitHub, GitLab, Bitbucket</td><td>GitHub, GitLab</td><td>GitHub</td><td>GitHub</td><td>GitHub</td></tr>
<tr>
<td>Real-time Preview</td><td>Limited</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr>
<td>Authentication</td><td>Git providers, Custom</td><td>Git providers, Custom</td><td>GitHub</td><td>GitHub</td><td>GitHub, Auth.js</td></tr>
</tbody>
</table>
</div><h2 id="heading-technical-aspects">Technical Aspects</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Aspect</td><td>DecapCMS</td><td>TinaCMS</td><td>SveltiaCMS</td><td>KeystaticCMS</td><td>OutstaticCMS</td></tr>
</thead>
<tbody>
<tr>
<td>Setup Complexity</td><td>Low</td><td>Medium</td><td>Medium</td><td>Low</td><td>Low</td></tr>
<tr>
<td>Content Format</td><td>Markdown, YAML</td><td>Markdown, JSON</td><td>Markdown</td><td>Markdown, YAML</td><td>Markdown</td></tr>
<tr>
<td>TypeScript Support</td><td>Partial</td><td>Full</td><td>Yes</td><td>Full</td><td>Full</td></tr>
<tr>
<td>Local Development</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr>
<td>API Type</td><td>REST</td><td>GraphQL</td><td>REST</td><td>REST</td><td>REST</td></tr>
<tr>
<td>Media Handling</td><td>Built-in</td><td>Built-in</td><td>Git-based</td><td>Built-in</td><td>Built-in</td></tr>
</tbody>
</table>
</div><h2 id="heading-user-experience">User Experience</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>DecapCMS</td><td>TinaCMS</td><td>SveltiaCMS</td><td>KeystaticCMS</td><td>OutstaticCMS</td></tr>
</thead>
<tbody>
<tr>
<td>Interface Style</td><td>Traditional CMS</td><td>Visual editor</td><td>Traditional CMS</td><td>Modern minimal</td><td>Modern minimal</td></tr>
<tr>
<td>Learning Curve</td><td>Low</td><td>Medium</td><td>Medium</td><td>Low</td><td>Low</td></tr>
<tr>
<td>Content Editing</td><td>Form-based</td><td>Visual + Form</td><td>Form-based</td><td>Form-based</td><td>Form-based</td></tr>
<tr>
<td>Documentation</td><td>Extensive</td><td>Good</td><td>Basic</td><td>Good</td><td>Basic</td></tr>
<tr>
<td>Community Size</td><td>Large</td><td>Medium</td><td>Small</td><td>Growing</td><td>Small</td></tr>
</tbody>
</table>
</div><h2 id="heading-use-cases-amp-best-fits">Use Cases &amp; Best Fits</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>CMS</td><td>Best For</td><td>Not Ideal For</td></tr>
</thead>
<tbody>
<tr>
<td>DecapCMS</td><td>• Simple sites • Documentation • Multi-framework projects</td><td>• Complex workflows • Large teams</td></tr>
<tr>
<td>TinaCMS</td><td>• React projects • Visual editing needs • Complex content structures</td><td>• Non-React projects • Simple blogs</td></tr>
<tr>
<td>SveltiaCMS</td><td>• Svelte projects • Simple content management • Quick setup</td><td>• Large applications • Complex workflows</td></tr>
<tr>
<td>KeystaticCMS</td><td>• React/Next.js projects • Developer-first approach • Type-safe content</td><td>• Non-React projects • Complex visual editing</td></tr>
<tr>
<td>OutstaticCMS</td><td>• Next.js projects • Simple content needs • Quick setup</td><td>• Complex applications • Non-Next.js projects</td></tr>
</tbody>
</table>
</div><h2 id="heading-deployment-amp-hosting">Deployment &amp; Hosting</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Aspect</td><td>DecapCMS</td><td>TinaCMS</td><td>SveltiaCMS</td><td>KeystaticCMS</td><td>OutstaticCMS</td></tr>
</thead>
<tbody>
<tr>
<td>Deployment Options</td><td>Any static host</td><td>Vercel, Netlify, Any</td><td>Any static host</td><td>Vercel, Any</td><td>Vercel focused</td></tr>
<tr>
<td>Build Process</td><td>Simple</td><td>Medium</td><td>Simple</td><td>Simple</td><td>Simple</td></tr>
<tr>
<td>CI/CD Integration</td><td>Easy</td><td>Medium</td><td>Easy</td><td>Easy</td><td>Easy</td></tr>
<tr>
<td>Hosting Costs</td><td>Free</td><td>Free-Premium</td><td>Free</td><td>Free</td><td>Free</td></tr>
</tbody>
</table>
</div><h2 id="heading-unique-strengths">Unique Strengths</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>CMS</td><td>Key Advantages</td></tr>
</thead>
<tbody>
<tr>
<td>DecapCMS</td><td>• Most mature and stable • Large community • Framework agnostic • Extensive documentation</td></tr>
<tr>
<td>TinaCMS</td><td>• Rich visual editing • Strong React integration • Flexible content modeling • Good developer experience</td></tr>
<tr>
<td>SveltiaCMS</td><td>• Svelte-native experience • Lightweight • Simple setup • Good performance</td></tr>
<tr>
<td>KeystaticCMS</td><td>• Type-safe content • Modern DX • Good React integration • Local-first approach</td></tr>
<tr>
<td>OutstaticCMS</td><td>• Next.js optimized • Simple setup • Good Auth.js integration • Modern interface</td></tr>
</tbody>
</table>
</div><h2 id="heading-recent-updates-amp-development">Recent Updates &amp; Development</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>CMS</td><td>Activity Level</td><td>Latest Features</td><td>Future Roadmap</td></tr>
</thead>
<tbody>
<tr>
<td>DecapCMS</td><td>Active</td><td>• Improved UI • Better TypeScript support</td><td>Stable maintenance</td></tr>
<tr>
<td>TinaCMS</td><td>Very Active</td><td>• Visual editing improvements • Better MDX support</td><td>Rich editing features</td></tr>
<tr>
<td>SveltiaCMS</td><td>Moderate</td><td>• SvelteKit support • Improved auth</td><td>Growing feature set</td></tr>
<tr>
<td>KeystaticCMS</td><td>Very Active</td><td>• Enhanced type safety • Better previews</td><td>Expanding ecosystem</td></tr>
<tr>
<td>OutstaticCMS</td><td>Active</td><td>• App router support • Enhanced UI</td><td>Next.js features</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[New build tool just come out, 6x faster than Vite, 20x faster than Webpack, built on top of Rust]]></title><description><![CDATA[There is a new build tool in the town, and it comes out as faster as many popular options in the production like Vite and Webpack.
This tool is called Farm and it currently has 3.4k stars in their Github. Its creator is brightwu（吴明亮） worked in ByteDa...]]></description><link>https://blog.borristw.com/new-build-tool-just-come-out-6x-faster-than-vite-20x-faster-than-webpack-built-on-top-of-rust</link><guid isPermaLink="true">https://blog.borristw.com/new-build-tool-just-come-out-6x-faster-than-vite-20x-faster-than-webpack-built-on-top-of-rust</guid><category><![CDATA[vite]]></category><category><![CDATA[news]]></category><category><![CDATA[webpack]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Build tool]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Tue, 25 Jun 2024 07:06:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719299091352/df5255f0-96d7-4655-9419-6c3ec3ee913d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is a new build tool in the town, and it comes out as faster as many popular options in the production like <code>Vite</code> and <code>Webpack</code>.</p>
<p>This tool is called <a target="_blank" href="https://www.farmfe.org/">Farm</a> and it currently has <strong>3.4k</strong> stars in their <a target="_blank" href="https://github.com/farm-fe/farm">Github</a>. Its creator is <a target="_blank" href="https://github.com/wre232114">brightwu（吴明亮）</a> worked in ByteDance.</p>
<p>The first commit of the Github was from <a target="_blank" href="https://github.com/farm-fe/farm/commit/9f6842a4a5f5a400a65090f1dea6e4e08d503f07">2 years ago</a> - however I believe the Design and architecture thinking must be longer than that.</p>
<h2 id="heading-what-is-farm">What is Farm?</h2>
<p>So what is Farm? Quoting from their own intro in their Github readme.</p>
<blockquote>
<p>Farm is a extremely fast vite-compatible web-building tool written in Rust. It's designed to be fast, powerful and consistent, aims to provide best experience for web development, which is the real next generation build tool.</p>
</blockquote>
<h3 id="heading-performance-benchmark-comparison">Performance Benchmark Comparison</h3>
<p>The Stable version 1.0 has been released <a target="_blank" href="https://github.com/farm-fe/farm/releases/tag/v1.0">few weeks now</a>. On top of Rust, they used a benchmark from Turborepo which is to render 1000 React components. and managed to be faster in different timings like Cold Start, Hot Start, HMR, Build Time, etc.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><strong>Startup</strong></td><td><strong>HMR (Root)</strong></td><td><strong>HMR (Leaf)</strong></td><td><strong>Production Build</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Webpack</td><td>8035ms</td><td>345ms</td><td>265ms</td><td>11321ms</td></tr>
<tr>
<td>Vite</td><td>3078ms</td><td>35ms</td><td>18ms</td><td>2266ms</td></tr>
<tr>
<td>Turbopack</td><td>3731ms</td><td>62ms</td><td>54ms</td><td>6442ms</td></tr>
<tr>
<td>Rspack</td><td>831ms</td><td>104ms</td><td>96ms</td><td>724ms</td></tr>
<tr>
<td>Farm</td><td>403ms</td><td>11ms</td><td>10ms</td><td>288ms</td></tr>
</tbody>
</table>
</div><p><img src="https://github.com/farm-fe/performance-compare/raw/main/full.png" alt="xx" /></p>
<blockquote>
<p>Check this for more info <a target="_blank" href="https://github.com/farm-fe/performance-compare">https://github.com/farm-fe/performance-compare</a> on the comparison and benchmark performance.</p>
</blockquote>
<h3 id="heading-other-exciting-features">Other Exciting Features</h3>
<p>It is mentioned in their release docs, that Farm is compatible with Vite ecosystem / plugins and not only that it has other features like</p>
<ul>
<li><p>Basic compilation capabilities</p>
</li>
<li><p>Lazy compilation</p>
</li>
<li><p>Incremental Building: Module-level disk persistent cache supported, unchanged modules won't be compiled twice, hot start time is reduced by <strong>80%</strong>, with lazy compilation enabled, preview projects of any size in <strong>1s</strong></p>
</li>
<li><p>Production optimization / Browser compatibility</p>
</li>
<li><p>Partial Bundling: Bundle thousands of modules into about 20-30 output files according to dependency ship. Perfectly avoiding the two extreme modes of <code>bundle</code> and <code>bundless</code> and can ensure loading performance while improving cache reuse rate.</p>
</li>
<li><p>Plugin Ecosystem</p>
</li>
<li><p>SSR Support</p>
</li>
</ul>
<h3 id="heading-future-features-roadmaps">Future Features / Roadmaps</h3>
<ul>
<li><p>Continue to build more top-level frameworks, such as SSR framework. (NextJS/ NuxtJS/ etc)</p>
</li>
<li><p>Extend the Rust plugin ecosystem, use Rust to implement or reconstruct existing common tools, and provide a more extreme performance experience</p>
</li>
</ul>
<h2 id="heading-my-thoughts-on-farm">My Thoughts on Farm</h2>
<p>New technology that comes out every now and then is always great and disrupting the development ecosystem like how even Vite comes out itself because Webpack is slow. I really enjoy how Vite makes it faster for new projects as well compared to 5 years ago where everything will take longer to start and so on (HMR will take 2 - 5 secs in big projects).</p>
<p>With this coming who knows, Vite and other tools may take inspiration from it and grow each other as better tools for the ecosystem.</p>
<p>As Farm is still new to the ecosystem, there soon will be issues and reports for many different testing environment from many people like different devices, OS, etc. Who knows Farm can handle all of those like Vite and the rest do yet. As of the time writing this article Farm has only &lt; 100 issues in their Github.</p>
<p>They have tested Farm with many options as well like Electron and Tauri which is cross platform tools which will definitely speed things up in their build time with Rust multithread features.</p>
<p>I hope for the best for the ecosystem to keep pushing each other to get to a better performance, stability, and scalability throughout the ecosystem.</p>
<h2 id="heading-quick-start-try-it-yourself">Quick Start (Try it yourself)</h2>
<p>Farm provides official templates to quickly create a React, Vue, Solid, Svelte, etc. project:</p>
<pre><code class="lang-plaintext">pnpm create farm
</code></pre>
<p>Then select the type of project, features, etc. to be created. After the project is created, start the project:</p>
<pre><code class="lang-plaintext">pnpm start
</code></pre>
<p>You can create and start your first Farm project in 20 seconds!</p>
<hr />
<p>Let me know your thoughts on the comment below whether Farm will succeed or not and what is your reasoning!</p>
]]></content:encoded></item><item><title><![CDATA[Dynamic Load Dialog Component with a Reusable Trigger and Loading Tricks in NextJS]]></title><description><![CDATA[Recently, I got a task to do a wizard form dialog component that is being used in multiple places in a NextJS project - There are few ways we could approach this. However, I find this trick really fun to be done in a way.
To use this trick I installe...]]></description><link>https://blog.borristw.com/dynamic-load-dialog-component-with-a-reusable-trigger-and-loading-tricks-in-nextjs</link><guid isPermaLink="true">https://blog.borristw.com/dynamic-load-dialog-component-with-a-reusable-trigger-and-loading-tricks-in-nextjs</guid><category><![CDATA[Next.js]]></category><category><![CDATA[SEO]]></category><category><![CDATA[React]]></category><category><![CDATA[tricks]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Mon, 27 May 2024 12:19:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/9614865475709497677464dfd90c18f3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I got a task to do a wizard form dialog component that is being used in multiple places in a NextJS project - There are few ways we could approach this. However, I find this trick really fun to be done in a way.</p>
<p>To use this trick I installed <code>zustand</code> for global state management - can be used with other global state management libraries as well.</p>
<p>I know the importance of the First Time Load in SEO sense, so usually we will have to dynamic load the Dialog component where it is not visible to the first page load hence reducing the Dialog component from being downloaded. In NextJS this can be dynamic loaded by using <code>next/dynamic</code> built-in from NextJS.</p>
<h3 id="heading-getting-started">Getting Started</h3>
<p>For a starter, we will use NextJS with ShadcnUI.</p>
<p>First, we make our Dialog component that uses ShadcnUI Dialog component and the inside can be anything for now - usually Wizard components has quite few files inside it for different forms for each steps - to make it succinct we are skipping that part.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/wizard-dialog/dialog.tsx</span>

<span class="hljs-keyword">import</span> { Dialog, DialogContent } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/dialog'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> WizardDialog = ({
  open,
  setOpen
}: {
  open: <span class="hljs-built_in">boolean</span>;
  setOpen: <span class="hljs-function">(<span class="hljs-params">by: <span class="hljs-built_in">boolean</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>;
}) =&gt; {
  <span class="hljs-keyword">return</span> (
    &lt;Dialog open={open} onOpenChange={setOpen}&gt;
      &lt;DialogContent&gt;
        {<span class="hljs-comment">/* Pretty Big Content that needs to be dynamic loaded */</span>}
      &lt;/DialogContent&gt;
    &lt;/Dialog&gt;
  )
}
</code></pre>
<p>After having the dialog component, we need to setup our global state management store for storing the state of the Dialog, like its open state and loading state - this can be custom to our needs - for now we will go with the basic setting.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/wizard-dialog/store.ts</span>

<span class="hljs-keyword">import</span> { create } <span class="hljs-keyword">from</span> <span class="hljs-string">'zustand'</span>

<span class="hljs-keyword">interface</span> WizardDialogState {
  open: <span class="hljs-built_in">boolean</span>
  setOpen: <span class="hljs-function">(<span class="hljs-params">by: <span class="hljs-built_in">boolean</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>
  isLoading: <span class="hljs-built_in">boolean</span>
  setIsLoading: <span class="hljs-function">(<span class="hljs-params">by: <span class="hljs-built_in">boolean</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useWizardDialog = create&lt;WizardDialogState&gt;()(<span class="hljs-function">(<span class="hljs-params">set</span>) =&gt;</span> ({
  open: <span class="hljs-literal">false</span>,
  setOpen: <span class="hljs-function">(<span class="hljs-params">by</span>) =&gt;</span> set(<span class="hljs-function">() =&gt;</span> ({ open: by })),
  isLoading: <span class="hljs-literal">false</span>,
  setIsLoading: <span class="hljs-function">(<span class="hljs-params">by</span>) =&gt;</span> set(<span class="hljs-function">() =&gt;</span> ({ isLoading: by })),
}))
</code></pre>
<p>Having the store and the dialog component now, we can create our dynamic loaded functionality we will call it <code>consumer.tsx</code> - which we will put it in the <code>app/layout.tsx</code> for global usage.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/wizard-dialog/consumer.tsx</span>

<span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { useWizardDialog } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/wizard-dialog/store'</span>
<span class="hljs-keyword">import</span> dynamic <span class="hljs-keyword">from</span> <span class="hljs-string">'next/dynamic'</span>
<span class="hljs-keyword">import</span> { useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">const</span> WizardDialogLoading = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> setIsLoading = useWizardDialog(<span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.setIsLoading)

  useEffect(<span class="hljs-function">() =&gt;</span> {
    setIsLoading(<span class="hljs-literal">true</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      setIsLoading(<span class="hljs-literal">false</span>)
    }
  }, [])

  <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
}

<span class="hljs-keyword">const</span> WizardDialog = dynamic(
  <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">'@/app/wizard-dialog/dialog'</span>).then(<span class="hljs-function">(<span class="hljs-params">mod</span>) =&gt;</span> mod.WizardDialog),
  {
    ssr: <span class="hljs-literal">false</span>,
    loading: WizardDialogLoading,
  }
)

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> WizardDialogConsumer = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> open = useWizardDialog(<span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.open)
  <span class="hljs-keyword">const</span> setOpen = useWizardDialog(<span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.setOpen)
  <span class="hljs-keyword">return</span> open &amp;&amp; &lt;WizardDialog open={open} setOpen={setOpen} /&gt;
}
</code></pre>
<p>A bit explanation here, we use the open state from the store to indicate that when it is true, it will start the dynamic load the <code>&lt;WizardDialog /&gt;</code> component.</p>
<p>After creating a the consumer here we can put it inside the <code>app/layout.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/layout.tsx</span>

<span class="hljs-keyword">import</span> { Inter } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/font/google'</span>
<span class="hljs-keyword">import</span> { WizardDialogConsumer } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/wizard-dialog/consumer'</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">'./globals.css'</span>

<span class="hljs-keyword">const</span> inter = Inter({ subsets: [<span class="hljs-string">'latin'</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">RootLayout</span>(<span class="hljs-params">{
  children,
}: Readonly&lt;{
  children: React.ReactNode
}&gt;</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;html lang=<span class="hljs-string">'en'</span>&gt;
      &lt;body className={inter.className}&gt;
        &lt;WizardDialogConsumer /&gt;
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  )
}
</code></pre>
<p><code>WizardDialogLoading</code> is pretty interesting trick here to manipulate our global state for our loading state. When the component is being first loaded, it will set loading to true and when it is mounted it will make our loading state to be false - with this logic, we can use globally the loading state later in our trigger to render any global loading component.</p>
<p>Below is the code for our <code>trigger.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/wizard-dialog/trigger.tsx</span>

<span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { useWizardDialog } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/wizard-dialog/store'</span>
<span class="hljs-keyword">import</span> { cn } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/utils'</span>
<span class="hljs-keyword">import</span> { Loader2 } <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> WizardDialogTrigger = <span class="hljs-function">(<span class="hljs-params">{
  children,
}: {
  children: React.ReactElement
}</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> setOpen = useWizardDialog(<span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.setOpen)
  <span class="hljs-keyword">const</span> isLoading = useWizardDialog(<span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> s.isLoading)

  <span class="hljs-keyword">const</span> onClick = <span class="hljs-function">() =&gt;</span> {
    setOpen(<span class="hljs-literal">true</span>)
  }

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      {children &amp;&amp;
        React.cloneElement(children, {
          onClick,
          className: cn(
            children?.props?.className,
            isLoading &amp;&amp; <span class="hljs-string">'flex flex-items-center justify-center'</span>
          ),
          children: isLoading ? (
            &lt;Loader2 className=<span class="hljs-string">'animate-spin text-current'</span> /&gt;
          ) : (
            children?.props?.children
          ),
        })}
    &lt;/&gt;
  )
}
</code></pre>
<p>A bit explanation here, we are using <code>React.cloneElement</code> to our children component to modify its <code>onClick</code> function - so that we can globally let whichever component children of this trigger would be clickable and changing the state to be true.</p>
<p>And we can use the trigger like so - just wrap any clickable component with our custom trigger component.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/page.tsx</span>

<span class="hljs-keyword">import</span> { WizardDialogTrigger } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/wizard-dialog/trigger'</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">Page</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;WizardDialogTrigger&gt;
        &lt;button&gt;Take Survey&lt;/button&gt;
      &lt;/WizardDialogTrigger&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<h3 id="heading-learnings">Learnings</h3>
<p>We have done making our Dialog component dynamically loaded into the our NextJS app to ensure it will not block the first page timing of our page.</p>
<p>This is only one way of doing it - hope it can be a foundation of what next to be implemented in the future enhancements.</p>
<p>Further enhancement can be like dividing the loading state into multiple keys states instead of one global loading state. <code>React.cloneElement</code> is easy to get pitfall from the React documentation - we can use alternative like pass the children into a prop.</p>
<hr />
<p>I have uploaded a sample repo into GitHub as well incase needed.</p>
<p><a target="_blank" href="https://github.com/linkb15/blog-dynamic-load-dialog">https://github.com/linkb15/blog-dynamic-load-dialog</a></p>
<p>Cheers!</p>
]]></content:encoded></item><item><title><![CDATA[Confirm Password using schema Zod with React Hook Form]]></title><description><![CDATA[initialize the Next.js project using this command below.
pnpm create next-app@latest my-app --typescript --tailwind --eslint

and fill up with the prompt with default answer like so.
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to...]]></description><link>https://blog.borristw.com/confirm-password-using-schema-zod-with-react-hook-form</link><guid isPermaLink="true">https://blog.borristw.com/confirm-password-using-schema-zod-with-react-hook-form</guid><category><![CDATA[React]]></category><category><![CDATA[forms]]></category><category><![CDATA[zod]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Sat, 25 May 2024 05:44:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716615865963/c4138088-1bd6-44b6-9e79-a87fc8ff503c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>initialize the Next.js project using this command below.</p>
<pre><code class="lang-bash">pnpm create next-app@latest my-app --typescript --tailwind --eslint
</code></pre>
<p>and fill up with the prompt with default answer like so.</p>
<pre><code class="lang-bash">✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import <span class="hljs-built_in">alias</span> (@/*)? … No
</code></pre>
<p>Install the Library that we need for our form and <code>zod</code> schema validation.</p>
<pre><code class="lang-bash">pnpm i react-hook-form zod @hookform/resolvers
</code></pre>
<h3 id="heading-schema">Schema</h3>
<p>For this tutorial we will be creating a simple register form which user will have fill up email, password and confirm their password. A <code>zod</code> schema would look like this</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/register-form/schema.ts</span>
<span class="hljs-keyword">import</span> z <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> registerSchema = z
  .object({
    email: z.string().email().min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">'Email is Required'</span> }),
    password: z.string().min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">'Password is Required'</span> }),
    confirmPassword: z
      .string()
      .min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">'Please confirm your password'</span> }),
  })
</code></pre>
<p>To add validation for the confirm password, there are multiple ways we can do that. In this tutorial I will use <code>.superRefine</code> from <code>zod</code> to have more freedom than normal <code>.refine</code> in defining our validation. The modified schema will look like this.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/register-form/schema.ts</span>
<span class="hljs-keyword">import</span> z <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> registerSchema = z
  .object({
    email: z.string().email().min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">'Email is Required'</span> }),
    password: z.string().min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">'Password is Required'</span> }),
    confirmPassword: z
      .string()
      .min(<span class="hljs-number">1</span>, { message: <span class="hljs-string">'Please confirm your password'</span> }),
  })
  .superRefine(<span class="hljs-function">(<span class="hljs-params">val, ctx</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (val.password !== val.confirmPassword) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: <span class="hljs-string">'Password is not the same as confirm password'</span>,
        path: [<span class="hljs-string">'confirmPassword'</span>],
      })
    }
  })

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> RegisterSchema <span class="hljs-keyword">extends</span> z.infer&lt;typeof registerSchema&gt; {}
</code></pre>
<p>Take note, since we are using typescript, we need to export the schema type as well by using <code>z.infer</code> of our schema.</p>
<h3 id="heading-register-form-component">Register form component</h3>
<p>We will use the schema with the <code>react-hook-form</code> and create a simple register form with simple styling and when we finish filling up the form, it will alert the data.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/register-form/form.tsx</span>
<span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { useForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-hook-form'</span>
<span class="hljs-keyword">import</span> { zodResolver } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hookform/resolvers/zod'</span>
<span class="hljs-keyword">import</span> { registerSchema, RegisterSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/register-form/schema'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> RegisterForm = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm&lt;RegisterSchema&gt;({
    resolver: zodResolver(registerSchema),
  })

  <span class="hljs-keyword">const</span> onSubmit = handleSubmit(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(data)
    alert(<span class="hljs-built_in">JSON</span>.stringify(data))
  })

  <span class="hljs-keyword">return</span> (
    &lt;form onSubmit={onSubmit} className=<span class="hljs-string">'space-y-3'</span>&gt;
      &lt;div className=<span class="hljs-string">'flex flex-col gap-1'</span>&gt;
        &lt;label htmlFor=<span class="hljs-string">'email'</span>&gt;Email&lt;/label&gt;
        &lt;input
          id=<span class="hljs-string">'email'</span>
          {...register(<span class="hljs-string">'email'</span>)}
          className=<span class="hljs-string">'bg-transparent border py-2 px-4'</span>
        /&gt;
        {errors.email?.message &amp;&amp; &lt;p&gt;{errors.email?.message}&lt;/p&gt;}
      &lt;/div&gt;

      &lt;div className=<span class="hljs-string">'flex flex-col gap-1'</span>&gt;
        &lt;label htmlFor=<span class="hljs-string">'email'</span>&gt;Password&lt;/label&gt;
        &lt;input
          id=<span class="hljs-string">'password'</span>
          <span class="hljs-keyword">type</span>=<span class="hljs-string">'password'</span>
          {...register(<span class="hljs-string">'password'</span>)}
          className=<span class="hljs-string">'bg-transparent border py-2 px-4'</span>
        /&gt;
        {errors.password?.message &amp;&amp; &lt;p&gt;{errors.password?.message}&lt;/p&gt;}
      &lt;/div&gt;

      &lt;div className=<span class="hljs-string">'flex flex-col gap-1'</span>&gt;
        &lt;label htmlFor=<span class="hljs-string">'confirmPassword'</span>&gt;Confirm Password&lt;/label&gt;
        &lt;input
          id=<span class="hljs-string">'confirmPassword'</span>
          <span class="hljs-keyword">type</span>=<span class="hljs-string">'password'</span>
          {...register(<span class="hljs-string">'confirmPassword'</span>)}
          className=<span class="hljs-string">'bg-transparent border py-2 px-4'</span>
        /&gt;
        {errors.confirmPassword?.message &amp;&amp; (
          &lt;p&gt;{errors.confirmPassword?.message}&lt;/p&gt;
        )}
      &lt;/div&gt;

      &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">'submit'</span> className=<span class="hljs-string">'px-4 py-2 rounded bg-gray-500'</span> /&gt;
    &lt;/form&gt;
  )
}
</code></pre>
<p>now to test this we just need to replace our page component like so.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/app/page.tsx</span>
<span class="hljs-keyword">import</span> { RegisterForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/register-form/form'</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">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;main className=<span class="hljs-string">'flex min-h-screen flex-col items-center justify-between p-24'</span>&gt;
      &lt;RegisterForm /&gt;
    &lt;/main&gt;
  )
}
</code></pre>
<p>Now we can test this code in our <code>http://localhost:3000</code> using <code>pnpm dev</code> in our terminal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716615225207/7f338c24-090a-40ec-946e-67e8be91650e.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-additional-information">Additional information</h3>
<p>Zod is powerful schema validation library that can help you type-safe your code, if you refactor your schema like changing from email to username, all your code will have an error because, you have to update them from email to username in the files that use the schema.</p>
<p><code>.superRefine</code> is a powerful functions to do a custom validation as well as doing so much things like we can call an APIs for checking email uniquely or not for the validation.</p>
<p>Cheers!</p>
]]></content:encoded></item><item><title><![CDATA[Configure next/font with Tailwind in Storybook]]></title><description><![CDATA[Next.js has font optimization option using their package next/font using google fonts and local. When adding a storybook to a Next.js project, you will start to wonder on how we can achieve the same font to be loaded into the storybook itself as well...]]></description><link>https://blog.borristw.com/configure-nextfont-with-tailwind-in-storybook</link><guid isPermaLink="true">https://blog.borristw.com/configure-nextfont-with-tailwind-in-storybook</guid><category><![CDATA[next/font]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Storybook]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Fri, 24 May 2024 13:01:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716555667578/d90ea5fb-bedb-4b7b-8514-a98774eef98f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Next.js has font optimization option using their package <code>next/font</code> using google fonts and local. When adding a storybook to a Next.js project, you will start to wonder on how we can achieve the same font to be loaded into the storybook itself as well.</p>
<h3 id="heading-to-solve-this">To solve this</h3>
<p>We can reuse <code>next/font</code> that we use in the <code>app/layout.tsx</code> in our storybook configuration. To do this, we need to create another file and export the font from there to be used in our <code>app/layout.tsx</code> and our storybook configuration.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app/font.ts</span>

<span class="hljs-comment">// Pick your font family that you want</span>
<span class="hljs-keyword">import</span> { Mukta, PT_Serif } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/font/google'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fontSerif = PT_Serif({
  subsets: [<span class="hljs-string">'latin'</span>],
  weight: [<span class="hljs-string">'400'</span>, <span class="hljs-string">'700'</span>],
  variable: <span class="hljs-string">'--font-serif'</span>,
  display: <span class="hljs-string">'swap'</span>
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fontSans = Mukta({
  subsets: [<span class="hljs-string">'latin'</span>],
  weight: [<span class="hljs-string">'300'</span>, <span class="hljs-string">'400'</span>, <span class="hljs-string">'500'</span>, <span class="hljs-string">'700'</span>],
  variable: <span class="hljs-string">'--font-sans'</span>,
  display: <span class="hljs-string">'swap'</span>
});
</code></pre>
<p>In the <code>.storybook/preview.tsx</code> , we just need to create a decorator that will inject our variable class using <code>useEffect</code> so that it is applied in the body, not just in the iframe of our story.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// .storybook/preview.tsx</span>
<span class="hljs-keyword">import</span> { fontSans, fontSerif } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/app/font'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Preview } <span class="hljs-keyword">from</span> <span class="hljs-string">'@storybook/react'</span>;

<span class="hljs-keyword">const</span> preview = {
  decorators: [
    <span class="hljs-function">(<span class="hljs-params">Story</span>) =&gt;</span> {
      useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">document</span>.body.classList.add(
          fontSans.variable,
          fontSerif.variable,
          <span class="hljs-string">'font-sans'</span>,
          <span class="hljs-string">'antialiased'</span>
        );
      }, []);

      <span class="hljs-keyword">return</span> (
        &lt;TooltipProvider&gt;
          &lt;Story /&gt;
        &lt;/TooltipProvider&gt;
      );
    }
  ],
} satisfies Preview;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> preview;
</code></pre>
<p>Why do we do this? The reason is if we do add the font class in a <code>div</code> and have the <code>&lt;Story /&gt;</code> as the children - it will not work for <code>Dialog</code> / <code>Modal</code> component that injects itself in a <code>Portal</code> as sibling of the storybook iframe - hence our font will not be applied.</p>
<p>Since <code>Shadcn UI</code> has been mostly a popular option for many frontend developers to be used and their <code>Dialog</code> component is using <code>Radix UI</code> which is injecting element into the <code>Portal</code> as sibling of the iframe.</p>
<p>Now after adding this decorator, you should see the body has the class added as well as the font updated.</p>
<p>Cheers!</p>
]]></content:encoded></item><item><title><![CDATA[Bun: Testing Storybook ShadcnUI with Vite React]]></title><description><![CDATA[Continuing blog, for Bun from the last post; Bun.sh with React, Typescript, TailwindCSS and Storybook
In this blog, we will explore the basics of Storybook testing with its runner.
Getting Started
We will be following this installation documentation ...]]></description><link>https://blog.borristw.com/bun-testing-storybook-shadcnui-with-vite-react</link><guid isPermaLink="true">https://blog.borristw.com/bun-testing-storybook-shadcnui-with-vite-react</guid><category><![CDATA[2Articles1Week]]></category><category><![CDATA[Bun]]></category><category><![CDATA[Storybook]]></category><category><![CDATA[React]]></category><category><![CDATA[shadcn]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Tue, 26 Sep 2023 03:26:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695698788801/63ae4f63-8d6b-4901-8a66-0f1792e2ea47.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Continuing blog, for Bun from the last post; <a target="_blank" href="https://linkb.hashnode.dev/bunsh-with-react-typescript-tailwindcss-and-storybook">Bun.sh with React, Typescript, TailwindCSS and Storybook</a></p>
<p>In this blog, we will explore the basics of Storybook testing with its runner.</p>
<h3 id="heading-getting-started">Getting Started</h3>
<p>We will be following this installation documentation from <a target="_blank" href="https://storybook.js.org/docs/react/writing-tests/test-runner">Storybook Test Runner</a></p>
<p>Start by installing the add-on</p>
<pre><code class="lang-bash">bun add -d @storybook/test-runner
</code></pre>
<p>and add new scripts to the <code>package.json</code></p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"test-storybook"</span>: <span class="hljs-string">"test-storybook"</span>
}
</code></pre>
<p>Since the underlying of this package is using Jest and Playwright - we need to run our code first before running the test.</p>
<p>Run the storybook first in the first terminal</p>
<pre><code class="lang-bash">bun storybook
</code></pre>
<p>Run the test on the second terminal</p>
<pre><code class="lang-bash">bun test-storybook
</code></pre>
<p>Running this command directly will be a smoke test for every story in the storybook. This is useful to check if there are any errors in mounting or rendering the components.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695693656723/e3adc8b5-35b9-4d0f-841d-4843b8e2a29b.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-adding-form-for-interaction">Adding Form for Interaction</h3>
<p>In this example, we will add a Form component using the <a target="_blank" href="https://ui.shadcn.com/docs/components/form">ShadcnUI Form</a> and Input component.</p>
<pre><code class="lang-bash">bunx shadcn-ui@latest add form input
</code></pre>
<p>For illustration purposes, we will just use the example from the ShadcnUI Profile Form component. Add this ProfileForm component to the <code>components</code> folder.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// components/ProfileForm.tsx</span>
<span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { zodResolver } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hookform/resolvers/zod'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> z <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>

<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/button'</span>
<span class="hljs-keyword">import</span> {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/form'</span>
<span class="hljs-keyword">import</span> { Input } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/input'</span>
<span class="hljs-keyword">import</span> { useForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-hook-form'</span>

<span class="hljs-keyword">const</span> formSchema = z.object({
  username: z.string().min(<span class="hljs-number">2</span>, {
    message: <span class="hljs-string">'Username must be at least 2 characters.'</span>,
  }),
})

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProfileForm</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> form = useForm&lt;z.infer&lt;<span class="hljs-keyword">typeof</span> formSchema&gt;&gt;({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: <span class="hljs-string">''</span>,
    },
  })

  <span class="hljs-keyword">const</span> onSubmit = form.handleSubmit(<span class="hljs-function">(<span class="hljs-params">values</span>) =&gt;</span> {
    <span class="hljs-comment">// Do something with the form values.</span>
    <span class="hljs-comment">// ✅ This will be type-safe and validated.</span>
    <span class="hljs-built_in">console</span>.log(values)
  })

  <span class="hljs-keyword">return</span> (
    &lt;Form {...form}&gt;
      &lt;form onSubmit={onSubmit} className=<span class="hljs-string">'space-y-8'</span>&gt;
        &lt;FormField
          control={form.control}
          name=<span class="hljs-string">'username'</span>
          render={<span class="hljs-function">(<span class="hljs-params">{ field }</span>) =&gt;</span> (
            &lt;FormItem&gt;
              &lt;FormLabel&gt;Username&lt;/FormLabel&gt;
              &lt;FormControl&gt;
                &lt;Input placeholder=<span class="hljs-string">'shadcn'</span> {...field} /&gt;
              &lt;/FormControl&gt;
              &lt;FormDescription&gt;
                This is your <span class="hljs-keyword">public</span> display name.
              &lt;/FormDescription&gt;
              &lt;FormMessage /&gt;
            &lt;/FormItem&gt;
          )}
        /&gt;
        &lt;Button <span class="hljs-keyword">type</span>=<span class="hljs-string">'submit'</span>&gt;Submit&lt;/Button&gt;
      &lt;/form&gt;

      {form.formState.isSubmitted &amp;&amp; (
        &lt;div className=<span class="hljs-string">'p-4 bg-green-100 rounded-md'</span>&gt;
          &lt;p className=<span class="hljs-string">'text-green-800'</span>&gt;
            Your profile has been updated successfully.
          &lt;/p&gt;
        &lt;/div&gt;
      )}
    &lt;/Form&gt;
  )
}
</code></pre>
<p>After we have created the <code>ProfileForm</code> component, we can create a new story in the Storybook.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// stories/ProfileForm.stories.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Meta, StoryObj } <span class="hljs-keyword">from</span> <span class="hljs-string">'@storybook/react'</span>

<span class="hljs-keyword">import</span> { ProfileForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ProfileForm'</span>

<span class="hljs-keyword">const</span> meta = {
  title: <span class="hljs-string">'Example/ProfileForm'</span>,
  component: ProfileForm,
  parameters: {
    layout: <span class="hljs-string">'centered'</span>,
  },
  tags: [<span class="hljs-string">'autodocs'</span>],
  argTypes: {},
} satisfies Meta&lt;<span class="hljs-keyword">typeof</span> ProfileForm&gt;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> meta
<span class="hljs-keyword">type</span> Story = StoryObj&lt;<span class="hljs-keyword">typeof</span> meta&gt;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Primary: Story = {
  args: {},
}
</code></pre>
<p>We can check this in the Storybook it will show like below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695694678387/0916dc35-057d-412a-a214-a861c22177ce.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-interactive-testing-in-storybook">Interactive Testing in Storybook</h3>
<p>For the interaction test, we will follow this <a target="_blank" href="https://storybook.js.org/docs/react/writing-tests/interaction-testing">guideline</a> from Storybook documentation.</p>
<p>Install the library needed for this test.</p>
<pre><code class="lang-json">bun add -d @storybook/testing-library @storybook/jest @storybook/addon-interactions
</code></pre>
<p>Now comes the interesting part, writing your user interaction test code. Create a new story for the Filled form.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Filled: Story = {
  play: <span class="hljs-keyword">async</span> ({ canvasElement }) =&gt; {
    <span class="hljs-keyword">const</span> canvas = within(canvasElement)

    <span class="hljs-comment">// 👇 Simulate interactions with the component</span>
    <span class="hljs-keyword">await</span> userEvent.type(canvas.getByTestId(<span class="hljs-string">'email'</span>), <span class="hljs-string">'email@provider.com'</span>)

    <span class="hljs-keyword">await</span> userEvent.type(canvas.getByTestId(<span class="hljs-string">'password'</span>), <span class="hljs-string">'a-random-password'</span>)

    <span class="hljs-comment">// See https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel</span>
    <span class="hljs-keyword">await</span> userEvent.click(canvas.getByRole(<span class="hljs-string">'button'</span>))

    <span class="hljs-comment">// 👇 Assert DOM structure</span>
    <span class="hljs-keyword">await</span> expect(
      canvas.getByText(
        <span class="hljs-string">'Everything is perfect. Your account is ready and we should probably get you started!'</span>
      )
    ).toBeInTheDocument()
  },
}
</code></pre>
<p>add the import lines as well</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { userEvent, within } <span class="hljs-keyword">from</span> <span class="hljs-string">'@storybook/testing-library'</span>
<span class="hljs-keyword">import</span> { expect } <span class="hljs-keyword">from</span> <span class="hljs-string">'@storybook/jest'</span>
</code></pre>
<p>I faced a type error while writing the test.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695695915651/f6bb1b1f-d1cb-4e08-a6dc-07dede4849f0.png" alt class="image--center mx-auto" /></p>
<p>hence add this to the <code>compilerOptions</code> in the <code>tsconfig.json</code></p>
<pre><code class="lang-json"><span class="hljs-string">"types"</span>: [<span class="hljs-string">"@testing-library/jest-dom"</span>]
</code></pre>
<p>Running again the storybook and checking the Filled story of ProfileForm, there will be a Pass test in the interaction tab.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695696588799/2f25985f-3369-4bbc-a168-05959be2acdd.png" alt class="image--center mx-auto" /></p>
<p>With this, we can even move back to the previous state and play the interaction on our own in the Storybook UI.</p>
<p>We can test again the test-storybook command and see the results of this new interaction test in the test runner.</p>
<pre><code class="lang-typescript">bun test-storybook --verbose
</code></pre>
<p>Running in verbose mode <code>--verbose</code>, so we can see that the Filled story is different than the rest of the stories here which is "play-test" instead of "smoke-test".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695696726121/a2791534-68d6-437d-b51a-cf9c40fccba5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-final-thoughts">Final Thoughts</h3>
<p>Testing is a huge subject to cover - we only cover a small portion of it.</p>
<p>After going through this post, testing with Bun and Storybook in Vite React App seems working fine.</p>
<hr />
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing this post, we are mostly still running with NodeJS to execute the command.</div>
</div>

<p>Following <a target="_blank" href="https://bun.sh/docs/cli/run#bun">this documentation</a>, we can try to run the Storybook in bun runtime as well like so</p>
<pre><code class="lang-typescript">bun --bun storybook
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695697955074/3b49dd07-654c-4b5e-aaac-5b14bc3af27c.png" alt class="image--center mx-auto" /></p>
<p>However, it is still a <a target="_blank" href="https://github.com/oven-sh/bun/issues/3794">tracked issue</a> currently in the Bun Repository - therefore we shall await more stability coming into the Bun runtime in the coming weeks.</p>
]]></content:encoded></item><item><title><![CDATA[Bun.sh with React, Typescript, TailwindCSS and Storybook]]></title><description><![CDATA[After the release version of Bun 1.0 last few weeks, it has moved the industry benchmark in the javascript world almost as fast as other popular programming languages that are widely known to be fast (like Go and Rust) which is proof that the major f...]]></description><link>https://blog.borristw.com/bunsh-with-react-typescript-tailwindcss-and-storybook</link><guid isPermaLink="true">https://blog.borristw.com/bunsh-with-react-typescript-tailwindcss-and-storybook</guid><category><![CDATA[Bun]]></category><category><![CDATA[React]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Storybook]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Sat, 23 Sep 2023 23:39:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695512662478/37fae14c-78a5-4034-93dc-e0a0c09e440d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After the release <a target="_blank" href="https://bun.sh/blog/bun-v1.0">version of Bun 1.0</a> last few weeks, it has moved the industry benchmark in the javascript world almost as fast as other popular programming languages that are widely known to be fast (like Go and Rust) which is proof that the major focus towards improving performance is still in demand and the potential have not yet reached its limits.</p>
<p>The tutorial will cover installing Bun and creating a React project with Typescript, and Vite and the styling will be using TailwindCSS, ShadcnUI and Storybook.</p>
<p>This tutorial will be using Mac since the stable version of Bun is only for Mac currently, the Windows version is said to be experimental.</p>
<h3 id="heading-getting-started">Getting Started</h3>
<p>Install Bun with the following command from the Bun website.</p>
<pre><code class="lang-bash">curl -fsSL https://bun.sh/install | bash
</code></pre>
<p>After installing, we create a react project with the following command</p>
<pre><code class="lang-bash">bun create vite
</code></pre>
<p>The project name is left default as <code>vite-project</code>, and React as the framework with Typescript turned on.</p>
<p>Boom 💥 the installation process did not even more than 10 seconds.</p>
<p>The development server can be started by</p>
<pre><code class="lang-bash">bun dev
</code></pre>
<p>and it should be running in your <a target="_blank" href="http://localhost:5173/">http://localhost:5173/</a></p>
<h3 id="heading-adding-tailwindcss">Adding TailwindCSS</h3>
<p>Since we are using the Vite app, we will follow the installation for <a target="_blank" href="https://tailwindcss.com/docs/guides/vite">Tailwind with Vite</a>. To install the TailwindCSS library, the Bun command is similar to how Yarn or NPM is used.</p>
<pre><code class="lang-bash">bun add -d tailwindcss postcss autoprefixer
</code></pre>
<p>following the TailwindCSS tutorial we need to init the configuration.</p>
<pre><code class="lang-typescript">bunx tailwindcss init -p
<span class="hljs-comment">// or </span>
bun run tailwindcss init -p
</code></pre>
<p>On NPM we have npx, in Bun, we have bunx to help initialize the module right away.</p>
<p>The rest of the steps are supposed the same as the ones from the guide from TailwindCSS documentation.</p>
<p>edit <code>content</code> inside <code>tailwind.config.js</code></p>
<pre><code class="lang-typescript"><span class="hljs-comment">/** @type {import('tailwindcss').Config} */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  content: [<span class="hljs-string">'./index.html'</span>, <span class="hljs-string">'./src/**/*.{js,ts,jsx,tsx}'</span>],
  theme: {
    extend: {},
  },
  plugins: [],
}
</code></pre>
<p>replace the content of your <code>index.css</code> with the TailwindCSS directives</p>
<pre><code class="lang-css"><span class="hljs-comment">/* index.css */</span>
<span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<h3 id="heading-adding-shadcnui">Adding ShadcnUI</h3>
<p><a target="_blank" href="https://ui.shadcn.com/">ShadcnUI</a> has been a great help in improving the adoption of best practices in styling the whole React projects in the industry.</p>
<p>We will follow <a target="_blank" href="https://ui.shadcn.com/docs/installation/vite">this guide</a> from the website documentation.</p>
<p>Since the first two are the same as TailwindCSS documentation, we will skip that and straight towards the <em>Absolute Path</em> configuration in Vite.</p>
<p>Add this part of the code inside <code>compilerOptions</code> object</p>
<pre><code class="lang-json"><span class="hljs-string">"baseUrl"</span>: <span class="hljs-string">"."</span>,
<span class="hljs-string">"paths"</span>: {
  <span class="hljs-attr">"@/*"</span>: [<span class="hljs-string">"./src/*"</span>]
}
</code></pre>
<p>Add <code>@types/node</code> for type safety for importing from the path module.</p>
<pre><code class="lang-bash">bun add -d @types/node
</code></pre>
<p>And change the vite.config.ts to understand the @ sign as well as an alias of the "src" folder.</p>
<pre><code class="lang-typescript"><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> react <span class="hljs-keyword">from</span> <span class="hljs-string">'@vitejs/plugin-react'</span>
<span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'vite'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      <span class="hljs-string">'@'</span>: path.resolve(__dirname, <span class="hljs-string">'./src'</span>),
    },
  },
})
</code></pre>
<p>Now we can run the init command from ShadcnUI</p>
<pre><code class="lang-bash">bunx shadcn-ui@latest init
</code></pre>
<p>Pick Typescript and Default style then Slate as base color when asked. For the global CSS, src/index.css - and the rest of the questions mostly we will use the default answer for it.</p>
<pre><code class="lang-bash">✔ Would you like to use TypeScript (recommended)? … no / yes
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Where is your global CSS file? … src/index.css
✔ Would you like to use CSS variables <span class="hljs-keyword">for</span> colors? … no / yes
✔ Where is your tailwind.config.js located? … tailwind.config.js
✔ Configure the import <span class="hljs-built_in">alias</span> <span class="hljs-keyword">for</span> components: … @/components
✔ Configure the import <span class="hljs-built_in">alias</span> <span class="hljs-keyword">for</span> utils: … @/lib/utils
✔ Are you using React Server Components? … no / yes
✔ Write configuration to components.json. Proceed? … yes
</code></pre>
<p>After that is set up we can try to add a Button component from the library.</p>
<pre><code class="lang-bash">bunx shadcn-ui@latest add button
</code></pre>
<p>That's it! initializing ShadcnUI and adding more components - it is advised to explore their documentation as needed.</p>
<h3 id="heading-adding-storybook">Adding Storybook</h3>
<p>We will be following the <a target="_blank" href="https://storybook.js.org/docs/react/get-started/install">Storybook documentation</a> for this.</p>
<pre><code class="lang-bash">bunx storybook@latest init --skip-install
bun i
</code></pre>
<p>We are skipping the installation since Bun is new hence they are not detected by the Storybook CLI and by default it will use NPM.</p>
<p>After that, we will have <code>.storybook</code> folder and <code>stories</code> folder added to the root folder.</p>
<p>Run the Storybook and follow their Tour.</p>
<pre><code class="lang-typescript">bun storybook
</code></pre>
<p>Let's update the Story in the <code>stories/Button.stories.ts</code> to use the Button that has been generated by ShadcnUI.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Meta, StoryObj } <span class="hljs-keyword">from</span> <span class="hljs-string">'@storybook/react'</span>

<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/button'</span>

<span class="hljs-comment">// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export</span>
<span class="hljs-keyword">const</span> meta = {
  title: <span class="hljs-string">'Example/Button'</span>,
  component: Button,
  parameters: {
    <span class="hljs-comment">// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout</span>
    layout: <span class="hljs-string">'centered'</span>,
  },
  <span class="hljs-comment">// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs</span>
  tags: [<span class="hljs-string">'autodocs'</span>],
  <span class="hljs-comment">// More on argTypes: https://storybook.js.org/docs/react/api/argtypes</span>
  argTypes: {
    <span class="hljs-comment">// backgroundColor: { control: 'color' },</span>
  },
} satisfies Meta&lt;<span class="hljs-keyword">typeof</span> Button&gt;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> meta
<span class="hljs-keyword">type</span> Story = StoryObj&lt;<span class="hljs-keyword">typeof</span> meta&gt;

<span class="hljs-comment">// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Primary: Story = {
  args: {
    variant: <span class="hljs-string">'default'</span>,
    children: <span class="hljs-string">'Primary'</span>,
  },
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Secondary: Story = {
  args: {
    variant: <span class="hljs-string">'secondary'</span>,
    children: <span class="hljs-string">'Secondary'</span>,
  },
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Large: Story = {
  args: {
    size: <span class="hljs-string">'lg'</span>,
    children: <span class="hljs-string">'Large'</span>,
  },
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Small: Story = {
  args: {
    size: <span class="hljs-string">'sm'</span>,
    children: <span class="hljs-string">'Small'</span>,
  },
}
</code></pre>
<p>Update the import and all the props to match our Button props.</p>
<p>However, even after adding all of this, the styling from TailwindCSS has not applying yet. This is because we have not import the index.css into our Storybook. We can do this by adding an import into our <code>.storybook/preview.ts</code></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// .storybook/preview.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">'@/index.css'</span>

...
</code></pre>
<p>Once, save the hot reload should apply and the styling should be applied to our Butotn stories.</p>
<hr />
<h3 id="heading-final-thoughts">Final Thoughts</h3>
<p>Testing Bun with React, Typescript, Vite, TailwindCSS, ShadcnUI and Storybook has been successful so far. The installation and runtime speed of Bun has been a breeze. Looking forward to more development from Bun, Node and Deno in the Javascript world to keep improving the industry standard ahead for years to come.</p>
]]></content:encoded></item><item><title><![CDATA[First Article]]></title><description><![CDATA[I'm starting to find writing article fun and would like to declare myself to work on an article in a certain timeframe from now on 😁]]></description><link>https://blog.borristw.com/first-article</link><guid isPermaLink="true">https://blog.borristw.com/first-article</guid><category><![CDATA[first post]]></category><dc:creator><![CDATA[Borris]]></dc:creator><pubDate>Sat, 29 Jan 2022 00:13:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/vrbZVyX2k4I/upload/v1643415087663/QTMAGiDfV.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm starting to find writing article fun and would like to declare myself to work on an article in a certain timeframe from now on 😁</p>
]]></content:encoded></item></channel></rss>