<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Niki</title>
    <description>The latest articles on DEV Community by Niki (@snikidev).</description>
    <link>https://dev.to/snikidev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F370563%2F72c5ca46-0b5e-4129-b832-4de4ef5cbfe5.jpeg</url>
      <title>DEV Community: Niki</title>
      <link>https://dev.to/snikidev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/snikidev"/>
    <language>en</language>
    <item>
      <title>The silent promise that will kill your Node.js server</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Fri, 23 Jan 2026 06:35:01 +0000</pubDate>
      <link>https://dev.to/snikidev/the-silent-promise-that-will-kill-your-nodejs-server-1b8g</link>
      <guid>https://dev.to/snikidev/the-silent-promise-that-will-kill-your-nodejs-server-1b8g</guid>
      <description>&lt;p&gt;You've got a production server. It's been running fine for hours. Then suddenly it crashes. No warning, no graceful degradation. Just dead.&lt;/p&gt;

&lt;p&gt;The culprit? A single line of code that looks completely innocent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;saveMessageToDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;You're building a chat API. When a user sends a message, you want to stream the response back immediately while saving it to the database in the background. Classic fire-and-forget pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleChat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Fire and forget - don't wait for this&lt;/span&gt;
    &lt;span class="nf"&gt;saveMessageToDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. What could go wrong?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Kill Shot
&lt;/h2&gt;

&lt;p&gt;Three hours later, the AI provider has a hiccup. The stream connection drops mid-transfer. &lt;code&gt;saveMessageToDatabase&lt;/code&gt; throws an error.&lt;/p&gt;

&lt;p&gt;Your server is dead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"PM2/Docker/Kubernetes will restart it!" Sure — and you've still lost every in-flight request, any unsaved state, and your users just saw an error. Prevention beats recovery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why try-catch Didn't Save You
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;try-catch&lt;/code&gt; only catches errors from &lt;strong&gt;awaited&lt;/strong&gt; promises:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ This try-catch is useless&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;saveMessageToDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Returns immediately&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Never reached&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;await&lt;/code&gt;, the function returns a Promise and exits the try block successfully. The actual error happens &lt;em&gt;later&lt;/em&gt;, asynchronously, after the try-catch is long gone.&lt;/p&gt;

&lt;p&gt;When that Promise eventually rejects with no handler attached, Node.js sees an "unhandled rejection" and, depending on your Node version, terminates the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;One character. Well, a few:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;saveMessageToDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. By attaching &lt;code&gt;.catch()&lt;/code&gt;, you tell Node "I know this might fail, and I'm handling it." The callback can be empty if you're logging inside the function. You just need to mark the Promise as "handled."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;For any fire-and-forget async operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Always attach .catch() to floating promises&lt;/span&gt;
&lt;span class="nf"&gt;doSomethingAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;

&lt;span class="c1"&gt;// Or log the error if you want visibility&lt;/span&gt;
&lt;span class="nf"&gt;doSomethingAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Background task failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;try-catch&lt;/code&gt; and &lt;code&gt;async/await&lt;/code&gt; are not magic. They follow specific rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;try-catch&lt;/code&gt; only catches synchronous throws and awaited rejections&lt;/li&gt;
&lt;li&gt;A Promise without &lt;code&gt;await&lt;/code&gt; or &lt;code&gt;.catch()&lt;/code&gt; is a ticking time bomb&lt;/li&gt;
&lt;li&gt;"Fire and forget" still needs a &lt;code&gt;.catch()&lt;/code&gt; — you're forgetting the &lt;em&gt;result&lt;/em&gt;, not the &lt;em&gt;error&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your server will thank you.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>My go-to patterns for full-stack/frontend projects</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Thu, 15 Jan 2026 11:50:06 +0000</pubDate>
      <link>https://dev.to/snikidev/my-go-to-patterns-for-full-stackfrontend-projects-150a</link>
      <guid>https://dev.to/snikidev/my-go-to-patterns-for-full-stackfrontend-projects-150a</guid>
      <description>&lt;p&gt;After working on quite a few frontend and full-stack projects (mostly React + TypeScript + some flavour of server/backend), I kept coming back to the same handful of patterns. They bring structure, reduce mental overhead, and make the codebase feel maintainable even as it grows.&lt;/p&gt;

&lt;p&gt;These aren't revolutionary, but they're pragmatic choices that have worked well across different apps. Here's the current set I reach for almost every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. React Query + Query Key Factory pattern
&lt;/h2&gt;

&lt;p&gt;I end up using &lt;strong&gt;TanStack Query&lt;/strong&gt; (React Query) in nearly every project. To keep query keys consistent, readable, and refactor-friendly, I follow the &lt;a href="https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories" rel="noopener noreferrer"&gt;&lt;strong&gt;query key factory&lt;/strong&gt;&lt;/a&gt; approach.&lt;/p&gt;

&lt;p&gt;Centralised factories make keys predictable and give great auto-completion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/query-keys.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bookingKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bookings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;bookingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;upcoming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;bookingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upcoming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bookingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookingId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getBooking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookingId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same factory file becomes the single source of truth for &lt;strong&gt;invalidations&lt;/strong&gt; too. You can define an invalidation map and trigger &lt;code&gt;queryClient.invalidateQueries()&lt;/code&gt; calls from one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Same file or a companion invalidations.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invalidateOnBookingChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bookingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Or more granular:&lt;/span&gt;
  &lt;span class="c1"&gt;// queryClient.invalidateQueries({ queryKey: bookingKeys.upcoming(...) });&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having invalidations centralised means you can easily track and manage data freshness across different parts of the app (dashboard, lists, details pages) without hunting through components or mutations. One change here ripples everywhere consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Server Actions / Server Functions
&lt;/h2&gt;

&lt;p&gt;I almost never write traditional API routes anymore. Instead, I use whatever flavour of server actions / functions the framework provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/13/app/api-reference/functions/server-actions" rel="noopener noreferrer"&gt;Next.js Server Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/actions/" rel="noopener noreferrer"&gt;Astro Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest/docs/framework/react/guide/server-functions" rel="noopener noreferrer"&gt;TanStack Start server functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are still essentially API-like endpoints under the hood—they can be called directly (e.g., via fetch or form POST), so you &lt;strong&gt;must protect them&lt;/strong&gt; with authentication, rate limiting, CSRF tokens (where applicable), and input validation just like any API.&lt;/p&gt;

&lt;p&gt;The big wins come from less boilerplate and more purpose-driven code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct function calls from client → no manual endpoint definitions&lt;/li&gt;
&lt;li&gt;Automatic type safety between client and server&lt;/li&gt;
&lt;li&gt;Easier error handling and revalidation&lt;/li&gt;
&lt;li&gt;Colocated logic (form → action → DB → response)&lt;/li&gt;
&lt;li&gt;Better integration with React's Suspense and transitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not magic, it just removes ceremony while keeping security responsibilities intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Permission / Authorisation Management with CASL
&lt;/h2&gt;

&lt;p&gt;Most apps end up needing fine-grained permissions. I centralise that logic with &lt;a href="https://casl.js.org/v6/en/" rel="noopener noreferrer"&gt;CASL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Define abilities once (often user/session-based):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AbilityBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMongoAbility&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@casl/ability&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defineAbilitiesFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cannot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbilityBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createMongoAbility&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;cannot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// explicit deny example&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use in services with simple conditionals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BookingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;updateBooking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bookingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Booking&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAbilitiesFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;booking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBookingDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookingId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// from queries&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;booking&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not authorized to update this booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Proceed with update...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateBooking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookingId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// show sensitive data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeps permission logic declarative, testable, and out of business-flow code.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Lightweight Repository / Query pattern
&lt;/h2&gt;

&lt;p&gt;I keep a &lt;code&gt;queries/&lt;/code&gt; folder with plain async functions that are &lt;strong&gt;pure DB statements&lt;/strong&gt; and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// queries/bookings.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBookingDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Booking&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Drizzle/Prisma/etc. query only&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateBooking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Booking&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Pure update, no side effects&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strict rules for these functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only data access (i.e. SELECT, INSERT, UPDATE, DELETE)&lt;/li&gt;
&lt;li&gt;No business logic&lt;/li&gt;
&lt;li&gt;No authorisation checks&lt;/li&gt;
&lt;li&gt;No emails, queues, external calls, or side effects&lt;/li&gt;
&lt;li&gt;Reusable from any service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This thin &lt;strong&gt;Data Access Layer&lt;/strong&gt; makes swapping ORMs trivial (change only &lt;code&gt;queries/&lt;/code&gt; files) and keeps services focused on orchestration.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Optimistic Initial Data in React Query
&lt;/h2&gt;

&lt;p&gt;Pass SSR/SSG-fetched data as &lt;code&gt;initialData&lt;/code&gt; to avoid loading flashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bookingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcoming&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUpcomingBookings&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;initialUpcoming&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SSR is non-negotiable today.&lt;/strong&gt; The era of plain Create React App SPAs is over. Even the React team officially deprecated CRA for new apps in early 2025 and &lt;a href="https://react.dev/learn/start-a-new-react-project" rel="noopener noreferrer"&gt;recommends frameworks instead&lt;/a&gt;. Modern file-based routing frameworks (Next.js, Tanstack Start, Astro, etc.) all have SSR/SSG built in. Leveraging that initial data improves perceived performance, reduces layout shifts, and gives users something meaningful on first paint. Why throw it away?&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Container / Presentational  aka Smart / Dumb Components pattern
&lt;/h2&gt;

&lt;p&gt;I still like &lt;a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0" rel="noopener noreferrer"&gt;this classic separation&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Presentational&lt;/strong&gt; (dumb): only props, no hooks/state/fetching → pure UI, very easy to unit test and reason about&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container&lt;/strong&gt; (smart): handles data, state, orchestration, passes props down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Presentational – great for snapshot/visual testing&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BookingListView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPageChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Skeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BookingItem&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;booking&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pagination&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onPageChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Container&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BookingList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBookings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BookingListView&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...{&lt;/span&gt; &lt;span class="nx"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPageChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setPage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dumb components become trivial to test in isolation, since it requires no mocking data layers or auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Custom Hook pattern
&lt;/h2&gt;

&lt;p&gt;Any time you see a component growing fat with state + data fetching + pagination + error handling → extract it to a custom hook.&lt;/p&gt;

&lt;p&gt;Before: 50+ lines of useQuery/useState/session logic inside the component.&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PatientDashboard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialUpcoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialPast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialNext&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;upcoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;past&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nextAppointment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isLoadingUpcoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;upcomingPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;setUpcomingPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDashboard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialUpcoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialPast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialNext&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"space-y-8"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NextAppointmentCard&lt;/span&gt; &lt;span class="na"&gt;booking&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nextAppointment&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoadingNextAppointment&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BookingList&lt;/span&gt; &lt;span class="na"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;upcoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;upcomingPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onPageChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setUpcomingPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; If you see &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useQuery&lt;/code&gt; (or similar) clustered together for one clear purpose → extract to a custom hook.&lt;/p&gt;

&lt;p&gt;Components stay focused on rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Strategy pattern (e.g. for third-party providers)
&lt;/h2&gt;

&lt;p&gt;When you might switch providers (Zoom → Google Meet → others), hide the implementation behind a unified interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/meeting.ts&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MeetingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createMeeting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateMeetingInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// strategy selected by config / env&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;activeMeetingProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeps services clean and future-proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;These patterns show up in almost every project I work on now. Together they deliver:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Readable, well-organised code&lt;/li&gt;
&lt;li&gt;Fewer weird logic bugs (everything has its place)&lt;/li&gt;
&lt;li&gt;Lower maintenance cost (easier tests, fewer surprises)&lt;/li&gt;
&lt;li&gt;Faster feature development (less time fighting structure)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And most importantly, when everything follows clear conventions (and you document them in a single &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; or similar) AI tools like Cursor or Copilot suddenly become much more accurate. They "get" the patterns right away and generate code that actually fits, without you prompting it 10 more times to put it all in the right folders, in the right format.&lt;/p&gt;

&lt;p&gt;All the classic engineering benefits, without overcomplicating things.&lt;/p&gt;

</description>
      <category>react</category>
      <category>patterns</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I am an AI Engineer</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Mon, 20 Oct 2025 20:14:41 +0000</pubDate>
      <link>https://dev.to/snikidev/i-am-an-ai-engineer-1i95</link>
      <guid>https://dev.to/snikidev/i-am-an-ai-engineer-1i95</guid>
      <description>&lt;p&gt;Over the past year, I've completed five projects for medium and large established companies. Four were greenfield builds from scratch; the other involved integrating AI features into an existing platform.&lt;/p&gt;

&lt;p&gt;I'm fundamentally a &lt;strong&gt;software engineer&lt;/strong&gt; who's deeply curious about AI. I've gravitated here because it's fascinating.&lt;/p&gt;

&lt;p&gt;That said, if you need to pin me on the AI spectrum, &lt;strong&gt;AI Engineer&lt;/strong&gt; fits best. The field is still evolving, so titles are a bit of a mess. I mean, we're still debating software engineering titles after decades!&lt;/p&gt;

&lt;p&gt;In this post, I thought I'd tell you more about what I actually do day-to-day and what I believe makes me an AI Engineer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "AI Engineer"?
&lt;/h2&gt;

&lt;p&gt;While looking for gigs, I’ve seen a LOT of job titles. They're like a startup's org chart - fluid and confusing. Here's some that I saw and what job descriptions and requirements they had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine Learning Engineer&lt;/strong&gt;: Heavy on deploying ML models at scale, often with a focus on production pipelines (e.g., using Kubernetes for model serving). It's more ops-intensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Scientist&lt;/strong&gt;: This is a classic data role. They dive into data analysis, hypothesis testing, and storytelling with stats. They might build prototypes in Jupyter notebooks and prepare a presentation about findings, but they're not usually wiring it all into a full app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Engineer&lt;/strong&gt;: The pipeline maestro. They build ETL processes, manage data lakes on AWS S3 or GCP BigQuery, and ensure data flows cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Learning Engineer&lt;/strong&gt;: Specialised in neural nets, often researching architectures like transformers or GANs. Think tweaking layers in PyTorch for cutting-edge performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Solutions Architect&lt;/strong&gt;: Basically a Solutions Architect (or DevOps?) that knows how to work with AI-specific tools. They design high-level systems, advise on cloud setups (e.g., Azure AI vs. Google Vertex), and align tech with business goals. Less coding, more diagramming and cloud infrastructure setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research Engineer&lt;/strong&gt;: Academia-adjacent, pushing boundaries with papers. They invent new algorithms, not integrate them into products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm Engineer&lt;/strong&gt;: I've seen this pop up too: focused on optimising core math, like efficient sorting for AI inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI space is still nascent, so overlap is huge, and for some roles the title and description were completely different from what I have here.&lt;/p&gt;

&lt;p&gt;For me, &lt;strong&gt;AI Engineer&lt;/strong&gt; screams &lt;em&gt;hands-on builder&lt;/em&gt;. I'm not theorising in a lab or handling streams of petabytes of data, but rather I'm crafting end-to-end solutions that &lt;em&gt;work&lt;/em&gt; in the real world.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does an AI Engineer actually do?
&lt;/h2&gt;

&lt;p&gt;At my core, I'm solving &lt;strong&gt;software engineering problems&lt;/strong&gt; in an AI context. I take AI capabilities (like pre-trained models from Azure AI Foundry or OpenRouter) and make them work in production.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of my toolkit and typical tasks:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Model setup and tweaking&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;On every project I had to choose a foundation model (e.g., OpenAI GPT-5, XAI Grok-4, Gemini-2.5 from Google AI, or Google Vertex).&lt;/p&gt;

&lt;p&gt;So far I never had to fine-tune a model for a production-ready project; most things were resolved via different prompting or extra context. I did fine-tune once in a hackathon we had at Sainsbury’s, and if I had to do it for a project, I would just use standard platform-provided tools.&lt;/p&gt;

&lt;p&gt;For example, Azure &lt;a href="https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/fine-tuning-overview" rel="noopener noreferrer"&gt;has a guide&lt;/a&gt; on techniques, fine-tuning data formats, and tools it has to improve general LLMs. This would be my starting point.&lt;/p&gt;

&lt;p&gt;Another important and frequent task I found myself doing is testing and experimenting with prompts, handling token limits, and just making sure it behaves more or less deterministically.&lt;/p&gt;

&lt;p&gt;“Deterministic” as in “trying to control” or “predict” what the output will be so that we could prep our code/app/ui for this.&lt;/p&gt;

&lt;p&gt;Counterintuitive? Yes. But that’s what a lot of companies want, that’s what I’ve seen the most amongst requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Integration and infrastructure&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;LLM calls are just API calls to someone else’s infrastructure. These calls can’t exist on their own; you have to embed them into a framework.&lt;/p&gt;

&lt;p&gt;Of course, some LLM- or AI agent-oriented frameworks have a built-in server, like &lt;a href="https://docs.langchain.com/langsmith/langgraph-server" rel="noopener noreferrer"&gt;LangGraph server&lt;/a&gt;, but not all, and quite often those are raw. I, personally, don’t like using those in production.&lt;/p&gt;

&lt;p&gt;So what I did was set up either a FastAPI server, where I could add auth middleware, database integrations, custom routing, or just use existing Next.js or Astro API routes as a back-end to expose AI models or agents, and custom SSE or WebSockets setup to handle real-time responses in the front-end.&lt;/p&gt;

&lt;p&gt;With that setup came lots of Docker containers. Lots and lots of Docker images… 😄&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Frontend and user experience&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Why would you do all this infrastructure if no one will be able to interact with it? So we’re putting on a front-end engineer hat and handle all those streaming chat responses token-by-token for that snappy feel with React and TypeScript.&lt;/p&gt;

&lt;p&gt;Moreover, LLMs not only generate text but also images. And they accept images and docs. So I spent a lot of time debugging multimodal issues, figuring out why an LLM rejects an image, converting it to base64, and sending it correctly via REST API.&lt;/p&gt;

&lt;p&gt;And then doing the same vice versa.&lt;/p&gt;

&lt;p&gt;Also, lots of parsing and formatting tasks, because LLMs don’t like HTML. So if you scraped a page and want to feed it into an LLM, then you better convert it to markdown.&lt;/p&gt;

&lt;p&gt;Same goes for tables. LLMs don’t understand tables. Transforming them to an LLM-appropriate markdown or JSON is what I did.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;End-to-end problem solving&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Just like any software project, there’s a lot more to handle end-to-end, like observability and monitoring with tools like Azure Insights, Sentry, LangSmith; ensuring low latency; adding guardrails and AI configs so that AI doesn’t say anything inappropriate; API rate limiting; error handling (do you send an error as a chat message or show a toast with an error message?); handling secrets (because we’re not inexperienced vibe coders who push secrets to public repos, are we?)…&lt;/p&gt;

&lt;p&gt;Essentially, all of the classical software engineering, I’d say. Just with a bit of AI twist. 🤷&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’m not touching
&lt;/h2&gt;

&lt;p&gt;I'm not touching the "data territory" much. If you need me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scour datasets, clean outliers with Pandas,&lt;/li&gt;
&lt;li&gt;Train from scratch on SageMaker,&lt;/li&gt;
&lt;li&gt;Evaluate with metrics like ROC-AUC or Weights &amp;amp; Biases dashboards,&lt;/li&gt;
&lt;li&gt;Iterate on hyperparameters...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...I &lt;em&gt;can&lt;/em&gt; do it (I know the theory from self-study and curiosity). But that's veering into Data Scientist/Engineer land, and if you’re not doing PoC you better find someone who’s better at this.&lt;/p&gt;

&lt;p&gt;My sweet spot is &lt;strong&gt;software issues&lt;/strong&gt;. Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging why a model inference fails in production.&lt;/li&gt;
&lt;li&gt;Optimising API endpoints for 100ms response times.&lt;/li&gt;
&lt;li&gt;Ensuring cross-browser compatibility for AI-generated content.&lt;/li&gt;
&lt;li&gt;Scaling from prototype to handling 10k concurrent users.&lt;/li&gt;
&lt;li&gt;Integrating with legacy systems (e.g., bridging AI outputs to a monolithic Java backend).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I’m a blue-collar worker of the AI World
&lt;/h2&gt;

&lt;p&gt;In essence, as an AI Engineer, I'm the bridge between fancy AI models and real apps. I handle the coding, setup, and fixes to make everything work smoothly in production. From my five projects with different companies, I've learned this role is all about practical building, not data crunching or research.&lt;/p&gt;

&lt;p&gt;No PhD needed. Just strong coding skills and a love for getting AI into users' hands.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>software</category>
      <category>career</category>
    </item>
    <item>
      <title>Understanding the Agentic Commerce Protocol: your guide to selling on ChatGPT</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Mon, 06 Oct 2025 09:13:12 +0000</pubDate>
      <link>https://dev.to/snikidev/understanding-the-agentic-commerce-protocol-your-guide-to-selling-on-chatgpt-2jeh</link>
      <guid>https://dev.to/snikidev/understanding-the-agentic-commerce-protocol-your-guide-to-selling-on-chatgpt-2jeh</guid>
      <description>&lt;p&gt;The e-commerce world just got a major upgrade with the launch of &lt;strong&gt;ChatGPT’s Instant Checkout&lt;/strong&gt;, powered by the &lt;strong&gt;Agentic Commerce Protocol (ACP)&lt;/strong&gt;. If you haven’t checked it out yet, go to &lt;a href="https://www.agenticcommerce.dev/" rel="noopener noreferrer"&gt;agenticcommerce.dev&lt;/a&gt; to see what it is. As a software engineer with zero experience in ACP let me tell you how I understand it (in simple terms).&lt;/p&gt;

&lt;p&gt;Because I know that starting tomorrow, companies will start posting job listings asking for “5+ years of ACP experience”. So, let’s figure out what it takes to get shops selling on ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Easy Way In: Etsy and Shopify Merchants
&lt;/h2&gt;

&lt;p&gt;If you run your shop on &lt;strong&gt;Etsy&lt;/strong&gt; or &lt;strong&gt;Shopify&lt;/strong&gt;, you’ve hit the jackpot. These platforms are already integrated with ChatGPT’s Instant Checkout. No extra coding, no fuss. Your products are automatically eligible to appear when users search for stuff like “large t-shirt under $40.” OpenAI has done the heavy lifting, so Etsy and Shopify merchants are live (or rolling out soon) with zero setup. Lucky you!&lt;/p&gt;

&lt;h2&gt;
  
  
  For Everyone Else: Here’s the Work
&lt;/h2&gt;

&lt;p&gt;If you aren’t on Etsy or Shopify, integrating with ChatGPT requires some effort. Don’t worry - it’s manageable, especially if you’ve built e-commerce APIs before. The process boils down to four steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registering&lt;/li&gt;
&lt;li&gt;Pushing products&lt;/li&gt;
&lt;li&gt;Setting up checkout endpoints&lt;/li&gt;
&lt;li&gt;Capturing payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s walk through it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0: Register as a Merchant
&lt;/h3&gt;

&lt;p&gt;First, you need to get your shop approved by OpenAI. Head to &lt;a href="https://chatgpt.com/merchants" rel="noopener noreferrer"&gt;chatgpt.com/merchants&lt;/a&gt; and fill out the merchant application form. It’s straightforward - provide details about the business, primary product categories, and whether you’re setting up a product feed or checkout integration. OpenAI reviews submissions on a rolling basis (expect 1-2 weeks). This step ensures only vetted merchants join the ecosystem, keeping things secure and trustworthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Push Products to ChatGPT’s Collection
&lt;/h3&gt;

&lt;p&gt;Once approved, you need to make ChatGPT aware of your products. This isn’t a public web search. ChatGPT uses a curated registry of approved ACP implementers to ensure compliance, accuracy, and trust. You’ll provide a &lt;strong&gt;product feed&lt;/strong&gt; in a structured JSON format, either via an endpoint (e.g., &lt;code&gt;/products/feed&lt;/code&gt;) or a file, following OpenAI’s &lt;a href="https://developers.openai.com/commerce/specs/feed" rel="noopener noreferrer"&gt;Product Feed Spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what goes in the feed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product details&lt;/strong&gt;: ID, name, description, price, images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants&lt;/strong&gt;: Size, color, etc. (e.g., “large blue t-shirt”).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt;: Stock status, shipping options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can update the feed every 15 minutes to reflect price changes or stock levels or whatever. Right now, product rankings are organic, based purely on relevance to the user’s query (e.g., “large t-shirt under $40”). No paid promotions yet, but I bet OpenAI will add that soon. Capitalism always finds a way!&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Set Up Checkout Endpoints with ACP
&lt;/h3&gt;

&lt;p&gt;When a user finds a product in ChatGPT and clicks “Buy,” the magic happens through the &lt;strong&gt;Agentic Commerce Protocol&lt;/strong&gt;. Think of ACP as a standardized API with predictable inputs and outputs that any AI agent (like ChatGPT) can understand. It’s like USB-C for commerce - universally agreed-upon formats that make life easier. My iPhone’s Lightning cable still drives me nuts in a USB-C world! But I digress.&lt;/p&gt;

&lt;p&gt;Here’s how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initiate Checkout&lt;/strong&gt;: ChatGPT sends a POST request to your &lt;code&gt;/checkout_sessions&lt;/code&gt; endpoint to start the process. The payload includes buyer info (name, email, maybe address—ChatGPT often pre-fills this to speed things up), items, and shipping details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate and Update&lt;/strong&gt;: As the user selects sizes, delivery options, or tweaks the cart, ChatGPT sends more requests to update the session (e.g., POST to &lt;code&gt;/checkout_sessions/{id}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete Purchase&lt;/strong&gt;: When the user enters payment details (e.g., card or Apple Pay), ChatGPT sends a secure payment token to &lt;code&gt;/checkout_sessions/{id}/complete&lt;/code&gt;. You create the order and process the payment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These endpoints map to your client’s existing backend (e.g., inventory, tax calculations), so you’re just wrapping your current APIs in ACP’s format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Capture Payments and Orders
&lt;/h3&gt;

&lt;p&gt;Payments are handled via your existing provider, with &lt;strong&gt;Stripe&lt;/strong&gt; being the easiest (it supports ACP’s Shared Payment Token out of the box). ChatGPT passes a secure token (no raw card data, keeping it PCI-compliant), and you charge it using your usual payment flow. If you’re not on Stripe, you’ll need to align your provider with ACP’s Delegated Payments Spec. Fulfillment (shipping, returns) stays with your existing systems, and you update ChatGPT via webhooks (e.g., “order shipped”).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;ACP makes AI-driven commerce predictable and scalable. Unlike custom APIs that vary by platform, ACP’s standardized endpoints mean your shop can work with ChatGPT today and other AI agents tomorrow. It’s a win for developers like me who love consistency. Plus, you stay in control as the merchant of record, owning the customer relationship without ChatGPT getting in the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Here’s the plan if you already want to get your e-shop on ChatGPT:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt;: Submit the merchant form at &lt;a href="https://chatgpt.com/merchants" rel="noopener noreferrer"&gt;chatgpt.com/merchants&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Feed&lt;/strong&gt;: Create a product feed endpoint or file with your client’s product data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Endpoints&lt;/strong&gt;: Implement ACP’s &lt;code&gt;/checkout_sessions&lt;/code&gt; and related endpoints, mapping to your existing APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test and Launch&lt;/strong&gt;: Use OpenAI’s sandbox to test, get certified, and go live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is just a generic overview guide. Didn't want to get into the weeds too much. But if you have any questions or want some assistance making this work for your shop - get in touch, I'll be happy to help. 🙌&lt;/p&gt;

</description>
      <category>acp</category>
      <category>chatgpt</category>
      <category>ai</category>
      <category>openai</category>
    </item>
    <item>
      <title>From 60 to 25 hours: practical tips for effective AI-assisted development</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Thu, 15 May 2025 21:06:31 +0000</pubDate>
      <link>https://dev.to/snikidev/from-60-to-25-hours-practical-tips-for-effective-ai-assisted-development-5e3n</link>
      <guid>https://dev.to/snikidev/from-60-to-25-hours-practical-tips-for-effective-ai-assisted-development-5e3n</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cut development time by 60% (from 60 to 25 hours) using AI tools effectively&lt;/li&gt;
&lt;li&gt;Choose your tech stack yourself - don't let AI make these decisions&lt;/li&gt;
&lt;li&gt;Set up your project structure manually - AI tends to use outdated packages and weird folder structures&lt;/li&gt;
&lt;li&gt;Break down tasks into small, specific requests for AI - don't ask it to build entire features at once&lt;/li&gt;
&lt;li&gt;Provide relevant documentation snippets to help AI understand framework-specific requirements&lt;/li&gt;
&lt;li&gt;Keep context minimal - attach only relevant files, not entire codebases&lt;/li&gt;
&lt;li&gt;Start fresh sessions for new features to avoid recurring patterns&lt;/li&gt;
&lt;li&gt;Always refactor AI-generated code - it tends to be messy and needs cleanup&lt;/li&gt;
&lt;li&gt;Try different AI models to find what works best for you (Claude, Gemini-2.5, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Bottom line: Use AI to handle tedious tasks while maintaining control over architecture and design decisions&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I was doing one of the projects, I was using Copilot and I noticed that I finished this project way faster than I ever finished a similar project in the past. 25 vs 60 hours. AMAZING, no?! 🔥&lt;/p&gt;

&lt;p&gt;I was like turbo-charged! I was able to do code as fast as my ADHD mind was thinking about different aspects of it: layout, styling, state management, comms with back-end API, DB schema and relationships, error handling…&lt;/p&gt;

&lt;p&gt;All of this I was able to keep in my head AND be productive! Which is usually not the case, and I end up with a bunch of &lt;code&gt;TODO&lt;/code&gt; comments and no code (at first).&lt;/p&gt;

&lt;p&gt;And then refactoring all that multiple times using a different design pattern!&lt;/p&gt;

&lt;p&gt;So this blog post is an instruction on how to make yourself productive with AI copilot/cursor/whatever, instead of fighting with LLM, swearing at it and rejecting all of its work.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don't need AI to select the stack
&lt;/h2&gt;

&lt;p&gt;Maybe a little bit of DeepSearch.&lt;/p&gt;

&lt;p&gt;When starting you should already know what tech stack and patterns you're going to use.&lt;/p&gt;

&lt;p&gt;The only reasons you should use AI at the start of the project if you wanna compare frameworks for your particular use case, e.g. zustand vs jotai.&lt;/p&gt;

&lt;p&gt;In my case I knew I had to use Astro, because it was a front-end project with only some back-end required. It was perfect.&lt;/p&gt;

&lt;p&gt;This was a "live app", i.e. all users needed to be notified of the changes, so obviously I had to use WebSockets for that.&lt;/p&gt;

&lt;p&gt;And then data was of predictable shape, had lots of relationships, so it meant SQL, and PostgreSQL in particular. LOVE it! I'm even subscribed to &lt;a href="https://postgresweekly.com/" rel="noopener noreferrer"&gt;Postgres Weekly&lt;/a&gt;! 🐘&lt;/p&gt;

&lt;p&gt;I also wanted &lt;a href="https://orm.drizzle.team/" rel="noopener noreferrer"&gt;Drizzle ORM&lt;/a&gt; to manage all the migrations and CRUD operations.&lt;/p&gt;

&lt;p&gt;I knew that multiple components in React would need to interact with the same state, so I went with &lt;a href="https://effector.dev/" rel="noopener noreferrer"&gt;effector&lt;/a&gt;. It's my favourite because of the &lt;code&gt;sample&lt;/code&gt; , you can do some crazy logic with it, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// Watch for submitForm event, when it gets triggered ...&lt;/span&gt;
  &lt;span class="na"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ... take the $userName store's state ...&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$userName&lt;/span&gt;
  &lt;span class="c1"&gt;// ... and transform the state (`name`) AND the parameters from the submitForm function (`password`)&lt;/span&gt;
  &lt;span class="c1"&gt;// into parameters that signInFx would accept...&lt;/span&gt;
  &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// ... and then call the function with signInFx({ name, password })&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signInFx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isn't that awesome?! AND it all lives outside of your UI logic. SO clean!&lt;/p&gt;

&lt;p&gt;I could've also gone with &lt;a href="https://github.com/nanostores/nanostores" rel="noopener noreferrer"&gt;nanostores&lt;/a&gt;, that Astro is promoting.&lt;/p&gt;

&lt;p&gt;And, obviously, &lt;a href="https://ui.shadcn.com/" rel="noopener noreferrer"&gt;shadcn&lt;/a&gt; for UI components. Is there any other option?&lt;/p&gt;

&lt;p&gt;Ok, I like &lt;a href="https://www.heroui.com/" rel="noopener noreferrer"&gt;HeroUI&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;The rest feel raw or just weird.&lt;/p&gt;

&lt;p&gt;To sum up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't let AI choose your tech stack for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Init the project
&lt;/h2&gt;

&lt;p&gt;Don't ask AI to do it. It's just gonna do the shit job, use outdated packages, and setup weird-ass folder structure.&lt;/p&gt;

&lt;p&gt;Set it up yourself. The way YOU like. The way YOU are used to work.&lt;/p&gt;

&lt;p&gt;So I did. Just followed the Astro setup on shadcn basically&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-astro@latest astro-app  &lt;span class="nt"&gt;--template&lt;/span&gt; with-tailwindcss &lt;span class="nt"&gt;--install&lt;/span&gt; &lt;span class="nt"&gt;--add&lt;/span&gt; react &lt;span class="nt"&gt;--git&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I needed a dashboard. So I just took blocks from shadcn and adjusted items to just the ones I need.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.sniki.dev%2F.netlify%2Fimages%3Furl%3D_astro%252Fshadcn-blocks.BllGEqZf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.sniki.dev%2F.netlify%2Fimages%3Furl%3D_astro%252Fshadcn-blocks.BllGEqZf.png" alt="shadcn-blocks.png" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No AI. You're gonna spend more time explaining AI what you want.&lt;/p&gt;

&lt;p&gt;Same with things like &lt;code&gt;Dockerfile&lt;/code&gt; . There are templates, you can just copy and tweak it. Do it yourself, it's quicker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release the &lt;del&gt;Kraken&lt;/del&gt; AI!
&lt;/h2&gt;

&lt;p&gt;Once you have the setup ready, it's time to outsource some work.&lt;/p&gt;

&lt;p&gt;The only way I felt productive and content with what AI was giving me, is when I described step by step how I'd do it myself.&lt;/p&gt;

&lt;p&gt;So for the form I went&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a AddItem form React component using Tanstack Form.
It should have 3 inputs: customer (text, required), order number (text, required), vehicle (select field with Van and Truck as options, required)
It should have 2 buttons add (submit) and reset (nullify all the values in all the fields).
onSubmit create a function with empty body. We'll implement it later.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keep it short
&lt;/h2&gt;

&lt;p&gt;Notice how I didn't let it go further. If I didn't it might've went ahead and created some weird function, and an &lt;code&gt;/api/form&lt;/code&gt; route, it would've made all decisions for me.&lt;/p&gt;

&lt;p&gt;And I didn't want it to. I wanted it to make it the way I think is right.&lt;/p&gt;

&lt;p&gt;So that's one of the key secrets to make AI productive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stop Copilot from going. Don't ask it to do the entire project. Just ask it do a component.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then an endpoint. Then a util function. Then ask it to put it all together. AI is like children. Asking it too many things at once, will make it ignore your important details that you gave it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's never OpenAI
&lt;/h2&gt;

&lt;p&gt;For programming, I noticed, I never use any models released by OpenAI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's always Claude, either Sonnet 3.5 or 3.7 or, now, Gemini-2.5.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gemini-2.5 is actually &lt;a href="https://web.lmarena.ai/leaderboard" rel="noopener noreferrer"&gt;considered the best as of today&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Although GPT-4.1, they said, should be good at coding. And vibing…? Or is that GPT-4.5?&lt;/p&gt;

&lt;p&gt;Anyway, switch some models up, see which ones vibe the best with you.&lt;/p&gt;

&lt;p&gt;It's all about vibes these days, isn't it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Help it see what you see
&lt;/h2&gt;

&lt;p&gt;I've noticed that Cursor is doing web search. But Copilot isn't yet. So the model will not know everything, especially if it's new or rarely used.&lt;/p&gt;

&lt;p&gt;Like Astro. All models are really shit at Astro. Try asking it create an action with a certain input and output. Any one of them is gonna do such a shit job, that you'd start doubting the rest of its abilities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Insert snippets from Get Started or Example pages from the docs. This context would really help AI in generating the code that you have in your head and not a random shit from the internet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or you can setup &lt;a href="https://github.com/RohitKrish46/docs-mcp-server" rel="noopener noreferrer"&gt;MCP for yourself&lt;/a&gt; that would do the docs search for you, e.g.&lt;/p&gt;

&lt;p&gt;I haven't done that for myself yet tho.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't overdo the context
&lt;/h2&gt;

&lt;p&gt;You know how most (?) AI code editors let you attach files for context? This is an awesome feature!&lt;/p&gt;

&lt;p&gt;They also let you attach code bases. This is dumb.&lt;/p&gt;

&lt;p&gt;Remember, LLMs are like children: you give them too much, they'll forget most of it, you'll get garbage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attach only the relevant files for the ask.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't let it see all of it. In fact...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Close the session once you done the feature/ask. Let it start over.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That way you don't get it stuck in a recurring shit-generating pattern. Let it be a gold fish with a memory limited to a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor
&lt;/h2&gt;

&lt;p&gt;Once the feature is done, you won't be able to look at it, it's gonna be heinous.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ABR - Always Be Refactoring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The generated code will be all over the place, it's gonna have weird if/else logic, triple and quadruple ternary operators, repeated code… Complete unreadable mess!&lt;/p&gt;

&lt;p&gt;So you need to go through it with a fresh eye, move some functions out, change the logic… Refactor!&lt;/p&gt;

&lt;h2&gt;
  
  
  Notice your wins
&lt;/h2&gt;

&lt;p&gt;If you don't notice any significant wins when working with AI help, then you're either a) been working with AI for ages now and used to it (not the case today, I'd say) or b) it didn't work for you, fix it.&lt;/p&gt;

&lt;p&gt;For me I noticed these wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I switched from React provider pattern (I forgot I was using Astro) to state management in "vanilla JS" way in under 2 mins;&lt;/li&gt;
&lt;li&gt;I quickly scaffolded a gigantic switch statement for handling events for WebSockets on the server and associated actions it needs to take, e.g. call to DB, filter and map things out, handle errors, etc.;&lt;/li&gt;
&lt;li&gt;I created effector stores and events and connected all of them in multiple &lt;code&gt;sample&lt;/code&gt;s in under 5 mins with minor refactors, a thing I was always dreading to do even considering my admiration for &lt;code&gt;sample&lt;/code&gt; ;&lt;/li&gt;
&lt;li&gt;I went from using &lt;code&gt;react-use-websocket&lt;/code&gt; hook, to using &lt;code&gt;socket.io&lt;/code&gt; after realising that islands architecture wouldn't work with the hook, to native implementation of WebSockets, after learning that socket.io has a compatibility issue with my server;&lt;/li&gt;
&lt;li&gt;I scaffolded (and later refactored into a decent code) forms with validation and styling with a UI and form libraries of my choice in 5 mins. And I hate doing forms. This is the best win of all.&lt;/li&gt;
&lt;li&gt;I then refactored, cleaned and made all of my components, util functions, API endpoints, etc. small, testable, humanly readable, and DRY in less than 10 mins.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Most important win is, I didn't lose myself, the coder, the programmer that likes to write code, create and make shit work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I just outsourced the annoying and tedious bits that made my job soulless and energy draining.&lt;/p&gt;

&lt;p&gt;That's what AI is good for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>githubcopilot</category>
      <category>productivity</category>
      <category>llm</category>
    </item>
    <item>
      <title>Creating a simple RAG in python with AzureOpenAI and LlamaIndex</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Thu, 24 Oct 2024 12:33:53 +0000</pubDate>
      <link>https://dev.to/snikidev/creating-a-simple-rag-in-python-with-azureopenai-and-llamaindex-3loc</link>
      <guid>https://dev.to/snikidev/creating-a-simple-rag-in-python-with-azureopenai-and-llamaindex-3loc</guid>
      <description>&lt;p&gt;During a recent stream, I explored the process of ingesting a PDF using LlamaIndex and AzureOpenAI. This blog post will guide you through the steps to accomplish this task.&lt;/p&gt;

&lt;p&gt;The objective was straightforward: answer questions based on information contained in PDF files stored in a folder. Here's the process we'll follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Embed the PDF data: Convert PDFs into a format comprehensible for AI&lt;/li&gt;
&lt;li&gt;Initialise AzureOpenAI and provide it with the embedded data&lt;/li&gt;
&lt;li&gt;Ask a question and analyse the response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While the &lt;a href="https://docs.llamaindex.ai/en/stable/examples/customization/llms/AzureOpenAI/" rel="noopener noreferrer"&gt;LlamaIndex documentation&lt;/a&gt; provides an excellent guide, I've included some visual aids to enhance clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Environment
&lt;/h2&gt;

&lt;p&gt;First, let's install the necessary packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;dotenv llama-index llama-index-llms-azure-openai llama-index-embeddings-azure-openai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that AzureOpenAI is not included in the &lt;code&gt;llama-index&lt;/code&gt; package and must be installed separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring AzureOpenAI
&lt;/h2&gt;

&lt;p&gt;Navigate to Azure AI Studio and deploy your chosen model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.railyard.works%2F_astro%2Fdeploy-llm.Ds-yoiUk_Z1IDkHp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.railyard.works%2F_astro%2Fdeploy-llm.Ds-yoiUk_Z1IDkHp.webp" alt="Deploying LLM Model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once deployed, configure LlamaIndex to use it with the following settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AzureOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AZURE_OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;azure_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AZURE_OPENAI_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;api_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-05-01-preview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BUT.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Azure openAI resources unfortunately differ from standard openAI resources as you can't generate embeddings unless you use an embedding model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that we need a separate embedding model for generating embeddings. Let’s do that.&lt;/p&gt;

&lt;p&gt;Deploy an embedding model that starts with &lt;code&gt;text-embedding-*&lt;/code&gt;, since in this case we're working exclusively with text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.railyard.works%2F_astro%2Fdeploy-embedding-model.9p_PeLib_2dNNHH.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.railyard.works%2F_astro%2Fdeploy-embedding-model.9p_PeLib_2dNNHH.webp" alt="Deploying Embedding Model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure LlamaIndex to use the embedding model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embed_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AzureOpenAIEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deployment_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AZURE_OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;azure_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AZURE_OPENAI_EMBEDDING_ENDPOINT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;api_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2023-05-15&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing the RAG System
&lt;/h2&gt;

&lt;p&gt;With the setup complete, let's load PDFs into the data folder and query the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimpleDirectoryReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;query_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_query_engine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Can we advertise online gambling to under 18 year olds?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the script in our virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python ./src/main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we get a response of&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No, advertising online gambling to under 18 year olds is not permitted. Marketing communications must not exploit the vulnerabilities of this age group, and any advertisements that feature under-18s or are directed at them are likely to be considered irresponsible and in breach of established rules.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;We can make it better by embedding the data once and storing it in a database like Azure CosmosDB, which is a MongoDB at its core. This means we get rid of repetitive embedding overhead every time we run the script, allowing the LLM to access pre-embedded data directly from the database. I'll cover this in my future posts.&lt;/p&gt;

&lt;p&gt;And &lt;a href="https://github.com/snikidev/llamaindex-python-rag" rel="noopener noreferrer"&gt;here is the repo&lt;/a&gt; with this and any future code.&lt;/p&gt;

</description>
      <category>llamaindex</category>
      <category>rag</category>
      <category>azure</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I cut the deployment time of an Astro (Node.js) site on Azure from 1 hour to ~3 mins</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Mon, 23 Sep 2024 13:54:04 +0000</pubDate>
      <link>https://dev.to/snikidev/how-i-cut-the-deployment-time-of-an-astro-nodejs-site-on-azure-from-1-hour-to-3-mins-1c73</link>
      <guid>https://dev.to/snikidev/how-i-cut-the-deployment-time-of-an-astro-nodejs-site-on-azure-from-1-hour-to-3-mins-1c73</guid>
      <description>&lt;p&gt;Before:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frailyard.works%2Fblog_posts%2Fdeployment-on-azure-before.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frailyard.works%2Fblog_posts%2Fdeployment-on-azure-before.png" alt="Deployment on azure before" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frailyard.works%2Fblog_posts%2Fdeployment-on-azure-after.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frailyard.works%2Fblog_posts%2Fdeployment-on-azure-after.png" alt="Deployment on Azure after" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The way Azure sets it up for you when you go through the steps of deploying a new Azure Web App service is objectively horrible. This is what Azure pushes to your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy&lt;/span&gt;
&lt;span class="c1"&gt;# More GitHub Actions for Azure: https://github.com/Azure/actions&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and deploy Node.js app to Azure Web App&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Node.js version&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20.x"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install, build, and test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install&lt;/span&gt;
          &lt;span class="s"&gt;npm run build --if-present&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Zip artifact for deployment&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zip release.zip ./* -r&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload artifact for deployment job&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-app&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release.zip&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Production"&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy-to-webapp.outputs.webapp-url }}&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;#This is required for requesting the JWT&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download artifact from build job&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-app&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unzip artifact for deployment&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unzip release.zip&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Azure&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZUREAPPSERVICE_CLIENTID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZUREAPPSERVICE_TENANTID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;App"&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-to-webapp&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/webapps-deploy@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app-name"&lt;/span&gt;
          &lt;span class="na"&gt;slot-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Production"&lt;/span&gt;
          &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what’s happening here is that we&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the project in GitHub actions&lt;/li&gt;
&lt;li&gt;Zip it&lt;/li&gt;
&lt;li&gt;Send it to Azure&lt;/li&gt;
&lt;li&gt;Unzip it&lt;/li&gt;
&lt;li&gt;Run it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem is that we’re zipping it together with &lt;code&gt;node_modules&lt;/code&gt; folder. So sending it and unzipping it takes a LOT of time. 🤯&lt;/p&gt;

&lt;p&gt;I’m not sure why they recommend it this way, but after reading a bunch of poorly worded and outdated Azure guides and some short StackOverflow posts, I’ve figured out the way a Node.js app should be deployed.&lt;/p&gt;

&lt;p&gt;Essentially we need to only send the code from GitHub to Azure and build right there. For that you don’t need to do any post-install scripts or anything, Azure App Service does it for you, &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/configure-language-nodejs?pivots=platform-linux#customize-build-automation" rel="noopener noreferrer"&gt;here are the steps&lt;/a&gt; that it goes through as soon as the code reaches the service.&lt;/p&gt;

&lt;p&gt;But it doesn’t do that by default, so you need to enable it. You do that by setting the environment variable &lt;code&gt;SCM_DO_BUILD_DURING_DEPLOYMENT&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in &lt;strong&gt;Settings &amp;gt; Environment variables&lt;/strong&gt; in Azure Portal.&lt;/p&gt;

&lt;p&gt;Once we’ve done that, we now need to remove the build step from our GitHub Actions workflow and we’re done. Here is how the template looks like now. 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy&lt;/span&gt;
&lt;span class="c1"&gt;# More GitHub Actions for Azure: https://github.com/Azure/actions&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and deploy Node.js app to Azure Web App&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prepare&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Zip artifact for deployment&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zip release.zip ./* -r&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload artifact for deployment job&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-app&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release.zip&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prepare&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Production"&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy-to-webapp.outputs.webapp-url }}&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;#This is required for requesting the JWT&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download artifact from build job&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-app&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unzip artifact for deployment&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unzip release.zip&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Azure&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZUREAPPSERVICE_CLIENTID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZUREAPPSERVICE_TENANTID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Web&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;App"&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-to-webapp&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/webapps-deploy@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;app-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web-app"&lt;/span&gt;
          &lt;span class="na"&gt;slot-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Production"&lt;/span&gt;
          &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also a few “gotchas” I’ve noticed when deploying to Azure Web App service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Forget yarn or pnpm. npm only. There is no way you can get those into App Service environment. And if there is, I really don’t think it’s worth the effort. Azure is using &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/resources-kudu" rel="noopener noreferrer"&gt;Kudu&lt;/a&gt; so you better know its ins and outs, I feel like, if you decide to install additional packages there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HOST=0.0.0.0 PORT=8080&lt;/code&gt; before your start script, which in my (Astro) case was &lt;code&gt;node ./dist/server/entry.mjs&lt;/code&gt; . You can’t pick the port. I tried. Didn’t work.&lt;/li&gt;
&lt;li&gt;Node version &lt;code&gt;20.14&lt;/code&gt; is what it’s currently running in Azure as of the moment of writing this post. It was hard to find, they don’t print it in the dropdown when you select the environment when deploying the App Service. I got a few errors because the Node version wasn’t what my app was expecting 🤷‍♂️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And most important tip: &lt;strong&gt;upgrade your service to paid.&lt;/strong&gt; The whole build and deploy process is unbearably slow on a free plan. But, frankly, that’s not an Azure specific issue, that’s most of PaaS nowadays, isn’t it?&lt;/p&gt;

&lt;p&gt;Hope that helps! 😎&lt;/p&gt;

</description>
      <category>azure</category>
      <category>node</category>
      <category>githubactions</category>
      <category>astro</category>
    </item>
    <item>
      <title>Astro.js as an alternative to Next.js: pushing the limits</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Wed, 17 Apr 2024 10:02:29 +0000</pubDate>
      <link>https://dev.to/snikidev/astrojs-as-an-alternative-to-nextjs-pushing-the-limits-30ga</link>
      <guid>https://dev.to/snikidev/astrojs-as-an-alternative-to-nextjs-pushing-the-limits-30ga</guid>
      <description>&lt;p&gt;I’m tired of Next. I want to try something fresh. So I thought this was the time I look into other frameworks. Where would I go? Nuxt? Remix? Nah…&lt;/p&gt;

&lt;p&gt;Astro! Let’s test it and see if it can handle modern project requirements.&lt;/p&gt;

&lt;p&gt;Here is &lt;a href="https://github.com/snikidev/astro-limitless" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;, follow along!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why am I doing this?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Next.js is complicated. But some of the best websites out there use a plain, vanilla JS. We don’t need anything fancy to build great products. I mean, we could use a bit of help from a simple (!!) framework that gets out of your way and lets you deliver. And I want to find that framework.&lt;/li&gt;
&lt;li&gt;Next.js v13 was a &lt;del&gt;shit&lt;/del&gt; shift. In v13 Next.js introduced a mentality shift with RSC on how we develop an app, and since I have to shift my mentality, I’d like to shift it in all possible directions. Open my mind a little bit more than I have to.&lt;/li&gt;
&lt;li&gt;Vercel is not your friend. And Next.js is THEIR product. Sooner or later I feel like we’ll be shifting our mentality to adjust to how Vercel wants us to do things. I don’t wish to be locked in like that.&lt;/li&gt;
&lt;li&gt;I’m just tired of Next.js. Every company makes me work with Next.js. Always Next. I don’t really mind, but I miss those times when I was talking about Next as a “new framework” that we “should look into adopting”! I and others like me did a very good job. Well done, folks! But now I want to mix things up a bit, time to try something fresh! Again! 😅&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I guess you could say that Next.js is the reason I’m looking into Astro. 😁&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Astro.js
&lt;/h2&gt;

&lt;p&gt;I’ve been ignoring it like I do with Remix and Svelte and a bunch of other frameworks. However, I’ve learned that Astro is different, and I’m gonna talk about how it’s different later in the post.&lt;/p&gt;

&lt;p&gt;First, I needed to define a set of criteria with which I could judge its usefulness. How do I identify whether I should choose it (or recommend it) for a new project?&lt;/p&gt;

&lt;p&gt;So I started thinking and reflecting on all the companies and all the projects I ever I worked with. What was I doing there? What requirements they had in common? What kind of work they were assigning to me?&lt;/p&gt;

&lt;p&gt;And so I defined a list of such things, things that I was doing on EVERY job or contract. This is what everyone wanted me to do from tiny startups to humongous enterprises.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has React &amp;amp; TypeScript: because I’ve rarely seen a company that wants Vue or plain JavaScript. Everyone wants React and it has to be with TypeScript.&lt;/li&gt;
&lt;li&gt;Static pages: Fetch content from headless CMS at build time, create the HTML page and host it somewhere on a simple host.&lt;/li&gt;
&lt;li&gt;Global state management shared between components without prop drilling: Because there’s always at least a menu of some sort that you need to control from 3 different places.&lt;/li&gt;
&lt;li&gt;Custom theme (global): everyone has its own colours, fonts, etc. No hardcoding and repeating/copy+pasting those HEX values all over the CSS files!&lt;/li&gt;
&lt;li&gt;Scoped CSS (e.g. &lt;code&gt;.module.css&lt;/code&gt;): no one wants to load ALL of the CSS for every page and then use 1/100th of it.&lt;/li&gt;
&lt;li&gt;SSR rendered content that needs to be changed or updated from the client (e.g. using &lt;code&gt;@tanstack/query&lt;/code&gt;), e.g. you know that sidebar with filters on a page that lists a couple of dozens of products?&lt;/li&gt;
&lt;li&gt;User Authentication with multiple hidden pages: we don’t want to need to auth the user only on the client side. Let’s redirect the user on the server, i.e. BEFORE they get to the protected page.&lt;/li&gt;
&lt;li&gt;Localisation and internationalisation: I’ve done a bunch of projects that needed two or more languages. I’m doing one now that requires more than eight languages and more than ten geos/territories!&lt;/li&gt;
&lt;li&gt;Embedding Google Analytics and other tracking tools: because if it’s a client-facing site we &lt;em&gt;gots&lt;/em&gt; to know about our visitors, right?&lt;/li&gt;
&lt;li&gt;Design System in a Storybook: every company I joined (and I do mean EVERY) wanted their React components displayed neatly in a Storybook for all kinds of stakeholders to see/play with.&lt;/li&gt;
&lt;li&gt;Testing, incl. unit, integration and visual (i.e. snapshot testing).&lt;/li&gt;
&lt;li&gt;Server API function (or action) where secret keys can be used.&lt;/li&gt;
&lt;li&gt;Incremental Static Regeneration (ISR), for cases when we have hundreds or even thousands of pages.&lt;/li&gt;
&lt;li&gt;Deploy to a custom Docker-based environment somewhere to AWS Fargate, because not every company out there wants to use Vercel, Railway, and other “cool” and “hip” platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I did this silly “Frankenstein” project with those requirements. Let’s talk about the results!&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Astro is EXPLICIT
&lt;/h3&gt;

&lt;p&gt;The main conclusion that I got out of it is that Astro.js is like putting your project on a diet. On a JavaScript diet.&lt;/p&gt;

&lt;p&gt;The thing about Astro is that it promotes the ****“reduced JavaScript” approach. That’s a whole other kind of mentality shift than what I initially was hoping for.&lt;/p&gt;

&lt;p&gt;So it doesn’t render JavaScript unless you EXPLICITLY tell it to.&lt;/p&gt;

&lt;p&gt;“Explicit” is the word I’d associate with Astro. While Next hides a lot of implementation details, Astro does nothing of such a thing and leaves implementation to us, developers.&lt;/p&gt;

&lt;p&gt;If you want some interactivity with JavaScript on the front-end - you need to write it, e.g. &lt;code&gt;addListener&lt;/code&gt;s in the &lt;code&gt;script&lt;/code&gt; tags. Otherwise, Astro ships nothing to the browser.&lt;/p&gt;

&lt;p&gt;Before I wasn’t really thinking explicitly much about how much JavaScript is and isn’t used in the browser. Most of the time, I just let frameworks figure this out for me, and only when we were starting to have issues with performance, etc. only then I start looking “under the hood” and optimising.&lt;/p&gt;

&lt;p&gt;All in all, I’m happy with this approach. I kinda like it. Cognitively it feels easier to work this way.&lt;/p&gt;

&lt;p&gt;This leads to the next point.&lt;/p&gt;

&lt;h3&gt;
  
  
  React is just a sprinkle on top
&lt;/h3&gt;

&lt;p&gt;Astro allows you to use ALL sorts of UI frameworks. Including React. BUT!&lt;/p&gt;

&lt;p&gt;React is a UI library. And Astro is using it as such. RSC is not a thing there (yet?). So if you’re writing a &lt;code&gt;.tsx&lt;/code&gt;/ &lt;code&gt;.jsx&lt;/code&gt; file, you can be 100% sure it’s going to be rendered, live and work only in the browser.&lt;/p&gt;

&lt;p&gt;Yes, you can certainly feed it server-fetched data, so the user doesn’t see the loader for too long. But it still remains a client only component.&lt;/p&gt;

&lt;p&gt;For that reason, you can’t really use React ONLY or go too deep component-wise. They’re all end up on the client.&lt;/p&gt;

&lt;p&gt;Do you want that? Is that a good thing? It depends on the project.&lt;/p&gt;

&lt;p&gt;If you need to render things on the server first, say, for SEO reasons, and make it interactive, then I’d assume you’d need to have a conditional variable, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasBeenChangedByUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hasBeenChangedByUser&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactComponent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AstroComponent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And both &lt;code&gt;&amp;lt;AstroComponent /&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;ReactComponent /&amp;gt;&lt;/code&gt;, in this case, would look very similar. You could do a sharable code though, because &lt;a href="https://docs.astro.build/en/concepts/islands/#creating-an-island" rel="noopener noreferrer"&gt;Astro still allows rendering &lt;code&gt;.tsx&lt;/code&gt; component on the server&lt;/a&gt;, it just strips them of all the JavaScript, making them redundant for React purpose but great for layout, etc.&lt;/p&gt;

&lt;p&gt;In my experience, there weren’t many cases when we’d had to create something drastic like that, rather a healthy balance of server vs client. So this “React == UI only” approach doesn’t really bother me. I can render what I need on the server inside &lt;code&gt;.astro&lt;/code&gt; component, and components that need to be overly interactive, they’re client in any framework anyway, because interactivity == JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  No providers!
&lt;/h3&gt;

&lt;p&gt;With Astro, we have two types of files: &lt;code&gt;.astro&lt;/code&gt; and &lt;code&gt;.tsx&lt;/code&gt;. Guess what &lt;code&gt;.astro&lt;/code&gt; components can’t have that React just LOVES?&lt;/p&gt;

&lt;p&gt;Providers!&lt;/p&gt;

&lt;p&gt;Here’s another mentality shift for you. If you were thinking in a providers pattern, you need to snap out of it. You can no longer store your theme in a provider. No more &lt;a href="https://www.radix-ui.com/" rel="noopener noreferrer"&gt;RadixUI&lt;/a&gt; or &lt;a href="https://mantine.dev/" rel="noopener noreferrer"&gt;MantineUI&lt;/a&gt; for you.&lt;/p&gt;

&lt;p&gt;So to have a theme and reusable CSS variables you’d need to go with how Tailwind works. Tailwind doesn’t use providers. It stores all the variables in it’s own config file.&lt;/p&gt;

&lt;p&gt;Something like &lt;a href="https://nextui.org/" rel="noopener noreferrer"&gt;NextUI&lt;/a&gt; would work, i.e. a “New Gen” UI framework. It’s Tailwind based AND it works in both &lt;code&gt;.astro&lt;/code&gt; and &lt;code&gt;.tsx&lt;/code&gt; files. Amazing!&lt;/p&gt;

&lt;p&gt;To be frank, my last 4 projects were Tailwind-based. So I don’t see a problem with that. I can live with that choice.&lt;/p&gt;

&lt;p&gt;Guess WHAT also generally uses providers?&lt;/p&gt;

&lt;p&gt;State managers!&lt;/p&gt;

&lt;h3&gt;
  
  
  The time has come for the new kind of state managers
&lt;/h3&gt;

&lt;p&gt;We want to store state across Astro and React components. One state, two frameworks. How do you do that?&lt;/p&gt;

&lt;p&gt;Framework agnostic state managers!&lt;/p&gt;

&lt;p&gt;In its docs, Astro recommends &lt;a href="https://github.com/nanostores/nanostores" rel="noopener noreferrer"&gt;nanostores&lt;/a&gt;, but I’ve used &lt;a href="https://effector.dev/" rel="noopener noreferrer"&gt;effector&lt;/a&gt; in the past. And LOVED IT. So I’ve used it for this project as well.&lt;/p&gt;

&lt;p&gt;State managers like these are just JavaScript. They don’t care what frameworks they’re used with. They do have wrappers for them, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUnit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;effector-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUnit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but you can do without them.&lt;/p&gt;

&lt;p&gt;Have you noticed that in v5 &lt;code&gt;@tanstack/query&lt;/code&gt; moved on from using provider pattern for their QueryClient, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to a more function-based approach, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There’s a reason for that, I think. It’s more flexible, more universal, doesn’t lock you into a particular framework. You can fetch data on the server in &lt;code&gt;.astro&lt;/code&gt; component, store it in your &lt;code&gt;queryClient&lt;/code&gt; and then use it in a React component on the client with &lt;code&gt;useQuery&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Switch React to Vue and you won’t need to touch the state in any way. Amazing refactoring experience, isn’t it? 🤯&lt;/p&gt;

&lt;p&gt;Following the providers rant…&lt;/p&gt;

&lt;h3&gt;
  
  
  i18n
&lt;/h3&gt;

&lt;p&gt;Since there are no providers, we need to somehow communicate the language we’re working with to every component when setting up our localisation. So we do it this way 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In Astro component&lt;/span&gt;
&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLangFromUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Astro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"react"&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Then in Modal.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Modal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTranslations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ModalHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;modal.header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ModalHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the language won’t be coming from a provider, but rather as a prop. And we’ll be getting it from our server &lt;code&gt;.astro&lt;/code&gt; pages.&lt;/p&gt;

&lt;p&gt;Which, again, to me isn’t a big deal. It’s just different. It works and it still looks nice. We can even define a “parent” interface that we’ll be using for all the prop types, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PropsBase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ComponentProp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;PropsBase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Astro is just like Next but more explicit
&lt;/h3&gt;

&lt;p&gt;I keep coming back to that word.&lt;/p&gt;

&lt;p&gt;In a lot of other aspects, Astro is not that different from Next developer experience and mentality-wise. I didn’t even feel pain when switching. I already knew where things would go, where things would come from, how to structure a project, etc.&lt;/p&gt;

&lt;p&gt;Astro has its own place for &lt;code&gt;getServerSideProps&lt;/code&gt; (or &lt;code&gt;use server&lt;/code&gt;); it has an &lt;code&gt;api&lt;/code&gt; folder you can create to store some serverless functions in; it runs in node and you can build a Dockerfile for it. and you can throw all your Google Analytics scripts to &lt;a href="https://partytown.builder.io/google-tag-manager#forward-events" rel="noopener noreferrer"&gt;PARTYTOWN&lt;/a&gt;, just like you’d do in Next.&lt;/p&gt;

&lt;p&gt;And if you need to set up SSR authentication, you can do that, it’s pretty straightforward. I was able to set up one with &lt;a href="https://supabase.com/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr" rel="noopener noreferrer"&gt;Supabase Auth&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So it’s just like Next, but better.&lt;/p&gt;

&lt;p&gt;Just a few ‘gotchas’ though…&lt;/p&gt;

&lt;h3&gt;
  
  
  The “gotchas”
&lt;/h3&gt;

&lt;p&gt;Astro has no runtime. This means no unit tests. This also means no Storybook for your Astro components (although, they’re &lt;a href="https://github.com/storybookjs/storybook/issues/18356" rel="noopener noreferrer"&gt;working on it&lt;/a&gt;!)&lt;/p&gt;

&lt;p&gt;Can you live with that?&lt;/p&gt;

&lt;p&gt;I’d say yes. When it comes to testing, server-rendered components don’t have much interactivity going on anyway, no user clicks, no state changes, etc. So tests might be considered redundant or over-engineer.&lt;/p&gt;

&lt;p&gt;And the layout render we can always test in e2e/integration/visual kinds of tests with Cypress or PlayWright.&lt;/p&gt;

&lt;p&gt;As for Storybook, well, not everything needs to be in a Storybook. Surely we can find a nice balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Astro needs more work
&lt;/h3&gt;

&lt;p&gt;At the moment ISR comes down to cache &lt;a href="https://logsnag.com/blog/implementing-on-demand-revalidation-in-astro" rel="noopener noreferrer"&gt;control/invalidation&lt;/a&gt; in SSR pages, not static page regeneration.&lt;/p&gt;

&lt;p&gt;There is no per-page building, no ISR. If you rebuild static pages, you rebuild all of them.&lt;/p&gt;

&lt;p&gt;The build is fast though, as &lt;a href="https://astro.build/blog/experimental-static-build" rel="noopener noreferrer"&gt;it was pointed out&lt;/a&gt;, Astro certainly can handle 10K+ pages.&lt;/p&gt;

&lt;p&gt;Nonetheless, the Astro team is already &lt;a href="https://github.com/withastro/roadmap/issues/698" rel="noopener noreferrer"&gt;thinking and working on the build engine&lt;/a&gt;. So it will only become better.&lt;/p&gt;

&lt;p&gt;Not even more confusing as it was in Next.js case, I hope… 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  To sum up
&lt;/h2&gt;

&lt;p&gt;Astro is great. But it is slightly different from the way Next.js projects are done and not everyone would be happy to compromise. And that’s ok. We all have our own set of requirements, values, goals, the environment we work in, etc.&lt;/p&gt;

&lt;p&gt;However, I think Astro is the future. And I don’t mean Astro framework. I think the Astro way of doing things: explicit, transparent, and minimalistic. This is the future where all the frameworks work as one, and where frameworks don’t overwrite standardised APIs and expose important bits to the developers allowing us to modify and hack it to each project’s own needs.&lt;/p&gt;

&lt;p&gt;The future is simple, vanilla JavaScript because simplicity always wins. 🚀&lt;/p&gt;

&lt;p&gt;For more talking and less reading, feel free to &lt;a href="https://www.linkedin.com/posts/nkakuev_is-astro-ready-for-proper-projects-doing-activity-7171165857065099267-UlVW?utm_source=share&amp;amp;utm_medium=member_desktop" rel="noopener noreferrer"&gt;watch my stream&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Creating an autogrowing textarea with React and Typescript</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Thu, 28 Dec 2023 13:57:58 +0000</pubDate>
      <link>https://dev.to/snikidev/creating-an-autogrowing-textarea-with-react-and-typescript-45oc</link>
      <guid>https://dev.to/snikidev/creating-an-autogrowing-textarea-with-react-and-typescript-45oc</guid>
      <description>&lt;p&gt;In my 6 years career I almost never had to make an autogrowing textarea field like the ones we have in Jira or Linear (i.e. description fields) 👇&lt;/p&gt;


  


&lt;p&gt;But recently, I had to make one. While there are already some posts about this, e.g. &lt;a href="https://medium.com/@oherterich/creating-a-textarea-with-dynamic-height-using-react-and-typescript-5ed2d78d9848" rel="noopener noreferrer"&gt;THIS&lt;/a&gt; post by Owen, I thought I'd post my solution, which covers a slightly different scenario.&lt;/p&gt;

&lt;p&gt;So basically, I create a hook&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useInputHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setInputHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setInputHeight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setInputHeight&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;setInputHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns a &lt;code&gt;setInputHeight&lt;/code&gt; function that we call in our &lt;code&gt;onChange&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setNameInputHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInputHeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea[name="email"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emailValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleInputChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setInputHeight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other functionality&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you need a &lt;code&gt;value&lt;/code&gt; argument? No, you don't, but in my case, the value was being updated on the client, so I had to adjust the field height every time it updated without me triggering the &lt;code&gt;onChange&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;If that's not the case, simplify it to be&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useInputHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setInputHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;setInputHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all you'd need.&lt;/p&gt;

&lt;p&gt;You may have also noticed that I'm not using &lt;code&gt;useRef()&lt;/code&gt; and simply doing &lt;code&gt;document.querySelector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Would &lt;code&gt;ref&lt;/code&gt; work? Yes, yes it should. The only reason I was using &lt;code&gt;querySelector&lt;/code&gt; was because I was using a UI library and textarea there wasn't allowing a &lt;code&gt;ref&lt;/code&gt; prop. So I didn't want to do all those workarounds, &lt;code&gt;querySelector&lt;/code&gt; seemed like a great option.&lt;/p&gt;

&lt;p&gt;Do feel free to use a &lt;code&gt;ref&lt;/code&gt; if you'd like, after all, it IS a more 'React way' of doing things 😁👍&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>ui</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Storybook 7.0 + React.js + TailwindCSS + CSS modules + Typescript setup that #$%&amp; works</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Mon, 20 Feb 2023 22:48:18 +0000</pubDate>
      <link>https://dev.to/snikidev/storybook-70-reactjs-tailwindcss-css-modules-typescript-setup-that-works-281f</link>
      <guid>https://dev.to/snikidev/storybook-70-reactjs-tailwindcss-css-modules-typescript-setup-that-works-281f</guid>
      <description>&lt;p&gt;UPDATE: when writing this I have found &lt;a href="https://storybook.js.org/recipes/tailwindcss" rel="noopener noreferrer"&gt;THIS article&lt;/a&gt;, that, coincidentally, was also updated around the same time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;rant&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Setting up a storybook project should NOT be THAT hard. I mean, everyone uses Storybook, it’s the only viable option out there when working on a design system. &lt;/p&gt;

&lt;p&gt;TailwindCSS is at the stage when it’s already gained the momentum. Even large companies are considering it. Practically EVERY front-end dev has already worked with it.&lt;/p&gt;

&lt;p&gt;And CSS modules have beed out there for more that 6 years I believe. And even though in JavaScript terms it’s a dinosaur, a legacy, it’s very much not dead and still a top choice for many, including moi.&lt;/p&gt;

&lt;p&gt;Not even going to say anything about Typescript and React. If you’re not using TS or never worked with TS today - you’re unemployable. And React is a standard. &lt;/p&gt;

&lt;p&gt;Surely there should be an article somewhere on how to set all this up correctly. SURELY?!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;/rant&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Well, after 4 hours of googling and trial and error, I finally was able to crack this “OH SO UNUSUAL” tech stack setup. Here is &lt;a href="https://github.com/snikidev/storybook-tailwindcss-setup" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;, and let me highlight two points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Point 1: Webpack 5 builder and framework
&lt;/h2&gt;

&lt;p&gt;Webpack 4 is still the default choice for Storybook, but Storybook has already released support for Webpack 5, so I see no reason why we’d stick to v4. Important note here is that since we’re updating the &lt;code&gt;core&lt;/code&gt; property, we should also update the &lt;code&gt;framework&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/react-webpack5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;core&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/builder-webpack5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Point 2: Don’t overwrite default Webpack rules, but add more instead
&lt;/h2&gt;

&lt;p&gt;It (unfortunately!) became a habit for whatever reason, that if I need to add some custom loader, I exclude the default rule from the array and re-create it. We shouldn’t do that, because it messes up the Storybook config in a way that it becomes hard to unfuck it. &lt;/p&gt;

&lt;p&gt;Because we need to add a PostCSS loader to our &lt;code&gt;.css&lt;/code&gt; files, don’t try to re-create css rule with all the &lt;code&gt;css-loader&lt;/code&gt; and &lt;code&gt;style-loader&lt;/code&gt; etc., but just push that extra rule to the end of that array, and be done with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="na"&gt;webpackFinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;css$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postcss-loader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;postcssOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tailwindcss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;autoprefixer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s pretty much all I wanted to say. Those two points took me a while to understand, so hope your found my post within your first hour of searching. 😅&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Understanding front-end data visualization tools ecosystem in 2021 📊📈</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Fri, 05 Feb 2021 17:08:36 +0000</pubDate>
      <link>https://dev.to/cubejs/understanding-front-end-data-visualization-tools-ecosystem-in-2021-2nog</link>
      <guid>https://dev.to/cubejs/understanding-front-end-data-visualization-tools-ecosystem-in-2021-2nog</guid>
      <description>&lt;p&gt;Data is the new oil, and it's still true in 2021. However, to turn data into insights, we need to analyze and visualize. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So, here's the question: how to pick the right tool? 🛠&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post we're going to go through JavaScript frameworks and libraries that you can use to visualize your data. And I'd like to do a bit more than just list a few frameworks — I'm going to divide the list by the type of data or data visualization because "one size" doesn't fit all. There are different kinds of data, and each needs a specific visualization strategy.&lt;/p&gt;

&lt;p&gt;We'll go through...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;📈 general-purpose charting libraries&lt;/li&gt;
&lt;li&gt;📉 low-level and complex charting libraries&lt;/li&gt;
&lt;li&gt;🏁 tables and data grids&lt;/li&gt;
&lt;li&gt;⏰ timeline charts &amp;amp; time-based tools&lt;/li&gt;
&lt;li&gt;🗺 geospatial and mapping tools&lt;/li&gt;
&lt;li&gt;⛅️ word clouds&lt;/li&gt;
&lt;li&gt;🌎 3D visualization tools&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also, to help you choose the right tool for your project, I'm going to include a brief summary of each framework, i.e.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💵 whether it's paid or open-source&lt;/li&gt;
&lt;li&gt;⭐️ number of stargazers on GitHub, if it's open source&lt;/li&gt;
&lt;li&gt;📅 when it was last released (as of today)&lt;/li&gt;
&lt;li&gt;🧱 the size of the bundle (unpacked size on npm), and&lt;/li&gt;
&lt;li&gt;🔌 whether it's specific for a particular framework (e.g., React) or it can be used in any JavaScript-based project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decided to sort the frameworks and libraries by the number of stargazers because it roughly approximates their popularity and community size (but that's debatable).&lt;/p&gt;

&lt;p&gt;Also note that you won't find here an extensive list of every data visualization library which can be found on the internet. However, I hope that this post will help you build your own understanding of the ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;📊 And the icing on the cake: I'll provide links to guides and tutorials on data visualization tools built by the Cube.js team. &lt;a href="https://cube.dev?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;Cube.js&lt;/a&gt; helps to create an API over any database with ease, and it's often used as a very performant backend for analytical data visualizations.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  1. General-purpose charting libraries
&lt;/h1&gt;

&lt;p&gt;The first category is probably the most popular. Most data can be visualized with charts: either temporal (when you include time values to the plot, e.g., on the X axis), or non-temporal (when you have only numeric values or labels). Those include bar chart, pie chart, line graph, and similar. And because it's the most used type of charts, there are numerous options to choose from.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.chartjs.org" rel="noopener noreferrer"&gt;Chart.js&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 51.8K / 📅 19 Oct 2020 / 🧱 1.44 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Simple yet flexible JavaScript charting for designers &amp;amp; developers&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most popular open-source library for building responsive bar, pie, and line charts. I'd say this is the go-to library for most of the projects, as it fits most of the use cases.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;📊 See &lt;a href="https://cube.dev/blog/chart-js-example-with-dynamic-dataset/?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;Chart.js Example with Dynamic Dataset&lt;/a&gt; to learn how to modify the data being displayed with Chart.js on-the-fly.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://recharts.org/en-US/" rel="noopener noreferrer"&gt;Recharts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 15.6K / 📅 13 Jan 2021 / 🧱 11.2 MB / 🔌 React&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A composable charting library built on React components&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As per description, "It was built on top of SVG elements with a lightweight dependency on D3 submodules." It's a good choice for React-based projects, because you can use it natively as a component, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LineChart&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;XAxis&lt;/span&gt; &lt;span class="na"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;YAxis&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CartesianGrid&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#eee"&lt;/span&gt; &lt;span class="na"&gt;strokeDasharray&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"5 5"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Line&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"monotone"&lt;/span&gt; &lt;span class="na"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"uv"&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#8884d8"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Line&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"monotone"&lt;/span&gt; &lt;span class="na"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"pv"&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#82ca9d"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;LineChart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;📊 See &lt;a href="https://react-dashboard.cube.dev?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;React Dashboard: an Ultimate Guide&lt;/a&gt; to for a comprehensive all-in-one walkthrough exploring how to build a dashboard with Recharts and connect it to a backend.&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;a href="https://www.highcharts.com/blog/products/highcharts/" rel="noopener noreferrer"&gt;Highcharts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 Paid &amp;amp; non-commercial licenses / ⭐️ 9.8K / 📅 22 Oct 2020 / 🧱 43.2 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Highcharts is a JavaScript charting library based on SVG, with fallbacks to VML and canvas for old browsers.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Highcharts is good for large companies whose products rely heavily on data visualization. You can see the code on GitHub, try and use it for non-commercial purposes. And then you can purchase Highcharts license just for Hightcharts or Highcharts plugin for Stocks, Maps, or Gantt if you'd like to use it for commercial purposes. We'll cover those later in this post as well.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;📊 See &lt;a href="https://cube.dev/blog/react-highcharts-example/?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;React Highcharts Example with Cube.js&lt;/a&gt; for a practical tutorial on creating a dashboard with numerous Highcharts components.&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;a href="http://gionkunz.github.io/chartist-js/" rel="noopener noreferrer"&gt;Chartist.js&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 12.4K / 📅 11 Sep 2019 / 🧱 535 KB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Chartist.js is the product of a community that was disappointed about the abilities provided by other charting libraries.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This library is not as actively maintained as others, however, it still worths a mention because of its size with no dependencies. Less than a megabyte, wow!&lt;/p&gt;

&lt;p&gt;Just like others, it uses SVGs, it's flexible and it has clear separation of concerns, i. e., CSS is in CSS and JS is in JS, which may not fit all projects, considering that a lot of projects are using CSS-in-JS approach, yet it still deserves our attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://formidable.com/open-source/victory/" rel="noopener noreferrer"&gt;Victory&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 8.6K / 📅 1 Sep 2020 / 🧱 2.81 MB / 🔌 React&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;An ecosystem of composable React components for building interactive data visualizations.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Built by &lt;a href="https://formidable.com" rel="noopener noreferrer"&gt;Formidable Labs&lt;/a&gt;, a company behind such tools as &lt;a href="https://formidable.com/open-source/urql/" rel="noopener noreferrer"&gt;urqls&lt;/a&gt; and &lt;a href="https://formidable.com/open-source/spectacle/" rel="noopener noreferrer"&gt;Spectacle&lt;/a&gt;. Provides an opinionated, but fully overridable React components to use both in web and mobile (&lt;a href="https://github.com/FormidableLabs/victory-native" rel="noopener noreferrer"&gt;victory-native&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://uber.github.io/react-vis/" rel="noopener noreferrer"&gt;React-vis&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 7.6K / 📅 19 Apr 2019 / 🧱 1.81 MB / 🔌 React&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A collection of React components to render common data visualization charts, such as line/area/bar charts, heat maps, scatterplots, contour plots, hexagon heatmaps, pie and donut charts, sunbursts, radar charts, parallel coordinates, and tree maps.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This library is React-friendly, high-level and customisable, expressive and industry-strong, because it is backed by Uber, so chances are you'll get your answers in case you bump into an issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.amcharts.com/javascript-charts/" rel="noopener noreferrer"&gt;amCharts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 864 / 📅 18 Dec 2020 / 🧱 22.6 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A go-to library for data visualization. When you don't have time to learn new technologies.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not as popular as the rest, however, it's actively maintained and claims to be easy to use. It could be a good choice if you'd like to combine it with other data viz library for geo and timeline data. I'll cover those in Geo and Timeline sections.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;📊 See &lt;a href="https://dev.to/cubejs/slack-vibe-the-open-source-analytics-for-slack-2khl"&gt;Slack Vibe, the Open Source Analytics for Slack&lt;/a&gt; for a story of an analytical tool that uses amCharts to visualize Slack data.&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;a href="https://www.anychart.com" rel="noopener noreferrer"&gt;AnyCharts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 Paid &amp;amp; non-commercial licenses / ⭐️ 284 / 📅 29 Sep 2020 / 🧱 11.9 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Interactive JavaScript charts designed to be embedded and integrated into web, desktop, and mobile apps.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a paid library with non-commercial license, which could work well for a company who wants to save time and money on charts development, because a lot of charts pre-configured for you, just insert your data. Even such rear charts as Jump Line Chart, Sunburst Chart, or Circular Gauge.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Low-level and complex dataviz tools
&lt;/h1&gt;

&lt;p&gt;Here go tools for a more complicated and more advanced way to visualize your data. The main reason to go with this approach is either because none of the simple charts can be used to visualize your data, or you need to have more advanced interactivity on your elements.&lt;/p&gt;

&lt;p&gt;This approach requires a more advanced and specific knowledge in the area, e.g., SVG drawing or WebGL. Not everyone has access to such resource, so that's why there are libraries that expose a much simpler API for simple cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://d3js.org" rel="noopener noreferrer"&gt;D3.js&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 95.4K / 📅 22 Jan 2021 / 🧱 1 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;D3 (or D3.js) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas, and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is probably the most popular low-level data visualization library on the internet. There's a few keys to its popularity: first, its flexibility (many other data visualization tools are based on D3.js, we'll discuss them below); second, it's massive &lt;a href="https://observablehq.com/@d3/gallery" rel="noopener noreferrer"&gt;examples gallery&lt;/a&gt; which contains literally every visual you can imagine.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;📊 See &lt;a href="https://d3-dashboard.cube.dev?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;D3 Dashboard Tutorial&lt;/a&gt; for a good jump-starting point to learn how to use D3.js with a backend.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;&amp;lt;d3-based-tools&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And here are a few D3-based libraries: &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://plotly.com/javascript/" rel="noopener noreferrer"&gt;Plotly.js&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;💵 OSS / ⭐️ 12.7K / 📅 21 Dec 2020 / 🧱 60.4 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Built on top of D3.js and stack.gl, Plotly.js is a high-level, declarative charting library. plotly.js ships with over 40 chart types, including 3D charts, statistical graphs, and SVG maps.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Plotly allows you to combine the best of two words: simple api and power of a D3.js. It's good for quick prototyping as well as complex projects that require complicated graphs and need to be done in a timely manner.&lt;/p&gt;

&lt;p&gt;Plotly also has commercial offerings, but that's more in regard to its end-to-end development &amp;amp; deployment platform, Dash Enterprise.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://c3js.org" rel="noopener noreferrer"&gt;C3.js&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;💵 OSS / ⭐️ 9K / 📅 8 Aug 2020 / 🧱 1.46 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;D3-based reusable chart library.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://semiotic.nteract.io" rel="noopener noreferrer"&gt;Semiotic&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;💵 OSS / ⭐️ 2.1K / 📅 21 Jan 2021 / 🧱 2.77 MB / 🔌 React&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Semiotic is a data visualization framework combining React &amp;amp; D3&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://taucharts.com" rel="noopener noreferrer"&gt;Taucharts&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;💵 OSS / ⭐️ 1.9K / 📅 26 Feb 2020 / 🧱 1.54 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Taucharts is a data-focused JavaScript charting library based on D3 and designed with passion.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;/d3-based-tools&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://echarts.apache.org/en/index.html" rel="noopener noreferrer"&gt;Apache ECharts&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 44.9K / 📅 16 Jan 2021 / 🧱 36.3 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A Declarative Framework for Rapid Construction of Web-based Visualizations&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another open source charting library that has enormous pre-defined charts ready to be used. The reason I put it in this category, is because not only it has simple charts, like line and bar graphs, but also more complicated ones, like 3D globe, 3D lines, Scatter GL and others. Checkout their &lt;a href="https://echarts.apache.org/examples/en/index.html" rel="noopener noreferrer"&gt;example page&lt;/a&gt; and prepare to be amazed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dmitrybaranovskiy.github.io/raphael/" rel="noopener noreferrer"&gt;Raphaël&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 10.8K / 📅 14 Aug 2019 / 🧱 1.11 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Raphaël is a small JavaScript library that should simplify your work with vector graphics on the web&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Raphael is similar in D3.js in a way that it also allows you to draw your own svg graphics in html. E.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Creates canvas 320 × 200 at 10, 50&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;paper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Raphael&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Creates circle at x = 50, y = 40, with radius 10&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;circle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Sets the fill attribute of the circle to red (#f00)&lt;/span&gt;
&lt;span class="nx"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#f00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Sets the stroke attribute of the circle to white&lt;/span&gt;
&lt;span class="nx"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stroke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It hasn't been actively maintained and now most of the preference goes to D3.js. Yet it's still relevant and last release wasn't that long ago, so it does deserve a mention in the list.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Tables and data grids
&lt;/h1&gt;

&lt;p&gt;Some data is tabular and thus belongs to a table. But tables are more framework-specific, so it's hard to collect data tables for every framework in one post. However, here are a few options.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.ag-grid.com" rel="noopener noreferrer"&gt;ag-Grid&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS &amp;amp; paid / ⭐️ 6.9K / 📅 15 Jan 2021 / 🧱 n/a / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Advanced Data Grid / Data Table supporting JavaScript / React / AngularJS / Web Components&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or, as it claims on its website, "The Best JavaScript Grid in the World." It's packed with features and has both OSS and Enterprise versions with bindings for vanilla JS, React, Angular, Vue, and even Polymer.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://material-ui.com/components/data-grid/" rel="noopener noreferrer"&gt;Material UI Data Grid&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS &amp;amp; paid / ⭐️ 272 / 📅 26 Jan 2021 / 🧱 n/a / 🔌 React&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A fast and extendable data table and data grid for React.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a feature-rich component which is complementary to the whole &lt;a href="https://material-ui.com" rel="noopener noreferrer"&gt;Material UI&lt;/a&gt; set of React components. It's available in MIT and commercial versions, too.&lt;/p&gt;

&lt;p&gt;There's also some competition here. For the sake of comparison, I should mention the &lt;a href="https://ant.design/components/table/" rel="noopener noreferrer"&gt;data table&lt;/a&gt; component of Ant Design library, which is great to use if you need a complete set of UI components.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. Timeline charts &amp;amp; time-based tools
&lt;/h1&gt;

&lt;p&gt;Not all charting libraries include timelines and Gantt charts, they're quite unique in that sense. So if you have some time-based data to plot, then you may be interested in these libraries or plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.highcharts.com/blog/products/gantt/" rel="noopener noreferrer"&gt;Highcharts Gantt&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Similar to word clouds, you can have Highcharts Gantt plugin provided by the company. Check out out their page to learn more about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.amcharts.com/timeline-chart/" rel="noopener noreferrer"&gt;amCharts timeline&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Similar approach: get the main amCharts library and use additional timeline plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/visjs/vis-timeline" rel="noopener noreferrer"&gt;vis-timeline&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 3.3K / 📅 3 years ago / 🧱 n/a / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Create a fully customisable, interactive timelines and 2d-graphs with items and ranges.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an open-source alternative to the above two libs. It's quite flexible, well-designed, although its last release was about 3 years ago.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Geospatial and mapping tools
&lt;/h1&gt;

&lt;p&gt;Now we've arrived to geo data, the type of data that has a geographic component to it and should have map-based data visualizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://leafletjs.com" rel="noopener noreferrer"&gt;Leaflet&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 30K / 📅 4 Sep 2020 / 🧱 5.96 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Leaflet is the leading open-source JavaScript library for mobile-friendly interactive maps. Weighing just about 39 KB of gzipped JavaScript plus 4 KB of gzipped CSS code, it has all the mapping features most developers ever need.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Leaflet is lightweight, simple, and flexible, and is probably the most popular open-source mapping library at the moment. Leaflet was developed by Vladimir Agafonkin, who is currently in Mapbox (see below), and OSS community.&lt;/p&gt;

&lt;p&gt;There are numerous tutorials and guides on how to work with leaflet. So it's very cost and time efficient to start and keep working with Leaflet.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.mapbox.com" rel="noopener noreferrer"&gt;Mapbox&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 Free-ish &amp;amp; paid / ⭐️ 7.2K / 📅 28 Jan 2021 / 🧱 34 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Mapbox is a mapping and location cloud platform for developers.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These guys provide SDKs and APIs to companies like Foursquare, Lonely Planet, and Facebook. You can do a lot with Mapbox, and their APIs are a pleasure to work with, well-documented, and well-supported. Check out &lt;a href="https://docs.mapbox.com/mapbox-gl-js/example/" rel="noopener noreferrer"&gt;their examples page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Mapbox isn't completely free. It has a free tier for low volume apps. So it's free for up to 25,000 mobile users and 50,000 web loads. Beyond that you'd need to pay per additional requests.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;📊 See &lt;a href="https://mapbox-guide.cube.dev?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;JavaScript Map Data Visualization with Mapbox&lt;/a&gt; for a complete guide on Mapbox and different kinds of maps, from heatmaps to choropleths.&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;a href="https://developers.google.com/maps/documentation/javascript/visualization" rel="noopener noreferrer"&gt;Google Maps&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Google Maps can do a lot more than just show places on a map. You can map data across the world, create heatmaps, and even size circles.&lt;/p&gt;

&lt;p&gt;Google has a CDN URL you'd need to include in your project or use a framework-specific wrapper, like &lt;a href="https://www.npmjs.com/package/@react-google-maps/api" rel="noopener noreferrer"&gt;&lt;code&gt;@react-google-maps/api&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.amcharts.com/javascript-maps/" rel="noopener noreferrer"&gt;amCharts maps&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;If your project is already using amCharts, then you might be interested in using amCharts maps. There is no need to install anything extra, the main amCharts package already includes everything needed for creating a map visualization.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. Word clouds
&lt;/h1&gt;

&lt;p&gt;Not all data is quantifiable. Some data can only be illustrated with words. This is a perfect use case scenario for word clouds. A few libraries allow you to do those.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.jasondavies.com/wordcloud/" rel="noopener noreferrer"&gt;d3-cloud&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 3.3K / 📅 3 years ago / 🧱 n/a / 🔌 universal&lt;/p&gt;

&lt;p&gt;Great library to use if you already have D3.js as a part of your code ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.amcharts.com/demos/word-cloud/" rel="noopener noreferrer"&gt;amCharts word clouds&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is a plugin for amCharts. See more details about amCharts earlier in this post. To create word cloud you'd need the main library + the &lt;code&gt;wordCloud&lt;/code&gt; plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.highcharts.com/docs/chart-and-series-types/word-cloud-series" rel="noopener noreferrer"&gt;Highcharts word clouds&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Similar to amCharts word clouds, if you want to make word clouds with Highcharts, you'd need &lt;code&gt;modules/wordcloud.js&lt;/code&gt; module.&lt;/p&gt;

&lt;h1&gt;
  
  
  7. 3D visualization tools
&lt;/h1&gt;

&lt;p&gt;While every tool we've observed above was focused on planar, two-dimensional data visualizations, there's one more dimension to it 😀 The following tools help create 3D visuals.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://threejs.org" rel="noopener noreferrer"&gt;Three.js&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 66.6K / 📅 24 Dec 2020 / 🧱 24.4 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The aim of the project is to create an easy to use, lightweight, 3D library with a default WebGL renderer. The library also provides Canvas 2D, SVG and CSS3D renderers in the examples.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three.js is a different take on visualizing data. It's used to create and display animated 3D computer graphics in a web browser using WebGL. Three.js can be used not only visualize data, but also create such pieces of art as &lt;a href="https://threejs.org/examples/#webgl_animation_keyframes" rel="noopener noreferrer"&gt;Littlest Tokyo&lt;/a&gt;. The data can take ANY form your like. Literary. It's only limited by your creativity.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/visjs/vis-graph3d" rel="noopener noreferrer"&gt;vis-graph3d&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;💵 OSS / ⭐️ 44.9K / 📅 16 Jan 2021 / 🧱 36.3 MB / 🔌 universal&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Graph3d is an interactive visualization chart to draw data in a three dimensional graph. You can freely move and zoom in the graph by dragging and scrolling in the window.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a very interesting lightweight alternative to D3.js and Three.js.&lt;/p&gt;

&lt;h1&gt;
  
  
  Final remarks
&lt;/h1&gt;

&lt;p&gt;Honestly, it's impossible to include everything in one list. But I hope this post helped you get a better idea about the "big players" and types of data visualizations.&lt;/p&gt;

&lt;p&gt;It's worth mentioning that to communicate your data better to your user, it's always a good idea to mix and match different types of data visualizations. Moreover, you can also have a date formatting library (such as &lt;a href="https://momentjs.com" rel="noopener noreferrer"&gt;Moment.js&lt;/a&gt;) or number formatting library (such as &lt;a href="http://numeraljs.com" rel="noopener noreferrer"&gt;numeral.js&lt;/a&gt;) that could accompany your graphs and highlight any special or outstanding number in your dataset. &lt;/p&gt;

&lt;p&gt;Also, if you need an API to serve data to your charts or data visualization components, please consider to use &lt;a href="https://cube.dev?utm_source=dev-to&amp;amp;utm_medium=post&amp;amp;utm_campaign=dataviz-ecosystem-2021" rel="noopener noreferrer"&gt;Cube.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any particular recommendation or personal experience with any of the listed libraries to share, please feel free to comment below! 🙌&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>dataviz</category>
    </item>
    <item>
      <title>Custom Commerce.js input field for Sanity.io</title>
      <dc:creator>Niki</dc:creator>
      <pubDate>Wed, 06 Jan 2021 19:22:49 +0000</pubDate>
      <link>https://dev.to/snikidev/custom-commerce-js-input-fields-for-sanity-io-11pg</link>
      <guid>https://dev.to/snikidev/custom-commerce-js-input-fields-for-sanity-io-11pg</guid>
      <description>&lt;p&gt;A headless CMS paired with headless eCommerce is a perfect match for marketing and content teams. The headless commerce provider manages the product, cart, and checkout logic, with the content provider managing assets, data, and copy for the rest of the site.&lt;/p&gt;

&lt;p&gt;In this post I’ll show you how to connect your products from Commerce.js to your Sanity CMS. Doing this will allow content admins to add products into any page, blog, or app without leaving the Sanity CMS.&lt;/p&gt;

&lt;p&gt;This post was inspired by the article &lt;a href="https://www.sanity.io/guides/how-to-make-a-custom-input-component" rel="noopener noreferrer"&gt;how to make a custom input component&lt;/a&gt;. Following Thomas’ instructions, you’ll be able to create all sorts of input fields, and perhaps publish them later on npm and/or the &lt;a href="https://www.sanity.io/plugins?category=inputComponent" rel="noopener noreferrer"&gt;Sanity plugin page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this makes sense
&lt;/h2&gt;

&lt;p&gt;Decoupling your commerce and content allows you to customize your marketing site and shop any way you like: custom landing pages, custom page layouts, custom checkout journeys, custom data to be displayed etc. With a headless CMS and commerce powering your business site, your team will have greater flexibility and freedom when it comes to personalizing content.&lt;/p&gt;

&lt;p&gt;Like Commerce.js, &lt;a href="https://www.sanity.io/" rel="noopener noreferrer"&gt;Sanity.io&lt;/a&gt; is a headless provider. Sanity makes it easy to customise structured content, tailor it for a given format, and display it on any front-end. This includes custom input fields and different kinds of pages templates.&lt;/p&gt;

&lt;p&gt;The ability to select products from inside the Sanity CMS allows content teams to create targeted campaign pages that reference only certain products, publish new category pages for newly arrived products, and design a fancy slider section with featured products. Custom inputs can be created for all of these by linking your Commerce.js products and publishing content around those products.&lt;/p&gt;

&lt;p&gt;In this post we'll only cover how to get Commerce.js products into Sanity through a custom input field. The rest of the creative process I'll leave to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up Sanity locally
&lt;/h2&gt;

&lt;p&gt;To set up a new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i -g @sanity/cli 
sanity init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted, select “Blog schema”, and the rest could be defaults.&lt;/p&gt;

&lt;p&gt;Now get in your project folder and start the local server on &lt;code&gt;localhost:3333&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd &amp;lt;project-title&amp;gt;
yarn start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create a schema
&lt;/h2&gt;

&lt;p&gt;Let’s start from the end, and tell our post schema to display our field (that doesn’t exist yet). Go to &lt;code&gt;schemas/post.js&lt;/code&gt; and insert our field inside the existing &lt;code&gt;fields&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Related products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commercejsProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commercejsProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll call them “Related products”, because these could be the products that are related to the content in the blog post.&lt;/p&gt;

&lt;p&gt;You may have noticed that we’ve used field &lt;code&gt;type&lt;/code&gt; that doesn’t exist (see &lt;a href="https://www.sanity.io/docs/schema-types" rel="noopener noreferrer"&gt;default field types&lt;/a&gt;). So we need to create it inside the separate &lt;code&gt;schemas/commercejsProducts.js&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../components/CommercejsProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commercejsProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Commercejs Products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;singleProduct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;singleProduct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;],&lt;/span&gt;
 &lt;span class="na"&gt;inputComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this schema we will tell Sanity what component to use to display our data. Right now we’re importing a non-existent &lt;code&gt;CommercejsProducts&lt;/code&gt; component from the &lt;code&gt;components&lt;/code&gt; folder. We’ll get back to that later. But for now, we’re using the type &lt;code&gt;singleProduct&lt;/code&gt; that also doesn’t exist.&lt;/p&gt;

&lt;p&gt;We need to create a schema for every single one level deep &lt;code&gt;Object&lt;/code&gt;, because Sanity GraphQL cannot read nested objects, unless they’re registered globally. So if we want to create more complex structures, ie objects that go multiple-levels in depth, then we need to create a schema for every object that we’d like to nest, give it a name and only then use that in an object (like we did here). Now let’s create &lt;code&gt;schemas/singleProduct.js&lt;/code&gt; schema now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;singleProduct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;last_updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permalink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quantity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically, we can put all that inside our &lt;code&gt;schemas/commercejsProducts.js&lt;/code&gt; schema, nest it, and we’ll be able to even see that working on &lt;code&gt;localhost&lt;/code&gt;. But when you’ll try to deploy that to GraphQL, it will give you an error that would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Encountered anonymous inline object "singleProduct" for field/type "commercejsProducts". To use this field with GraphQL you will need to create a top-level schema type for it. See https://docs.sanity.io/help/schema-lift-anonymous-object-type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if we have a property in our Commerce.js product that is another object, we’ll create a separate schema for it, give it a name and reference it in the parent object. Like for the price we’ll have this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formatted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formatted_with_symbol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formatted_with_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So go through the product object, see what properties you need and create schemas for them. In the end, we’ll need to import our custom schemas in &lt;code&gt;schemas/schema.js&lt;/code&gt; file and register them with the &lt;code&gt;createSchema&lt;/code&gt; function:&lt;/p&gt;

&lt;p&gt;First, we must import the schema creator&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createSchema&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part:@sanity/base/schema-creator&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;schemaTypes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all:part:@sanity/base/schema-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;commercejsProducts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./commercejsProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;singleProduct&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./singleProduct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createSchema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schemaTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
   &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="c1"&gt;// Commerce.js product schemas&lt;/span&gt;
   &lt;span class="nx"&gt;commercejsProducts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="nx"&gt;singleProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schemas are now ready! 👏&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the input component
&lt;/h2&gt;

&lt;p&gt;Now go to your components folder and open your &lt;code&gt;CommercejsProducts.js&lt;/code&gt; component that we left hanging while doing schemas.&lt;/p&gt;

&lt;p&gt;Let’s make a dummy component and see how it looks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="nx"&gt;goes&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Voila! You can see our component at the bottom of all other fields in the Post type page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9n859pimasfif640aj8n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F9n859pimasfif640aj8n.png" alt="simple cammerce.js sanityio field" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let’s make it look like a Sanity field with the &lt;code&gt;Fieldset&lt;/code&gt; component. We’re importing it from Sanity &lt;code&gt;parts&lt;/code&gt;. You can read more about parts from the &lt;a href="https://www.sanity.io/docs/parts" rel="noopener noreferrer"&gt;Sanity blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In addition to making it look like a Sanity field, let’s make it behave like a Sanity field. Let’s save some of our data that we receive from Commerce.js to Sanity by firing an &lt;code&gt;onChange&lt;/code&gt; event &lt;code&gt;onMount&lt;/code&gt; of our component. We’ll take the &lt;code&gt;onChange&lt;/code&gt; event from &lt;code&gt;PatchEvent&lt;/code&gt;. &lt;a href="https://www.sanity.io/docs/http-patches" rel="noopener noreferrer"&gt;PatchEvent&lt;/a&gt; helps us synchronise all the events that are happening in Sanity across different browsers/users. So that means two users can be editing the same field of the same post and they’ll be aware of the changes.&lt;/p&gt;

&lt;p&gt;So let’s install our Commerce.js SDK and make that happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @chec/commerce.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fieldset&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part:@sanity/components/fieldsets/default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PatchEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unset&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part:@sanity/form-builder/patch-event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Commerce&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@chec/commerce.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commerce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Commerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pk_184625ed86f36703d7d233bcf6d519a4f9398f20048ec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createPatchFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
 &lt;span class="nx"&gt;PatchEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;commerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;
       &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createPatchFrom&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]));&lt;/span&gt;
     &lt;span class="p"&gt;})&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;There was an error fetching the products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Fieldset&lt;/span&gt;
     &lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Fieldset&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you open the changes sidebar, you’ll notice that &lt;code&gt;onMount&lt;/code&gt; fired &lt;code&gt;onChange&lt;/code&gt; event and we had unsaved changes to our blog post data.&lt;/p&gt;

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

&lt;p&gt;Click “Publish” and let’s see that from the GraphQL end, shall we?&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;sanity graphql deploy&lt;/code&gt; to deploy your GraphQL. You’ll be asked if you’d like to deploy it, say ‘Y’.&lt;/p&gt;

&lt;p&gt;That command will give us a URL where we can view the GraphQL schema, something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;sanity_id&amp;gt;.api.sanity.io/v1/graphql/production/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to that URL and query posts with our products using the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;allPost&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;commercejsProducts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;id&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="nx"&gt;description&lt;/span&gt;
      &lt;span class="nx"&gt;quantity&lt;/span&gt;
      &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;raw&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you should see this data returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allPost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commercejsProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod_NqKE50BR4wdgBL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Kettle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Black stove-top kettle&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quantity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;45.5&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod_kpnNwAMNZwmXB3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Book on grid systems&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quantity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;13.5&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now we’re getting the right data, let’s make it work with an input field so our content admins/managers can actually search and select the products they want 😁.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create input field
&lt;/h2&gt;

&lt;p&gt;We want to create a nice user experience, so let’s use async &lt;code&gt;react-select&lt;/code&gt; field&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add react-select
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fieldset&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part:@sanity/components/fieldsets/default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PatchEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unset&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;part:@sanity/form-builder/patch-event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Commerce&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@chec/commerce.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncSelect&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-select/async&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;debounce&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lodash.debounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commerce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Commerce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pk_184625ed86f36703d7d233bcf6d519a4f9398f20048ec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createPatchFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
 &lt;span class="nx"&gt;PatchEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputValue&lt;/span&gt;
     &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;commerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inputValue&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;commerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;}));&lt;/span&gt;

   &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createPatchFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
 &lt;span class="p"&gt;};&lt;/span&gt;

 &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Fieldset&lt;/span&gt;
     &lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;markers&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AsyncSelect&lt;/span&gt;
       &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="nx"&gt;cacheOptions&lt;/span&gt;
       &lt;span class="nx"&gt;isMulti&lt;/span&gt;
       &lt;span class="nx"&gt;defaultOptions&lt;/span&gt;
       &lt;span class="nx"&gt;loadOptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loadOptions&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Fieldset&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;CommercejsProducts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things are happening here.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Since all inputs are controlled in Sanity, we need to pass default value to our select component as a prop.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createPatchFrom&lt;/code&gt; function that handles the &lt;code&gt;onChange&lt;/code&gt; event, that we have in the &lt;code&gt;handleChange&lt;/code&gt; function. It fires when we select the item from the suggested list.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AsyncSelect&lt;/code&gt; component accepts &lt;code&gt;loadOptions&lt;/code&gt; that we need to put into &lt;code&gt;debounce&lt;/code&gt; so that we don’t send HTTP requests immediately on every keystroke.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;loadOptions&lt;/code&gt; has Commerce.js SDK package that is initialized at the top of our component, that gets the list of products, and depending whether this function is called by &lt;code&gt;onMount&lt;/code&gt; or &lt;code&gt;onChange&lt;/code&gt;, we’re displaying either full list of products or products that the user searched for.&lt;/li&gt;
&lt;li&gt;Inside the &lt;code&gt;loadOptions&lt;/code&gt; function we need to loop through the elements and create &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt; properties, because our &lt;code&gt;react-select&lt;/code&gt; component is looking for them. Don’t worry about it being passed higher, and it being found in GraphQL search, because Sanity takes out all the fields that are not declared in the schema.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;NOTE: DO NOT store your Commerce.js public key in your project, this is done solely for demonstration purposes. Keep your secret in &lt;code&gt;.env&lt;/code&gt; variables.&lt;/p&gt;

&lt;p&gt;And there we go, we have a multi select input component that allows us to link Commerce.js products to our blog post.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;This is a very simple component that lists some of the properties from a &lt;a href="https://commercejs.com/docs/api/?shell#products" rel="noopener noreferrer"&gt;Commerce.js product&lt;/a&gt;, you can enhance it any way you’d like, for example you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add more (or less) properties for your products; &lt;/li&gt;
&lt;li&gt;Change the dropdown input to a grid format with images, to make it easier for your content/marketing team to navigate through your products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also create another input field that lists categories, if you want those. &lt;a href="https://commercejs.com/docs/sdk/products#retrieve-category" rel="noopener noreferrer"&gt;Commerce.js has an API&lt;/a&gt; for it as well.&lt;/p&gt;

&lt;p&gt;If then you’d like to publish your &lt;code&gt;sanity-commercejs-input&lt;/code&gt; component(s) to npm, have a look at &lt;a href="https://www.youtube.com/watch?v=l9DeFfsv-vU&amp;amp;feature=youtu.be&amp;amp;t=3949" rel="noopener noreferrer"&gt;Espen’s and Knut’s stream&lt;/a&gt;, Espen goes through some instructions and tips starting from 1:05:49.&lt;/p&gt;

&lt;p&gt;You should now be able to see the power of using headless content with headless commerce. Integrating the two empowers content editors when it comes to creating and updating eCommerce related content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/snikidev/sanity-commercejs-products-input" rel="noopener noreferrer"&gt;GitHub repo 🔗&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
