<?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: Akos</title>
    <description>The latest articles on DEV Community by Akos (@akoskm).</description>
    <link>https://dev.to/akoskm</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%2F336917%2Fce3961ef-ba8d-4759-a58a-6d9671640b11.jpg</url>
      <title>DEV Community: Akos</title>
      <link>https://dev.to/akoskm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akoskm"/>
    <language>en</language>
    <item>
      <title>A Brief History of Web Development</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Wed, 27 Dec 2023 11:16:31 +0000</pubDate>
      <link>https://dev.to/akoskm/a-brief-history-of-web-development-51hc</link>
      <guid>https://dev.to/akoskm/a-brief-history-of-web-development-51hc</guid>
      <description>&lt;p&gt;The most thrilling days of web development are happening now. We have access to the best tools, making website and web app development an unparalleled experience. Faster, more efficient solutions are being developed monthly, causing the entire field to evolve like there's no tomorrow.&lt;/p&gt;

&lt;p&gt;But it hasn't always been like this.&lt;/p&gt;

&lt;p&gt;About twenty years ago, when I made that first website for the kindergarten where my mother worked, the coolest thing was the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/marquee"&gt;marquee&lt;/a&gt; effect that I just recently learned has been deprecated. 💔&lt;/p&gt;

&lt;p&gt;Meet Macromedia Dreamweaver, where web development started for me:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--H9zmtzJB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcy2kdkp3n6ymeejkae2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--H9zmtzJB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcy2kdkp3n6ymeejkae2.png" alt="Macromedia Dreamweaver 8" width="665" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But once I figured I could not only drag and drop things on the canvas, but there's this HTML view, and some say there's even a PHP server somewhere that can manipulate and store data, there was no going back.&lt;/p&gt;

&lt;p&gt;This is where my history of web development started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request-response
&lt;/h2&gt;

&lt;p&gt;I considered PHP as an extension on top of HTML altough this wasn't completely true.&lt;/p&gt;

&lt;p&gt;I could already create mindblowing static sites using the right amount of marquee (RIP 💔), but tapping into a data source on the server made me feel like Neo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT username FROM users WHERE id = 1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$conn&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Welcome, "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$user_username&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"!&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not sure if this is how PHP looks today. Hashnode's AI code generator gave this to me, and it certainly looks like something I wrote!&lt;/p&gt;

&lt;p&gt;Using this tech, I built my first SaaS right before the year I went to the university. It was a phpBB-like forum for our middle school. Using the same stack, I wrote an even bigger app used as a nationwide registration system for a national mathematical competition.&lt;/p&gt;

&lt;p&gt;I didn't know what SaaS or being productive as a programmer meant back then because I just dabbled with these things.&lt;/p&gt;

&lt;p&gt;15 years later, it's an absolute &lt;em&gt;yes&lt;/em&gt;. HTML, CSS, PHP, and MySQL made me extremely productive. And I haven't used JavaScript at this time. I used maybe snippets I found on the internet, but I showed no interest in learning it because I didn't need it.&lt;/p&gt;

&lt;p&gt;The apps I've built in those years mostly looked like current government websites:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KVhT24eP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/imhmfmqeuf2jemdprp9b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KVhT24eP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/imhmfmqeuf2jemdprp9b.png" alt="a beautiful government website" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember clicking that submit button a couple of times?&lt;/p&gt;

&lt;h2&gt;
  
  
  Ajax responses, the first spinners
&lt;/h2&gt;

&lt;p&gt;It didn't take me long to realize the value of Ajax. Suddenly, I didn't need full page reloads to show dropdowns with different values or to disable a Submit button when the request was already processing.&lt;/p&gt;

&lt;p&gt;Ajax improved the user experience by adding a slight cost to the maintenance.&lt;/p&gt;

&lt;p&gt;At this time, most of my applications were still 99% PHP, HTML, and CSS. Fast forward 4 years to my graduation.&lt;/p&gt;

&lt;p&gt;I learned a whole lot of Java at the university and continued selling websites as a side hustle until I landed my first developer job in January 2012.&lt;/p&gt;

&lt;p&gt;JSF (Jakarta Server Faces) was popular then, and the company I worked for was bullish on enterprise Java.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Looking at you, ReponseTaskServerAdapterPropertyObserverSetter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We built web apps that looked like desktop apps, which were incredibly painful to develop, deploy, and maintain.&lt;/p&gt;

&lt;p&gt;They looked like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uhdgvFb_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/muiwucfivqhc0x5d8lry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uhdgvFb_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/muiwucfivqhc0x5d8lry.png" alt="web page built with JSF" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JSF wanted to be a silver bullet; we didn't use much pure HTML because JSF libraries had components for buttons, inputs, autocompletes, etc. But it was a world full of promises and a horrifying development experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SPA revolution
&lt;/h2&gt;

&lt;p&gt;SPAs came like a savior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ukkST0pe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yafxhy91czfti7du6gez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ukkST0pe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yafxhy91czfti7du6gez.png" alt="freight train meme" width="500" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we realized we could expose REST APIs on the server and write our entire client in JavaScript, our productivity reached HTML, CSS, and PHP levels again.&lt;/p&gt;

&lt;p&gt;The client apps resembled what you would have today in a React application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// bundle.js
var message = new Message();
var messageView = new MessageView({ model: message });

$('#root').append(messageView.render().el);

// index.html
&amp;lt;html lang="en"&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script src="/bundle.js"&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nothingburger and a client app packed into a ginormous &lt;code&gt;bundle.js&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The bundle got to the client, but it had to fetch some data to render the app first.&lt;/p&gt;

&lt;p&gt;And this is when our apps started to have more of these:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMGJhdGpvbWI4cDlxcHRkYjdsaDBlZHV3Z29vM2JkbGM3N29zemw1MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/y1ZBcOGOOtlpC/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMGJhdGpvbWI4cDlxcHRkYjdsaDBlZHV3Z29vM2JkbGM3N29zemw1MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/y1ZBcOGOOtlpC/giphy.gif" alt="remix demo" width="150" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spinner Hell
&lt;/h2&gt;

&lt;p&gt;But it was not just the spinners. This client-rendering behavior introduced a whole lot of other issues, like layout shifts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://web.dev/static/articles/cls/video/web-dev-assets/layout-instability-api/layout-instability2.webm"&gt;https://web.dev/static/articles/cls/video/web-dev-assets/layout-instability-api/layout-instability2.webm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bundle sizes got out of control unless you were really, really careful. You could have gzipped a date library that added 50kB to your bundle size when you could have converted the timestamp from the db into a human-readable format before sending the response!&lt;/p&gt;

&lt;p&gt;Websites felt slower, but the spinner kept spinning, giving us some hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  RSC, Remix
&lt;/h2&gt;

&lt;p&gt;Up until now, we simply didn't have better ways to deal with this problem other than maybe using UI skeletons. They weren't a perfect solution either, and they looked weird when they got out of sync with the rest of the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ilLIVsKh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2os9350tgv3ljuo9v4fz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ilLIVsKh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2os9350tgv3ljuo9v4fz.png" alt="Skeleton Layout" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;React Server Components are an idea to tackle this problem with an initial implementation, but not much acceptance from the community yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hashnode.com/@ryanflorence"&gt;Ryan Florence&lt;/a&gt; wrote an excellent blog post about the key differences, both in theory and in practice, between RSC and Remix in &lt;a href="https://remix.run/blog/react-server-components"&gt;React Server Components and Remix&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To sum up, React Server Components follow the &lt;em&gt;Render-Fetch&lt;/em&gt; waterfall. This means the component gets rendered first and then it fires a fetch.&lt;/p&gt;

&lt;p&gt;In contrast, Remix uses the &lt;em&gt;Fetch, Then Render&lt;/em&gt; waterfall, which is closer in behavior to what we had before the spinner armageddon.&lt;/p&gt;

&lt;p&gt;Remix's loader does all the fetches, and when they're ready, only the result (component populated with the result of the loader) is returned to the browser.&lt;/p&gt;

&lt;p&gt;Remix acts like an SPA on subsequent navigations and fetches only the necessary data to show the view.&lt;/p&gt;

&lt;p&gt;See what happens after I switch between &lt;code&gt;/me&lt;/code&gt; and &lt;code&gt;/settings&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kWsk_w9T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmdp975o3nqwhguha17p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kWsk_w9T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmdp975o3nqwhguha17p.gif" alt="remix demo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hitting &lt;code&gt;/me&lt;/code&gt; for the first time, retrieves the simple HTML page already pre-populated with the data no need for loading spinners.&lt;/p&gt;

&lt;p&gt;Kind of like in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT username FROM users WHERE id = 1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$conn&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Welcome, "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$user_username&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"!&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But switching back and forth between the &lt;code&gt;/settings&lt;/code&gt; and the &lt;code&gt;/me&lt;/code&gt; page fetches only what's necessary a JSON containing the data needed to display &lt;code&gt;/me&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this changes everything
&lt;/h2&gt;

&lt;p&gt;Instead of going with the overused "Next.js is PHP now" memes, I want you to think about where it all started and how far we've come.&lt;/p&gt;

&lt;p&gt;This is an evolution.&lt;/p&gt;

&lt;p&gt;It's all about constantly changing, trying new things, keeping what worked, and leaving behind what didn't.&lt;/p&gt;

&lt;p&gt;From marquee 💔, PHP/HTML/CSS, to JSF, and the painfully slow deployment times that made web development with Java an absolute nightmare, back to a much simpler render request-response cycle.&lt;/p&gt;

&lt;p&gt;Today, we can pick from the best tools the industry offers.&lt;/p&gt;

&lt;p&gt;They all provide a lot better development experience than I could have imagined 20 or 10 years ago.&lt;/p&gt;

&lt;p&gt;Congrats to the Next.js and the Remix teams for their fantastic job and community support.&lt;/p&gt;

&lt;p&gt;I hope the React team will finalize the RSC Next year and everyone can use it to their fullest potential to build even better apps.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>career</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Your Guide to Testing React Components</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Thu, 21 Dec 2023 09:21:13 +0000</pubDate>
      <link>https://dev.to/akoskm/your-guide-to-testing-react-components-313n</link>
      <guid>https://dev.to/akoskm/your-guide-to-testing-react-components-313n</guid>
      <description>&lt;p&gt;This blog post gives you a bird's eye view of the different ways to test your React components and some ideas for converting your tests from Enzyme to React Testing Library.&lt;/p&gt;

&lt;p&gt;One of my most significant projects this year was moving the Enzyme tests of a gigantic fintech application to React Testing Library (that's a mouthful, so I'll use &lt;strong&gt;RTL&lt;/strong&gt; in the text).&lt;/p&gt;

&lt;p&gt;While there are tools to automate this process, we didn't want to simply write the equivalent of the enzyme tests in RTL but also improve the existing tests and cover previously untested behaviors. We learned a lot about how to find alternatives to the "Enzyme way" of testing, like prop assertions.&lt;/p&gt;

&lt;p&gt;So this year, I wrote mainly about testing React Components.&lt;/p&gt;

&lt;p&gt;As a result, the Testing React series was born, containing eight articles so far:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://akoskm.com/series/react-testing"&gt;https://akoskm.com/series/react-testing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, let's dive into it and see what to use and when!&lt;/p&gt;

&lt;h2&gt;
  
  
  Component Props
&lt;/h2&gt;

&lt;p&gt;If you read only the first article of the series, you're already good to go. &lt;a href="https://dev.to/akoskm/how-to-test-props-in-react-testing-library-306l"&gt;How to test props in React Testing Library&lt;/a&gt; presents you the simplest way to prop testing: mocking your component and, instead, the real thing returning a string that prints out the props, something like this:&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="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./PermissionsContainer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;profileId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`This is PermissionsContainer profileId:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this works great for most components, sometimes you run into two issues with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the prop is an object that's huge when stringified&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the rest of the test relies on the behavior of the component that you mocked&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why I edited the blog post and provided an alternative to this approach that lets you assert prop values but also call the actual component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permissionsContainerMock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PermissionsContainer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kr"&gt;any&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;permissionsContainerMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders Container with the correct props&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;permissionsContainerMock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&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;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requireActual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../components/Profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&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;Profile&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&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;span class="c1"&gt;// expect(permissionsContainerMock).toHaveBeenCalledWith(your props);});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two approaches will get you covered for prop testing and turning those Enzyme &lt;code&gt;component.prop&lt;/code&gt; tests into RTL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component Hooks
&lt;/h2&gt;

&lt;p&gt;Hooks are incredible mechanisms in React that help you abstract and share some logic between components.&lt;/p&gt;

&lt;p&gt;Usually, you want to test them with your component code, but some hooks are too complex to be verified with the component.&lt;/p&gt;

&lt;p&gt;This is why the RTL team gave us &lt;code&gt;renderHook&lt;/code&gt; that lets you render your hook and verify how it behaves while loading, receiving new arguments, etc.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&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;usePermissionsHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1234-fake-4567-uuid&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="nf"&gt;toEqual&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dev.to/akoskm/how-to-test-custom-hooks-with-react-testing-library-and-jest-3e4l-temp-slug-405409"&gt;How to Test Custom Hooks with React Testing Library and Jest&lt;/a&gt; gives you a complete walkthrough on testing React Hooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing the DOM
&lt;/h2&gt;

&lt;p&gt;While finding DOM elements based on CSS queries with Enzyme was easy, this is not so simple because of RTL's philosophy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classnames
&lt;/h3&gt;

&lt;p&gt;But sometimes, you want to ensure that an alert has the &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;success&lt;/code&gt; styling.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/akoskm/how-to-test-a-classname-with-jest-and-react-testing-library-lph-temp-slug-4378435"&gt;How to test a className with Jest and React testing library&lt;/a&gt;, I present you two ways to do this, but my preferred method is finding the text inside the alert using the &lt;code&gt;selector&lt;/code&gt; argument of the RTL query:&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;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Alert&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;"error"&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Something went wrong"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong&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;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.alert.alert-error&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;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This differs from what you used to do with Enzyme because RTL won't let you look only for the CSS selectors. First, you must specify a piece of text that will be visible to the user which is aligned with RTL's philosophy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;React Testing Library aims to test the components how users use them. Users see buttons, headings, forms and other elements by their role, not by their id, class, or element tag name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Complex structures
&lt;/h3&gt;

&lt;p&gt;Testing the DOM becomes increasingly more complex when you move on to headings or table structures because RTL supports many roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/akoskm/test-complex-dom-structures-with-react-testing-library-5hak-temp-slug-6746317"&gt;Test complex DOM structures with React Testing Library&lt;/a&gt; provides you with examples for testing headings and table cells.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshots
&lt;/h3&gt;

&lt;p&gt;If none of these methods work, you can still use Snapshot testing; that's a great way to assert complex DOM structures that rarely change. I found an excellent application: checking if the component rendered the right SVG.&lt;/p&gt;

&lt;p&gt;SVGs are sometimes too long to put into a test, and once you know you have the SVG for your "Home" icon, it rarely changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/akoskm/understanding-jest-snapshots-a-beginners-guide-to-snapshot-testing-46d3-temp-slug-3135488"&gt;Jest Snapshots: Beginner's Guide to Snapshot Testing&lt;/a&gt; helps you get started with snapshot testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Better
&lt;/h2&gt;

&lt;p&gt;Not exclusive to RTL or React Component testing, but after seeing so many tests and good/bad patterns, I felt the need for an article discussing strong vs. weak tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/akoskm/how-to-write-stronger-unit-tests-with-jest-1o3e-temp-slug-7478702"&gt;How to Write Stronger Unit Tests with Jest&lt;/a&gt; gives you hints on how to write better tests with almost no extra effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Faster
&lt;/h2&gt;

&lt;p&gt;On the bright side, if you've reached this point, you have many tests! Handling their slowness might seem like a good problem to have.&lt;/p&gt;

&lt;p&gt;Well-known testing mechanisms such as beforeEach/all can encapsulate the repeated parts of your tests, but remember, they add to the runtime of each test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/akoskm/how-to-speed-up-your-integration-tests-4ghp-temp-slug-6395277"&gt;How to speed up your integration tests&lt;/a&gt;, through a practical example, shows you how we speeded up our CI time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Finally, I'd like to balance out all this testing with one of my favorite tweets:&lt;/p&gt;

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

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



&lt;/p&gt;

&lt;p&gt;Don't dive straight into writing unit tests if you don't have an automated method for verifying the essential functionality of your app.&lt;/p&gt;

&lt;p&gt;Spend your and your team's time where it makes the most sense.&lt;/p&gt;

&lt;p&gt;Do everything with moderation, including unit tests.&lt;/p&gt;

</description>
      <category>jest</category>
      <category>react</category>
      <category>webdeb</category>
      <category>testing</category>
    </item>
    <item>
      <title>Creating a CI/CD Pipeline with Docker and GitHub Actions: A Step-by-Step Guide</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Fri, 15 Dec 2023 15:52:58 +0000</pubDate>
      <link>https://dev.to/akoskm/creating-a-cicd-pipeline-with-docker-and-github-actions-a-step-by-step-guide-3063</link>
      <guid>https://dev.to/akoskm/creating-a-cicd-pipeline-with-docker-and-github-actions-a-step-by-step-guide-3063</guid>
      <description>&lt;p&gt;We all &lt;em&gt;love&lt;/em&gt; shipping new features and building stuff probably the main reason we're programmers!&lt;/p&gt;

&lt;p&gt;And web development doesn't have to be any more complicated unless... something breaks.&lt;/p&gt;

&lt;p&gt;When something breaks, it can bite you for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You must find what broke and when this is quite challenging. It could be that the thing that doesn't work now has been broken for weeks or even months, but you just realized it!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bugs cost you money imagine launching some Cyber Monday deals for the users of your fresh SaaS with the payment system broken.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As someone who has experienced both issues, setting up a CI/CD pipeline is one of the first tasks I undertake for projects I intend to launch.&lt;/p&gt;

&lt;p&gt;But what is CI/CD, and why are they almost always mentioned together?&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminology
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CI
&lt;/h3&gt;

&lt;p&gt;CI stands for Continuous integrations. When you change your application code, you'd like it to work with the rest of the code. CI helps you verify this.&lt;/p&gt;

&lt;p&gt;Without automation, you'd test all app functionality manually before shipping the change to the users. With automation, these changes are tested as part of CI.&lt;/p&gt;

&lt;p&gt;Running a linter or the TypeScript compiler is a great way to start your CI workflow. They catch early bugs fast and prevent your more resource-intensive steps, such as unit or integration tests, from running.&lt;/p&gt;

&lt;p&gt;Once the CI step is done, you're confident your new code works with the rest of the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  CD
&lt;/h3&gt;

&lt;p&gt;CD is &lt;em&gt;showtime&lt;/em&gt;. 🚀 It's time to ship this change to your users, usually by deploying the app on a remote server. But for desktop applications, it means packaging and uploading the executables to storage.&lt;/p&gt;

&lt;p&gt;Because of this, I use CD as an acronym, Continuous Delivery, and not Deployment not everything is a web app and can be deployed.&lt;/p&gt;

&lt;p&gt;Whatever you do, at this point, CI has already given you the confidence that you're ready to move forward and introduce this change to your users.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;p&gt;GitHub Actions is a feature of GitHub for CI/CD within your GitHub repository. It allows you to create custom workflows to build, test, package, release, or deploy your code based on specific triggers such as a push event, a pull request, commits, etc. Workflows consist of one or more &lt;strong&gt;jobs&lt;/strong&gt; , which are series of &lt;strong&gt;steps&lt;/strong&gt; that run commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding CI to your existing project
&lt;/h2&gt;

&lt;p&gt;First, we'll build a simple CI workflow that will run &lt;code&gt;tsc&lt;/code&gt; when we make a commit. Creating a workflow starts with creating a workflow file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Workflow file
&lt;/h3&gt;

&lt;p&gt;Jump into your project's root directory and create the file &lt;code&gt;.github/workflows/main.yml&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;mkdir -p .github/workflowstouch .github/workflows/main.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command creates the folder structure (make sure you use &lt;code&gt;-p&lt;/code&gt; otherwise you'd have to create the &lt;code&gt;.workflows&lt;/code&gt; folder separately) and the second creates an empty file.&lt;/p&gt;

&lt;p&gt;Open this file and then proceed to the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding steps to your workflow file
&lt;/h3&gt;

&lt;p&gt;There are three elements of a workflow file you need to define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Deploy to Digital Oceanon:jobs:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name the name of your workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;on a rule that describes when this workflow is triggered.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;jobs - you can have as many jobs as you want. GitHub Actions jobs run in parallel. This means that if you have multiple jobs defined in your workflow, they will all start running simultaneously when the workflow is triggered. You can also make them dependent on each other with the &lt;code&gt;needs&lt;/code&gt; keyword, but we'll explore that next time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The steps listed in jobs can be many things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;single line commands such as copying files or logging parameters&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;running npm, yarn, or any other executable on the system&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;running other GitHub actions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example of a workflow file
&lt;/h3&gt;

&lt;p&gt;If you want to work with your repository in the workflow and what else you would do anyway the workflow must access it.&lt;/p&gt;

&lt;p&gt;We already mentioned that GitHub Actions can run other GitHub actions. Luckily, there's a GitHub action for checking out the current repository's source code. You'll be using it 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;jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that you have the source code in place, you can do &lt;code&gt;npm install&lt;/code&gt; and do some type-checking.&lt;/p&gt;

&lt;p&gt;Here's how the workflow will look like that does all this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Deploy to Digital Oceanon: push: branches: - main # Set a branch to deploy when pushedjobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install run: npm install - name: Run Typecheck run: npm run typecheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing the workflow file
&lt;/h3&gt;

&lt;p&gt;It's time to commit and push these changes to our main branch. Once you open the commit history, you'll notice a appeared next to your commit message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---3ARgWtQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mudw9hr6fkc66cbklk1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---3ARgWtQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mudw9hr6fkc66cbklk1j.png" alt="simple workflow passing in github actions" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This indicates that the commit triggered a workflow run. Click the to explore further.&lt;/p&gt;

&lt;p&gt;You recognize the steps from our workflow file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NdvNbsV_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/81ayuvjfi9mrkqg2oi0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NdvNbsV_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/81ayuvjfi9mrkqg2oi0v.png" alt="screenshot showing all workflow steps" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great job! You just triggered your workflow run that will automatically check for TypeScript errors every time a commit is made to the main branch!&lt;/p&gt;

&lt;h2&gt;
  
  
  Server Setup - AWS/DigitalOcean
&lt;/h2&gt;

&lt;p&gt;Now that we have some basic checks in place and the confidence in our codebase has grown, it's time to deploy our application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment options
&lt;/h3&gt;

&lt;p&gt;There are countless options for where to deploy your application, and new ones are added almost every month.&lt;/p&gt;

&lt;p&gt;Choosing a service provider is not an easy task, but I have two go-to solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DigitalOcean - I use it most of the time. Suitable for small MVPs as well as for more complex apps. Super versatile and easy to use. Simply pricing, fixed-price instances.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS - endless solutions, a lot more complex, but it's more feature-rich. DigitalOcean runs on AWS. Pay-as-you-go model - takes some time to understand, but it's flexible.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Luckily, both let you rent a single server instance that you can connect to with SSH and deploy your stuff, set up a database, and other applications that will support your main app, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Overview
&lt;/h3&gt;

&lt;p&gt;On a high level, for both providers, you have to do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Generate a pub/private key pair locally&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a server on the platform EC2 for AWS, Droplet on DigitalOcean&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tell the server to trust the pub key I'll describe this later&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSH into the server and deploy your app using Docker&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Create a Server
&lt;/h3&gt;

&lt;p&gt;You'll need a passwordless keypair for both providers.&lt;/p&gt;

&lt;p&gt;To create one use &lt;code&gt;ssh-keygen -t ed25519 -a 100 -f ~/.ssh/deploy_key_aws&lt;/code&gt;. To make it passwordless, simply don't specify a password when prompted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sign up for AWS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to &lt;strong&gt;Key pairs&lt;/strong&gt; and do &lt;strong&gt;Import key pair&lt;/strong&gt;. This is the only way I found to import locally created keypairs to AWS and avoid using their pem keys - that you have to download, store, etc. Click Browse under &lt;strong&gt;Key pair file&lt;/strong&gt; and upload &lt;code&gt;~/.ssh/deploy_key_aws.pub&lt;/code&gt;. Click &lt;strong&gt;Import key pair&lt;/strong&gt;. You can also follow the official docs &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html#how-to-generate-your-own-key-and-import-it-to-aws"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an EC2 Instance, and at the &lt;strong&gt;Key pair (login)&lt;/strong&gt; section, specify the keypair you imported. If you have an existing instance, see the docs &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/replacing-key-pair.html"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Launch the instance&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sign up for DigitalOcean with my &lt;a href="https://m.do.co/c/f54dc1760678"&gt;referral link&lt;/a&gt; or by going to &lt;a href="https://www.digitalocean.com/"&gt;www.digitalocean.com&lt;/a&gt;. In the sidebar, click Droplets, choose Create Droplet, and follow the wizard. You'll find the detailed instructions on creating your first Droplet from the Control Panel &lt;a href="https://docs.digitalocean.com/products/droplets/how-to/create/#create-a-droplet-in-the-control-panel"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the public key value. You can print it to the console using &lt;code&gt;cat ~/.ssh/deploy_key_aws.pub&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a Droplet&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect to the Droplet using their Console&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the file &lt;code&gt;.ssh/authorized_keys&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the value of Step 2. as a new line in this file and save the file&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Docker Setup
&lt;/h3&gt;

&lt;p&gt;Neither the DigitalOcean Droplet nor the AWS EC2 instance will have docker installed by default, so you have to follow the official guide to install and set them up correctly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This could be a guide for itself, but GitHub Copilot told me these are the steps and they worked.&lt;/p&gt;

&lt;p&gt;Log into your AWS EC2 machine and run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum update -ysudo yum install dockersudo service docker startsudo usermod -a -G docker ec2-usersudo chkconfig docker on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and log back in again to pick up the new &lt;code&gt;docker&lt;/code&gt; group permissions. But you don't have to do this because we'll deploy from GitHub Actions anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They actually have a guide! 👏&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04#step-1-installing-docker"&gt;https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04#step-1-installing-docker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't read the whole thing, just Step 1 and Step 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding CD
&lt;/h2&gt;

&lt;p&gt;Now, you'll connect your GitHub workflow to one of these servers and deploy your application.&lt;/p&gt;

&lt;p&gt;You'll use Docker Hub to store the images and Docker to deploy them.&lt;/p&gt;

&lt;p&gt;You'll find a detailed guide on Dockerizing your Node.js applications in &lt;a href="https://dev.to/akoskm/how-to-publish-docker-images-to-docker-hub-with-github-actions-4pgj"&gt;&lt;strong&gt;How to Publish Docker Images to Docker Hub with GitHub Actions&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For this guide, I'll assume you already read the previous guide and successfully dockerized the app you want to deploy.&lt;/p&gt;

&lt;p&gt;The previous guide ended with building and pushing the image to Docker Hub, pulling and running it locally.&lt;/p&gt;

&lt;p&gt;This time, instead of running it locally, you'll SSH into the AWS EC2 or Digital Ocean Droplet, pull and deploy the app there.&lt;/p&gt;

&lt;p&gt;To do this, extend your workflow file &lt;code&gt;.github/workflows/main.yml&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Deploy to Digital Oceanon: push: branches: - main # Set a branch to deploy when pushedjobs: deploy: runs-on: ubuntu-latest steps: # this is unchanged # checkout &amp;amp; typescript checks # pushing the image to docker hub - see the link above - name: SSH and deploy to AWS EC2 uses: appleboy/ssh-action@master with: host: ${{ secrets.AWS_EC2_IP }} username: ${{ secrets.AWS_EC2_USER }} key: ${{ secrets.AWS_EC2_SSH_KEY }} script: | docker pull akoskm/saas:latest docker stop saas_container docker rm saas_container docker run -d --name saas_container --env-file .env -p 3000:3000 akoskm/saas:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This GitHub action uses &lt;code&gt;appleboy/ssh-action&lt;/code&gt; to log into the target machine specified by &lt;code&gt;AWS_EC2_IP&lt;/code&gt; and using the credentials &lt;code&gt;AWS_EC2_USER&lt;/code&gt; and &lt;code&gt;AWS_EC2_SSH_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These will be your Action Secrets, which you must create by going to repository Settings. Select &lt;strong&gt;Secrets and Variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt; in the sidebar and click &lt;strong&gt;New repository secret&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RtCCfSsD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hi2g7h538vc0imw80sy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RtCCfSsD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hi2g7h538vc0imw80sy6.png" alt="adding a repository secret in GitHub" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how to find the values for all three secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AWS_EC2_IP&lt;/code&gt; - &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-to-linux-instance.html#connection-prereqs-get-info-about-instance"&gt;Get information about your instance&lt;/a&gt; to find out the IP address of your EC2 - screenshots included.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AWS_EC2_USER&lt;/code&gt; - the value of this, as I'm writing the blog post, is &lt;code&gt;ec2-user&lt;/code&gt;. If this changes, it'll probably be updated in &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-linux-inst-ssh.html"&gt;Connect to your Linux instance from Linux or macOS using SSH&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AWS_EC2_SSH_KEY&lt;/code&gt; - this is the value of the passwordless key we created in &lt;strong&gt;Create a Server&lt;/strong&gt;. Run &lt;code&gt;cat ~/.ssh/deploy_key_aws&lt;/code&gt; and use that as the value.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing CD
&lt;/h3&gt;

&lt;p&gt;Push a new commit to the main branch and watch the workflow trigger. Compared to what you were seeing in &lt;strong&gt;Testing the workflow file&lt;/strong&gt; previously, you should have these additional steps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SXiREe5k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xzb7159c16p4pm4wi0ro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SXiREe5k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xzb7159c16p4pm4wi0ro.png" alt="Complete workflow passing" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations, you just created a fully integrated CI/CD workflow for your application!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/fdyZ3qI0GVZC0/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/fdyZ3qI0GVZC0/giphy.gif" alt="proud Ron Gif" width="499" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and further improvements
&lt;/h2&gt;

&lt;p&gt;CI/CD doesn't stop here! While this was enough to get you started, here are some more ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;include in the workflow your unit and integration tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;create a workflow that deploys to a staging server from a &lt;code&gt;development&lt;/code&gt; branch and a workflow that deploys to production from &lt;code&gt;main&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;try creating multiple jobs inside the same workflow file. You could create a &lt;code&gt;static-checks&lt;/code&gt; job that runs npm install, build, typechecks, and lint, and a &lt;code&gt;deploy&lt;/code&gt; job that builds the image and deploys it, but only if &lt;code&gt;static-checks&lt;/code&gt; is successful. See &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds"&gt;&lt;code&gt;jobs.&amp;lt;job_id&amp;gt;.needs&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good luck with taking your app development workflow to the next level.&lt;/p&gt;

&lt;p&gt;I understand this process is not trivial, so if you have any questions, please reach out to me on X &lt;a href="https://x.com/akoskm"&gt;&lt;strong&gt;@akoskm&lt;/strong&gt;&lt;/a&gt; or in the comments.&lt;/p&gt;

&lt;p&gt;Thanks,&lt;/p&gt;

&lt;p&gt;Akos&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>docker</category>
    </item>
    <item>
      <title>The Ultimate Full Stack Framework for 2024: Remix</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Wed, 13 Dec 2023 09:46:17 +0000</pubDate>
      <link>https://dev.to/akoskm/the-ultimate-full-stack-framework-for-2024-remix-2f31</link>
      <guid>https://dev.to/akoskm/the-ultimate-full-stack-framework-for-2024-remix-2f31</guid>
      <description>&lt;p&gt;It's been eleven years since I set foot in the world of full-stack development. This path was not chosen out of preference, but rather out of necessity. Working with small companies from the start, the only chance to advance was to be as versatile as possible.&lt;/p&gt;

&lt;p&gt;I appreciate the technology I can work with today, as it hasn't always been this impressive.&lt;/p&gt;

&lt;p&gt;Between 2012 and 2014, our local environment's reload time after making changes to HTML or CSS was measured in &lt;em&gt;minutes&lt;/em&gt;. Java, JSF, and J2EE marked my entry into full-stack development. Although it felt enterprise-like on the backend, frontend development was a nightmare.&lt;/p&gt;

&lt;p&gt;This forced us to explore ways of shipping frontends separately from the backends. This is how I discovered, Backbone.js, Angular, and later React. Since 2014, I have used CRA, Next.js, and Razzle around React, but nothing felt as right as Remix.&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separation of concerns
&lt;/h2&gt;

&lt;p&gt;If I think about these years, this separation has always existed in some form.&lt;/p&gt;

&lt;p&gt;There was the server and the client. Two separate projects. Context switching, or at least project-switching all the time.&lt;/p&gt;

&lt;p&gt;Let's say you were working on a simple TODO app, and you wrote this frontend code:&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="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;useState&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;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;TodoComponent&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="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;setTitle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todo&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&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;title&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="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="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&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;"text"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&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="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTitle&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;target&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="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Enter todo title"&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="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add Todo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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="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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TodoComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This is great, but is there anything else &lt;code&gt;/todo&lt;/code&gt; expects besides the title? What I would do at this point is open up the API endpoint implementing &lt;code&gt;POST /todo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTodo&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;~/todoStore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todo&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="nx"&gt;req&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="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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;description&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&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;Missing required fields&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;addTodo&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&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 then, I would figure out that I must provide &lt;code&gt;description&lt;/code&gt; too. Back to the frontend code, add a new field, send it to the backend, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Remix way
&lt;/h3&gt;

&lt;p&gt;In Remix, when I want to create a simple Todo form that accepts some text input and sends it to the backend, I do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;everything in the same file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;without registing an endpoint in a router&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;without importing all kinds of wrappers and reading framework-specific documentation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remix builds heavily on existing Web APIs, which I think is the reason why it can stay so lean.&lt;/p&gt;

&lt;p&gt;If you write a Todo form with a single input &lt;em&gt;and&lt;/em&gt; an endpoint that records that input, it would all go into the same file and look like this:&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;json&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;@remix-run/node&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActionFunctionArgs&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;@remix-run/node&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Form&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;@remix-run/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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTodo&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;~/todoStore&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionFunctionArgs&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;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;todo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Index&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post"&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="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Item name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&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="nt"&gt;input&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;"text"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&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="nt"&gt;input&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;"text"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"description"&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="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;Form&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;Sorcery, right?&lt;/p&gt;

&lt;p&gt;Backend &lt;em&gt;and&lt;/em&gt; the frontend, next to each other.&lt;/p&gt;

&lt;p&gt;Here's what else you see above:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form&lt;/strong&gt; - a Remix way of doing &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. If you would submit this plain form with a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; you would actually make a &lt;code&gt;POST&lt;/code&gt; request to the server. However, with &lt;code&gt;Form&lt;/code&gt; Remix realizes this &lt;code&gt;POST&lt;/code&gt; intent, intercepts it, and turns the &lt;code&gt;POST&lt;/code&gt; into a &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;action&lt;/strong&gt; - this is the default action invoked by submitting the only form on this page. Inside &lt;code&gt;action&lt;/code&gt; you're writing your backend code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/9r75ILTJtiDACKOKoY/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/9r75ILTJtiDACKOKoY/giphy.gif" alt="Magic Gif"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  No more states
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Form Inputs
&lt;/h3&gt;

&lt;p&gt;Totally unexpected, but throughout the building of my &lt;a href="https://github.com/akoskm/saas" rel="noopener noreferrer"&gt;SaaS boilerplate&lt;/a&gt; with Remix, I used &lt;code&gt;useState&lt;/code&gt; once in the entire project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F3yyi5wb2l9uvrr8rid8a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F3yyi5wb2l9uvrr8rid8a.png" alt="useState in my project"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was surprising, given that I have a Sign-up form separate for users and organizations, a Sign-in form, a User invite, and an edit form.&lt;/p&gt;

&lt;p&gt;That's a lot of forms!&lt;/p&gt;

&lt;p&gt;Yet, as you saw from the example above, because of how Remix handles form submissions, in most cases your usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&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;e&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;setEmail&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;target&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;isn't necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading states
&lt;/h3&gt;

&lt;p&gt;Indicating the loading state and disabling buttons at the right time are all something we've done.&lt;/p&gt;

&lt;p&gt;And we probably did it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&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;handleSubmit&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="nf"&gt;setLoading&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;Remix handles this (and a bunch of other common use cases) with its built-in hooks. One of them is &lt;a href="https://remix.run/docs/en/main/hooks/use-navigation" rel="noopener noreferrer"&gt;&lt;code&gt;useNavigation&lt;/code&gt;&lt;/a&gt;. It's a simple hook that returns you the current state of the page navigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;idle&lt;/strong&gt; - the page is idling, no requests are being made&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;submitting&lt;/strong&gt; - an &lt;code&gt;action&lt;/code&gt; is being called, likely after a form submission&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;loading&lt;/strong&gt; - The loaders for the next routes are being called to render the next page - we didn't discuss loaders here, but &lt;a href="https://remix.run/docs/en/main/hooks/use-loader-data#useloaderdata" rel="noopener noreferrer"&gt;check them out&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Based on &lt;code&gt;navigation.state&lt;/code&gt;, you always know if there is an action running on your page or if it's just idling. If it's the latter, it's probably safe to remove that loading indicator or re-enable that button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standards
&lt;/h2&gt;

&lt;p&gt;I highly recommend you go over the &lt;a href="https://remix.run/docs/en/main/start/tutorial" rel="noopener noreferrer"&gt;30-minute tutorial&lt;/a&gt; of Remix that explains how it works in-depth.&lt;/p&gt;

&lt;p&gt;Remix builds on "old-school web" instead of reinventing the wheel. The way a form POST is turned into fetch requests is a good example of that.&lt;/p&gt;

&lt;p&gt;It encourages the use of existing Web APIs. This is from their landing page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remix is a full stack web framework that lets you focus on the user interface and work back through web standards to deliver a fast, slick, and resilient user experience. People are gonna love using your stuff.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which I think summarizes what Remix represents.&lt;/p&gt;

&lt;p&gt;It also encourages &lt;em&gt;you&lt;/em&gt; to think differently and use web standards. This is how, probably for the first time ever, I found a platform API for something that I always used a library for:&lt;/p&gt;

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

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



&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I gave Remix a shot without any big expectations, and it blew me away. Guess this is when they say &lt;em&gt;to&lt;/em&gt; &lt;em&gt;underpromise&lt;/em&gt; &lt;em&gt;and&lt;/em&gt; &lt;em&gt;overdeliver&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I'll keep using Remix for my future projects. The one I'm building is a &lt;a href="https://github.com/akoskm/saas" rel="noopener noreferrer"&gt;SaaS boilerplate&lt;/a&gt; that combines Remix with FusionAuth, a customer authentication and authorization platform. Another tool that makes developers' lives awesome.&lt;/p&gt;

&lt;p&gt;Huge congrats to the Remix team.&lt;/p&gt;

&lt;p&gt;Make sure to check out their &lt;a href="https://remix.run/" rel="noopener noreferrer"&gt;website&lt;/a&gt; and join their &lt;a href="https://rmx.as/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
      <category>remix</category>
    </item>
    <item>
      <title>How to Publish Docker Images to Docker Hub with GitHub Actions</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Wed, 13 Dec 2023 08:56:03 +0000</pubDate>
      <link>https://dev.to/akoskm/how-to-publish-docker-images-to-docker-hub-with-github-actions-4pgj</link>
      <guid>https://dev.to/akoskm/how-to-publish-docker-images-to-docker-hub-with-github-actions-4pgj</guid>
      <description>&lt;p&gt;Docker has been gaining popularity ever since because it provides a simple way to package your code and ship it as a runnable image.&lt;/p&gt;

&lt;p&gt;Docker also brings you Docker Hub, where storing your images is free.&lt;/p&gt;

&lt;p&gt;Combined with GitHub Actions, you automate the process of building and pushing images to Docker Hub.&lt;/p&gt;

&lt;p&gt;This is one of the most cost-effective ways to deploy MVPs and most of your web apps. This is how I'm doing it, so I wanted to share my process here.&lt;/p&gt;

&lt;p&gt;I assume you have a Node.js application you want to Dockerize and deploy, and you have Docker installed on the machine or where you want to deploy the image. If not, see &lt;a href="https://docs.docker.com/engine/install/"&gt;Install Docker Engine&lt;/a&gt; (I also use Docker Desktop locally).&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminology
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Docker image: An image is a read-only template for creating a container. It's often based on another image with added customizations. For instance, you might build an image using the Ubuntu base but include your application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dockerfile: simple syntax for defining the steps needed to create and run the image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker container: a runnable instance of the image.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create the Dockerfile
&lt;/h2&gt;

&lt;p&gt;Open your Node.js project, and in the root directory, create a file &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This file describes how your application should be packaged and run. Some of these commands are self-explanatory, but I added comments to make things clear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Use a base image with the latest Node.js LTS installedFROM node:20# Set the working directory inside the containerWORKDIR /app# Copy package.json and package-lock.json to the working directoryCOPY package*.json ./# Install dependenciesRUN npm install# Copy the rest of the application code to the working directoryCOPY . .# Build the appRUN npm run build# Start the appCMD ["npm", "start"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Building and running your image locally
&lt;/h3&gt;

&lt;p&gt;Before we dive into how to automatize this, let's make sure that this actually works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To create a Docker image using the Dockerfile you just made, run the following command in your command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build -t my-image:latest .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format is always &lt;code&gt;[name of the image]:[tag]&lt;/code&gt;. This will help you better organize your images and align with how other docker images are named.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jyZzFCNu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v209elfg679ezfsj6aoe.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jyZzFCNu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v209elfg679ezfsj6aoe.gif" alt="building a docker image" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the command runs, type &lt;code&gt;docker images&lt;/code&gt; to list the images on your machine. I have a bunch of them, but you can see &lt;code&gt;my-image&lt;/code&gt; that I've just built:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5o071kWs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rp9cczawvtwls90fczhp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5o071kWs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rp9cczawvtwls90fczhp.png" alt="checking the build image using docker images" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running the image will start a container based on the image. You can run &lt;code&gt;docker ps&lt;/code&gt; to list the currently running containers.&lt;/p&gt;

&lt;p&gt;To run the image you just created, type this into your command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run my-tag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine docker containers like machines on your local network. They have their network inside the container that, by default, you can't access. If you spin up a Node.js app inside the container, you can't access it unless you expose the container's specific port to your machine. So, for example, if your Node.js app is running on port 3000, you might want to run the above command with the &lt;code&gt;-p&lt;/code&gt; argument that does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -p 3000:3000 my-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command starts a new Docker container just as before, and the &lt;code&gt;-p&lt;/code&gt; option maps port 3000 of the container to port 3000 of the host.&lt;/p&gt;

&lt;p&gt;If you want to supply environment variables from an &lt;code&gt;.env&lt;/code&gt; file to your application, use the &lt;code&gt;--env-file&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;To run the image in background mode, add the &lt;code&gt;-d&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -d -p 3000:3000 --env-file .env my-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To list the running docker containers after this command type &lt;code&gt;docker ps&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R6-fCOk7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/il5mvwd33y5j2b4p6ysz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R6-fCOk7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/il5mvwd33y5j2b4p6ysz.gif" alt="The running container shows up in docker ps." width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I ran the image using the above command, and it showed up in the running containers after I typed &lt;code&gt;docker ps&lt;/code&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a DockerHub account
&lt;/h2&gt;

&lt;p&gt;To easily deploy our images on different locations, for example, on an AWS EC2 instance or a DigitalOcean droplet, you need a way to distribute them.&lt;/p&gt;

&lt;p&gt;This is where Docker registries come into play. Docker Hub is a public registry that anyone can use, and Docker searches for images on Docker Hub by default. You can also run your private registry.&lt;/p&gt;

&lt;p&gt;You can create a Docker Hub account &lt;a href="https://hub.docker.com/"&gt;here&lt;/a&gt; if you haven't already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a GitHub Actions workflow
&lt;/h2&gt;

&lt;p&gt;Now that you have ensured our image works, it's time to automate building and uploading it to Docker Hub using GitHub Actions.&lt;/p&gt;

&lt;p&gt;If you're new to GitHub Actions, check out &lt;a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions"&gt;Understanding GitHub Actions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Start by creating a &lt;code&gt;.github/workflows/main.yml&lt;/code&gt; file in your project's directory with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Deployon: push: branches: - main # Set a branch to deploy when pushedjobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install run: npm install - name: Run Typecheck run: npm run typecheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what's happening above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The workflow triggers the &lt;code&gt;deploy&lt;/code&gt; job on a push into the &lt;code&gt;main&lt;/code&gt; branch&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The deploy job checks out the codebase and runs some checks on it. You do this because the following steps are expensive - they might last several minutes - and you don't want to package anything broken. This is the best time for your static analysis tools.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Push the image to Docker Hub
&lt;/h2&gt;

&lt;p&gt;After the checks have passed, let's publish our image to Docker Hub. Extend your workflow with two more steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Deployon: # No changesjobs: deploy: runs-on: ubuntu-latest steps: # No changes - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: push: true tags: akoskm/my-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, you log in to your Docker Hub account using your username and the token you created as described in &lt;a href="https://docs.docker.com/security/for-developers/access-tokens/"&gt;Create and manage access tokens&lt;/a&gt;. Notice that I added &lt;code&gt;akoskm&lt;/code&gt; as a prefix to my image name and tag. You'll have to use your Dockerhub username here.&lt;/p&gt;

&lt;p&gt;Then, build and push the docker image to the Docker Hub repository.&lt;/p&gt;

&lt;p&gt;After this is complete, you'll find the new image in your DockerHub Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7YzOCb92--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u2x8k1lb52o5490m01yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7YzOCb92--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u2x8k1lb52o5490m01yv.png" alt="Image uploaded to Docker Hub repository" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull and run the image from DockerHub
&lt;/h2&gt;

&lt;p&gt;Now that you pushed your image to DockerHub, you can pull it on any machine with Docker installed. As mentioned earlier, Docker searches for images on Docker Hub by default.&lt;/p&gt;

&lt;p&gt;So, for example, after logging in to your AWS EC2, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker pull akoskm/my-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TOpAjMqF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gd6bho782ghespk1j1t6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TOpAjMqF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gd6bho782ghespk1j1t6.gif" alt="pulling the image on the target machine" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, run your image as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -d -p 3000:3000 --env-file .env akoskm/my-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;I hope you found this article helpful and that it will make your next app launch easier!&lt;/p&gt;

&lt;p&gt;If you enjoyed the article, please consider liking it and, more importantly, sharing it with those who might find it useful!&lt;/p&gt;

&lt;p&gt;Want to see this in action? Check out my &lt;a href="https://github.com/akoskm/saas"&gt;SaaS boilerplate app&lt;/a&gt; that I'm building with Remix!&lt;/p&gt;

&lt;p&gt;For questions, suggestions, or help, reach out to me on X &lt;a href="https://x.com/akoskm"&gt;@akoskm&lt;/a&gt; or in the comments.&lt;/p&gt;

&lt;p&gt;Thanks,&lt;/p&gt;

&lt;p&gt;Akos&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to handle multiple form actions in Remix</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Thu, 07 Dec 2023 08:30:10 +0000</pubDate>
      <link>https://dev.to/akoskm/how-to-handle-multiple-form-actions-in-remix-3d5a</link>
      <guid>https://dev.to/akoskm/how-to-handle-multiple-form-actions-in-remix-3d5a</guid>
      <description>&lt;p&gt;Form actions in Remix are handled by the &lt;code&gt;action&lt;/code&gt; function that runs on your server. They work very much like loaders, except they're called when you make a &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, or &lt;code&gt;PUT&lt;/code&gt; to your route.&lt;/p&gt;

&lt;p&gt;Remix's action/loader structure is incredibly straightforward and easy to grasp! If you've got some experience with data flow in client-server applications, you'll find this a piece of cake. But before we answer how we can handle multiple form actions in Remix, let's understand how the data flow works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Remix Fullstack Data Flow
&lt;/h3&gt;

&lt;p&gt;In the Remix Fullstack Data Flow, the &lt;code&gt;loader&lt;/code&gt; fetches data from the backend and passes that data to your component. The &lt;code&gt;action&lt;/code&gt; passes your component data to the backend. After the &lt;code&gt;action&lt;/code&gt; finishes, loaders are revalidated and return the current state of the backend to the component.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kTHl8uBw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxe923z2u83uskzpq6ug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kTHl8uBw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxe923z2u83uskzpq6ug.png" alt="Remix Fullstack Data Flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is how Remix keeps your backend and frontend data in sync without any additional code to do refetches.&lt;/p&gt;

&lt;p&gt;Also, in Remix each route has one &lt;code&gt;loader&lt;/code&gt; and one &lt;code&gt;action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The issue arises when, on the client, you want to handle different functionalities, and each functionality would require a different action on the backend.&lt;/p&gt;

&lt;p&gt;Let's take a look at this simple Todo app:&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;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;loader&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getTodos&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;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;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionFunctionArgs&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;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;todo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Index&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;todos&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLoaderData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;loader&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;actionData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useActionData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;&amp;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="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;"max-w-md"&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="nt"&gt;h1&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;"text-2xl pb-10"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Todos&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&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;Form&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post"&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;"flex flex-col gap-4"&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="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&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="nt"&gt;input&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;"text"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt; &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"off"&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="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;Form&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="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;todos&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;todo&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&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;todo&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;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-lg pt-5"&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;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;li&lt;/span&gt;&lt;span class="p"&gt;&amp;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="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="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;There is nothing fancy here, just an input and a button to submit a todo item to your list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with multiple actions
&lt;/h2&gt;

&lt;p&gt;Let's say we want to extend the functionality with a button to remove all items with a single click:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post"&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;"flex flex-col gap-4"&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="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&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="nt"&gt;input&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;"text"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt; &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"off"&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="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Clear&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;Form&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;But because we have only one &lt;code&gt;action&lt;/code&gt; handler, it doesn't matter which of these buttons we click, the action result is the same:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NueX_NAZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2tst5g5m7o1jc7y68mga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NueX_NAZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2tst5g5m7o1jc7y68mga.png" alt="Delete Todos with incorrect behavior" width="470" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;To work around this, we'll add two new attributes to each of these buttons: &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I like to use &lt;code&gt;name="intent"&lt;/code&gt; (reminds me of Android's Intent) and a custom &lt;code&gt;value&lt;/code&gt; describing the action we want to run:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post"&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;"flex flex-col gap-4"&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="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&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="nt"&gt;input&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;"text"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"todo"&lt;/span&gt; &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"off"&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="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"intent"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"intent"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"clear"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Clear&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;Form&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;Now, whenever we submit our form data, the request's &lt;code&gt;request.formData&lt;/code&gt; will contain an additional field &lt;code&gt;formData.get("intent")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can use this value to decide what to do next:&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;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;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionFunctionArgs&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;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;todo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&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;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;intent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clear&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clearTodos&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&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="s2"&gt;Unexpected action&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And finally, here's how the UI works after taking into consideration the value of &lt;code&gt;intent&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hMMn-sw5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dyebwerjum188reu354p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hMMn-sw5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dyebwerjum188reu354p.png" alt="Delete Todos, fixed behavior" width="470" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can experiment with the above pattern in the &lt;a href="https://codesandbox.io/p/devbox/handle-many-actions-hhd63z?file=%2Fapp%2Froot.tsx&amp;amp;embed=1&amp;amp;layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clpth9y6z00093b6a47evigv0%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clpth9y6y00023b6a0ptapyjc%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clpth9y6y00063b6au87dkya7%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clpth9y6y00083b6ahs600e8c%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clpth9y6y00023b6a0ptapyjc%2522%253A%257B%2522id%2522%253A%2522clpth9y6y00023b6a0ptapyjc%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clpthg7ks00023b6af7lqe632%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A9%252C%2522startColumn%2522%253A1%252C%2522endLineNumber%2522%253A9%252C%2522endColumn%2522%253A1%257D%255D%252C%2522filepath%2522%253A%2522%252Fapp%252Froutes%252F_index.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clpthnbob00023b6afp7ygakm%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A20%252C%2522startColumn%2522%253A5%252C%2522endLineNumber%2522%253A20%252C%2522endColumn%2522%253A5%257D%255D%252C%2522filepath%2522%253A%2522%252Fpackage.json%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522clptizdiv00023b6ao80s4v39%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A29%252C%2522startColumn%2522%253A12%252C%2522endLineNumber%2522%253A29%252C%2522endColumn%2522%253A12%257D%255D%252C%2522filepath%2522%253A%2522%252Fapp%252Froot.tsx%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clptizdiv00023b6ao80s4v39%2522%257D%252C%2522clpth9y6y00083b6ahs600e8c%2522%253A%257B%2522id%2522%253A%2522clpth9y6y00083b6ahs600e8c%2522%252C%2522activeTabId%2522%253A%2522clpthny38003w3b6arfianvh0%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A3000%252C%2522id%2522%253A%2522clpthny38003w3b6arfianvh0%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522path%2522%253A%2522%252F%253Findex%2522%257D%255D%257D%252C%2522clpth9y6y00063b6au87dkya7%2522%253A%257B%2522id%2522%253A%2522clpth9y6y00063b6au87dkya7%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clpth9y6y00033b6afelr81jd%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522build%2522%257D%252C%257B%2522id%2522%253A%2522clpth9y6y00043b6ay8dapjjv%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%257D%252C%257B%2522id%2522%253A%2522clpth9y6y00053b6aronu3r99%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522typecheck%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clpth9y6y00043b6ay8dapjjv%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D"&gt;Codesandbox&lt;/a&gt; I created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://remix.run/docs/en/main/start/tutorial"&gt;Remix Tutorial&lt;/a&gt; - I highly recommend you do the ~30-minute long, comprehensive, official tutorial explaining basic Remix concepts such as data mutations, data loading, routing, styling helpers, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://remix.run/docs/en/main/start/community"&gt;Community&lt;/a&gt; - check out the Remix community links, including their Discord server and a vast collection of Remix-related links.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/akoskm/the-ultimate-full-stack-framework-for-2024-remix-55e2-temp-slug-2472531"&gt;The Ultimate Full Stack Framework for 2024: Remix&lt;/a&gt; - Remix blew my mind with its simplicity and philosophy. After using it only for a few days, I wrote about why it is my big bet for 2024.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article, please like it and share it with developers who would find it useful.&lt;/p&gt;

&lt;p&gt;Thanks,&lt;br&gt;
Akos&lt;/p&gt;

</description>
      <category>remix</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to clear form after submit in Remix</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Mon, 04 Dec 2023 08:30:10 +0000</pubDate>
      <link>https://dev.to/akoskm/how-to-clear-form-after-submit-in-remix-29ah</link>
      <guid>https://dev.to/akoskm/how-to-clear-form-after-submit-in-remix-29ah</guid>
      <description>&lt;p&gt;Clearing a form in React after submission is typically accomplished by setting the states containing the input values to an empty string.&lt;/p&gt;

&lt;p&gt;In the case of a simple TodoList component, this means something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TodoList&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="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;setTitle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&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;handleSubmit&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="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="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todo&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&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;title&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&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="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&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="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTitle&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;target&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="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="nt"&gt;button&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;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add Todo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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="nt"&gt;form&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;If you saw my post &lt;a href="https://dev.to/akoskm/the-ultimate-full-stack-framework-for-2024-remix-55e2-temp-slug-2472531"&gt;The Ultimate Full Stack Framework for 2024: Remix&lt;/a&gt;, you know that with Remix, you can completely avoid most of the states and still reset the form after the submission is complete.&lt;/p&gt;

&lt;p&gt;Let's see how!&lt;/p&gt;

&lt;h2&gt;
  
  
  The useActionData hook
&lt;/h2&gt;

&lt;p&gt;Remix provides many helpful hooks for submitting forms, inspecting if the page is loading, or automatically capturing output values of backend actions.&lt;/p&gt;

&lt;p&gt;The hook that helps you with the latter is &lt;a href="https://remix.run/docs/en/main/hooks/use-action-data"&gt;&lt;code&gt;useActionData&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It works like this:&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;// server action&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;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionFunctionArgs&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// client&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Todo&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;actionData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// after you fire a request to your action by submitting this form&lt;/span&gt;
  &lt;span class="c1"&gt;// after the response actionData will be { ok: true }&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;Form&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="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;Form&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;You fire a request by submitting the only form on the page. After the request is completed, the response is in &lt;code&gt;actionData&lt;/code&gt;. In this case, it's &lt;code&gt;{ ok: true }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can use the value in &lt;code&gt;actionData&lt;/code&gt; to detect that the submission was successful.&lt;/p&gt;

&lt;p&gt;Here's how to achieve that with things built into React and Remix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capturing the results
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;actionData&lt;/code&gt; first runs on the client, it returns &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because of this, we have to react (🥁) when &lt;code&gt;actionData&lt;/code&gt; change from &lt;code&gt;undefined&lt;/code&gt; to &lt;code&gt;{ ok: true }&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Todo&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;actionData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionData&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actionData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// reset the form&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;actionData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// rest of the component&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now, we have a way to identify that the submission is completed.&lt;/p&gt;

&lt;p&gt;Let's reset the form!&lt;/p&gt;

&lt;p&gt;To do that, we're going to use the HTMLFormElement's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset"&gt;&lt;code&gt;reset&lt;/code&gt;&lt;/a&gt; method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capturing the form
&lt;/h2&gt;

&lt;p&gt;To call the &lt;code&gt;reset&lt;/code&gt; method on the &lt;code&gt;HTMLFormElement&lt;/code&gt; we need a way to capture a reference to it.&lt;/p&gt;

&lt;p&gt;This is where React's &lt;a href="https://react.dev/reference/react/useRef"&gt;useRef&lt;/a&gt; helps us:&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;Todo&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// use effect as before&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;Form&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;form&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;Form&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;h3&gt;
  
  
  Clearing the form
&lt;/h3&gt;

&lt;p&gt;Now that we both know when the request was finished and have a reference to the form that sent the request, let's combine the above two snippets:&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;// server action&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;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionFunctionArgs&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// client&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Todo&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;actionData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionData&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actionData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;reset&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="nx"&gt;actionData&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;// after you fire a request to your action by submitting this form&lt;/span&gt;
  &lt;span class="c1"&gt;// after the response actionData will be { ok: true }&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;Form&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="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"title"&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="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;Form&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I like that this approach relies on information that's coming from Remix and not from some kind of states that we manually tracked before.&lt;/p&gt;

&lt;p&gt;The problem with such app state tracking is that it gets out of control once you add a separate state for loading indicators, error messages, disabling inputs, and buttons on form submission.&lt;/p&gt;

&lt;p&gt;All this can be achieved with Remix's built-in hooks without using a single state. This reduces the amount of client-side logic you implement and later maintain.&lt;/p&gt;

&lt;p&gt;Have fun using Remix!&lt;/p&gt;

</description>
      <category>remix</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Succeeding as a Software Developer: The Essential Mindset Shift</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Wed, 29 Nov 2023 08:05:04 +0000</pubDate>
      <link>https://dev.to/akoskm/succeeding-as-a-software-developer-the-essential-mindset-shift-1af6</link>
      <guid>https://dev.to/akoskm/succeeding-as-a-software-developer-the-essential-mindset-shift-1af6</guid>
      <description>&lt;p&gt;My developer journey began in an era that's hard to imagine.&lt;/p&gt;

&lt;p&gt;No social media, X (Twitter), YouTube, or online courses existed.&lt;/p&gt;

&lt;p&gt;I couldn't compare my skills to anyone because I didn't know any programmers in real life or online (you didn't really "know people online" back then).&lt;/p&gt;

&lt;p&gt;I didn't know if I was a bad, average, or good programmer. I believe this, alone, helped me more than anything else.&lt;/p&gt;

&lt;p&gt;It was 2001, and I bought a book about C a couple of months ago.&lt;/p&gt;

&lt;p&gt;There was the book, and I started writing my first program.&lt;/p&gt;

&lt;p&gt;I didn't share my progress because I had nobody to share it with, but I was happy when something worked. Okay, this is not entirely true; I demoed these programs to my parents. Although they showed signs of interest, it was probably because I was sitting in my room writing code instead of driving without a license.&lt;/p&gt;

&lt;p&gt;I loved experiments because I had a lot of questions, but I haven't had anyone to ask.&lt;/p&gt;

&lt;p&gt;You could run indefinite experiments with this thing called C, change variables and inputs, and see the results instantaneously. It was kind of a miracle for me.&lt;/p&gt;

&lt;p&gt;"Will this thing work? I have no clue, but let's give it a shot!" I learned a ton by experimenting and building many apps.&lt;/p&gt;

&lt;p&gt;Three years later, when I entered high school, I sold my PHP app to a local company. I never quit programming since (although I considered it once) and built pretty much everything that's the basis for anything we use today.&lt;/p&gt;

&lt;p&gt;Some points originate from the difference between the Fixed and the Growth mindset, which inspired this post. Many books were created on the same topic, but here's the original I recommend reading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/Mindset-Psychology-Carol-S-Dweck-ebook/dp/B000FCKPHG"&gt;Mindset: The New Psychology of Success&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The book at places might sound like it was made up, and it's just another self-help crap, but hold tight during those parts. I'm sure you'll read something new or understand why you did things in the past.&lt;/p&gt;

&lt;p&gt;My career is probably not even at 50%, but I feel there are some key elements of a successful programmer mindset that I wanted to share with you. Here they go.&lt;/p&gt;

&lt;h1&gt;
  
  
  Be The Beginner
&lt;/h1&gt;

&lt;p&gt;Two years ago, I wrote about the advantages of seeing yourself as an &lt;a href="https://akoskm.substack.com/p/akos-weekly-hash-be-the-amateur-4-790634"&gt;amateur&lt;/a&gt;. Studies in the book I recommended above show that prejudice affects people's decisions negatively. This is the result of the fixed mindset.&lt;/p&gt;

&lt;p&gt;If you assume you're a programming genius, your mind will try everything to keep this status.&lt;/p&gt;

&lt;p&gt;First, to appear as a genius, you don't make mistakes. This implies that you never try new things and never experiment, withdrawing yourself from the opportunity to grow.&lt;/p&gt;

&lt;p&gt;And that's how you stop growing as a programmer.&lt;/p&gt;

&lt;h1&gt;
  
  
  Advice is Plenty
&lt;/h1&gt;

&lt;p&gt;In programming, wherever you look, you get advice for free. You just have to see it as advice. And I'm not talking about self-made senior developers who earned internet points and are now telling you how to be a millionaire.&lt;/p&gt;

&lt;p&gt;I'm talking about other developers and teammates, carefully reviewing your code, and helping you.&lt;/p&gt;

&lt;p&gt;With a fixed mindset, these comments could feel paralyzing. The "X requested changes" might feel ashaming.&lt;/p&gt;

&lt;p&gt;"What I didn't get right this time, OMG".&lt;/p&gt;

&lt;p&gt;But there should be no question whether it's good that someone is requesting changes from you. It's someone else's knowledge applied to your stuff. Someone else's mind, seen in action. It's not something you want to fight although a healthy conversation about whether they were right or weren't is more than welcome you should &lt;em&gt;grab&lt;/em&gt; that advice.&lt;/p&gt;

&lt;h1&gt;
  
  
  Quit at 5
&lt;/h1&gt;

&lt;p&gt;Hustling was super popular in the Gary Vee era, and now people who were his avid followers are coming at you with a different tactic: &lt;em&gt;drop things at 5 PM&lt;/em&gt;. Don't spend any more minutes working than you absolutely have to.&lt;/p&gt;

&lt;p&gt;Here's what &lt;a href="https://en.wikipedia.org/wiki/Dave_Cutler"&gt;Dave Cutler&lt;/a&gt; has to say about this:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/n1VcqmSceMw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;During my early years at my first employment, I don't remember there was a thing I said no to, although I was employed as a front-end developer. Write a new backend service? Sure, why not? Design a DB Schema? Okay, do I need a pencil or paper for that, or ...? I asked many, seemingly stupid questions.&lt;/p&gt;

&lt;p&gt;But now, without a second thought, I hear this from beginner developers and contractors I work with: "I prefer not to do x and y".&lt;/p&gt;

&lt;p&gt;I get it. You don't like it, and that's fine.&lt;/p&gt;

&lt;p&gt;But remember that the things you use today, including this blogging platform, weren't built because people said, "I'd rather do something else".&lt;/p&gt;

&lt;h1&gt;
  
  
  The Money
&lt;/h1&gt;

&lt;p&gt;We do programming for a living. However, in my experience, how much money you make at a given moment could be less important in the grand scheme of things than you think.&lt;/p&gt;

&lt;p&gt;If there's a learning opportunity and your life circumstances allow, I'd take a job for 20% less than I would typically do and think of it as paying for a course.&lt;/p&gt;

&lt;p&gt;When I started freelancing, I was happy to take on projects where I could &lt;em&gt;earn and learn&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But be careful not to use &lt;em&gt;"I'm still leaning"&lt;/em&gt; as an excuse when someone is paying for your services.&lt;/p&gt;

&lt;p&gt;Learning doesn't authorize you to do anything less than your absolute best work, which might mean you go way below your hourly rate anyway because you have to do additional research, but time showed me it's worth it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Giving Back
&lt;/h1&gt;

&lt;p&gt;After working in various industries, such as healthcare and construction, for personal reasons or through client engagements, I see now that the growth of programming has been greatly influenced by the inherent mindset of sharing knowledge and educating others.&lt;/p&gt;

&lt;p&gt;Think of open source and just how many free tools you use today.&lt;/p&gt;

&lt;p&gt;Programming is a self-sustaining perpetual mobile that functions because people are thought to give back to the community in some form because that's also how they became programmers.&lt;/p&gt;

&lt;p&gt;Without being paid or expecting any future returns on it, because I bet there is a programmer who you set out on an incredible journey than you did your part.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>career</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I became a Web Developer</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Wed, 25 Jan 2023 23:00:00 +0000</pubDate>
      <link>https://dev.to/akoskm/how-i-became-a-web-developer-44ap</link>
      <guid>https://dev.to/akoskm/how-i-became-a-web-developer-44ap</guid>
      <description>&lt;p&gt;I often post Twitter threads about React, TypeScript, and JavaScript courses. However, after every successful post, my DMs are almost full of people asking, How to become a Web Developer?&lt;/p&gt;

&lt;p&gt;So this got me thinking, am I approaching teaching the wrong way, and should I start from the basics? You can learn a lot from what I share, but only if you're already familiar with programming. But if you're a beginner, they probably aren't that helpful.&lt;/p&gt;

&lt;p&gt;At some point in my career, I was also a beginner, and if you are too - I don't want to let you down.&lt;/p&gt;

&lt;p&gt;So here's my journey of becoming a Web Developer with no formal education when I was about to start middle school.&lt;/p&gt;

&lt;h1&gt;
  
  
  How to become a Web Developer, or really, anything
&lt;/h1&gt;

&lt;p&gt;There has to be some initial motivation to learn more about the profession. You most definitely can't become something you don't know anything about. You can't become a carpenter if you've never heard about carpenters.&lt;/p&gt;

&lt;p&gt;But I'm sure - since you're reading this - you already heard about web developers.&lt;/p&gt;

&lt;p&gt;In 2001 I got my first computer and my first dial-up modem.&lt;/p&gt;

&lt;p&gt;In my community, people knew almost nothing about computers, except that they are the next big thing and are expensive. Kind of like Crypto these days. For the price of a PC, you could have bought tons of stuff. My dad was thinking hard about buying a trailer or a computer. He could've bought a trailer for work for that money. But in the end, I think he made the right choice. 😄&lt;/p&gt;

&lt;p&gt;So we discovered this thing called: the Internet. I was fascinated by the fact that anyone in the world can make a website, a creation, a hello world with a marquee effect - and it becomes a thing for everyone. You can go, see it, tell it to other people where they can find it - if they had Internet, it was rare back then.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/krtDmVuQGssOtMWfRg/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/krtDmVuQGssOtMWfRg/giphy.gif" alt="gif" width="480" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I knew I had to understand how people create websites and how I could make them too.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;At this point in my life, I had little clue what HTML or CSS is. I knew some tools make a website appear in a certain way. My initial motivation wasn't to learn the tools but to learn to build the thing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So how to become a web developer? What's the neverending source of drive that can last forever?&lt;/p&gt;

&lt;p&gt;Is it HTML? I doubt so. I don't even write pure HTML anymore. Is it CSS? I never truly learned CSS and use UI Frameworks like Bootstrap and TailwindCSS to this day.&lt;/p&gt;

&lt;p&gt;I'd say it's the curiosity to explore the web and understand what you can do with it. To unleash what you have learned, again and again, to build different things with or without purpose.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reverse engineering at 16
&lt;/h1&gt;

&lt;p&gt;By 2003, the Internet was already "big" among the local nerds. I knew one other guy in my entire school who also knew what HTML is and how you can build basic websites.&lt;/p&gt;

&lt;p&gt;How did we learn to create HTML sites? Macromedia Dreamweaver!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0AzU4bcc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1659068892272/RidS3pG8Z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0AzU4bcc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1659068892272/RidS3pG8Z.png" alt="image.png" width="673" height="507"&gt;&lt;/a&gt;&lt;em&gt;This is version 8, I used something soo old I couldn't find a picture of it&lt;/em&gt; 😂&lt;/p&gt;

&lt;p&gt;What? No books? No Fancy editors?&lt;/p&gt;

&lt;p&gt;Back in 2003 - for a teenager - it was almost impossible to find programming HTML books, especially where I was living. We had a couple of software of unknown origin 🏴 and we had to work with what we had.&lt;/p&gt;

&lt;p&gt;We built a ton of websites in Dreamweaver's visual editor. And I mean a Ton. A website for our local school, the kindergarten, one for each neighbor's dog, for our favorite movies, anything you can imagine.&lt;/p&gt;

&lt;p&gt;But some annoying bugs forced us to look under the hood and switch to the Code view of Dreamweaver.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/qRVDIxBnu57gP0Jkg6/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/qRVDIxBnu57gP0Jkg6/giphy.gif" alt="gif" width="480" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is how we got introduced to HTML.&lt;/p&gt;

&lt;p&gt;Yep! No books, no tutorials, but out of necessity.&lt;/p&gt;

&lt;p&gt;As we built and tweaked more and more websites, we realized that coding some stuff in the code editor is much faster. It also produced fewer bugs than the UI builder.&lt;/p&gt;

&lt;p&gt;And this is how we switched from the visual to the HTML editor.&lt;/p&gt;

&lt;p&gt;In the meantime, our English has improved, and we started looking at online forums to expand our knowledge.&lt;/p&gt;

&lt;p&gt;One of the most important technologies that I picked up next and that led to my first client was: JavaScript? No. PHP. No! ASP? Nah It was Joomla!&lt;/p&gt;

&lt;p&gt;Why on Earth did you learn Joomla - you're wondering (Joomla is a Content Management System written in PHP).&lt;/p&gt;

&lt;p&gt;I won't surprise you if I tell you that most people don't know how to build websites. And it wasn't any different 20 years ago! I knew that to make some real money with the web, I have to give people more freedom. I can't tell them: "Hey if you don't like the footer you can change it on line 59!"&lt;/p&gt;

&lt;p&gt;So I started building out my new websites - you can call these portfolio websites - with Joomla.&lt;/p&gt;

&lt;p&gt;Being part of a small community, people already knew I do websites. Fortunately, a festival was preparing in my village around that time of the year.&lt;/p&gt;

&lt;p&gt;The organizers reached out to me and asked if I could make a website for them. They would update the contents later, the program schedule if it changes, publish new performers, etc.&lt;/p&gt;

&lt;p&gt;I showed them Joomla and they were blown away. And this is how I landed my first paid gig for some big money back then.&lt;/p&gt;

&lt;h1&gt;
  
  
  PHP, more side hustles, University
&lt;/h1&gt;

&lt;p&gt;In 2004 I was already learning PHP and tweaking Joomla. I also built a PHP app that was able to bulk-modify excel files.&lt;/p&gt;

&lt;p&gt;I sold this software to a local shop with a huge inventory where the prices were changing frequently based on the exchange rate of the local currency compared to USD. They were calculating and updating every price manually.&lt;/p&gt;

&lt;p&gt;I told them they could do this with a click of a button - you can imagine their reaction.&lt;/p&gt;

&lt;p&gt;But you can also imagine me slowly understanding where this can lead.&lt;/p&gt;

&lt;p&gt;I started middle school and focused on programming (Turbo C and PHP) - and pretty much-neglected everything else there was for me to learn. Learn about an operating system called Linux that changed everything for me forever.&lt;/p&gt;

&lt;p&gt;My final work in middle school was an online platform that schools in my country used to register their pupils for a National Math competition. It contained all elements of registration for school, and teachers, handling files uploads of CSV, excel, and transforming and saving them to a MySQL database.&lt;/p&gt;

&lt;p&gt;The system was used extensively for three months while the registrations lasted.&lt;/p&gt;

&lt;p&gt;It served the entire country and I felt super proud.&lt;/p&gt;

&lt;p&gt;One of my teachers only told me this after I presented my final work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;you could have sold this for a fortunate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/l0IsH7VVszP2tnzuo/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/l0IsH7VVszP2tnzuo/giphy.gif" alt="gif" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;He was right. I wanted to finish my studies ASAP and make money with programming. I didn't want to go to University - I was thinking, why waste four years when I could be selling people what they need?&lt;/p&gt;

&lt;p&gt;Finally, thanks to my parents, I decided to give the University a shot. I'll skip the debate on formal education or Bootcamps. I believe they don't compare. They provide different opportunities. If you go to the University, you'll encounter some situations only a University can bring. For example, I was able to publish a scientific work that has an ISBN! Whether they are relevant for what you'll be doing later, you'll find that out.&lt;/p&gt;

&lt;p&gt;One of my dear friends, who's a brilliant mind and is programming to this day, couldn't go the University, but instead, he invested that time into getting better at coding. He not only made a living for himself but helped his parents and now works for a company that everyone reading this knows.&lt;/p&gt;

&lt;p&gt;For me, the University was somewhat relevant. I learned a lot about data structures and algorithms at a very advanced level, and I worked a ton with Java - which later became the language I did Web Development with for four years.&lt;/p&gt;

&lt;p&gt;And the rest is history.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;I admit I originally titled this post "How to became a Web Developer" and wanted to turn it into a guide-like thing. But instead, I decided to write about how I did it.&lt;/p&gt;

&lt;p&gt;Because those were different times, learning and networking were also different.&lt;/p&gt;

&lt;p&gt;These times have their challenges.&lt;/p&gt;

&lt;p&gt;I respect everyone who tries to make sense of this neverending stream of programming content these days.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>programming</category>
      <category>career</category>
    </item>
    <item>
      <title>How to test props in React Testing Library</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Fri, 13 Jan 2023 09:21:13 +0000</pubDate>
      <link>https://dev.to/akoskm/how-to-test-props-in-react-testing-library-306l</link>
      <guid>https://dev.to/akoskm/how-to-test-props-in-react-testing-library-306l</guid>
      <description>&lt;p&gt;In this post, we'll learn how to test the props a React Function Component receives with React Testing Library and Jest.&lt;/p&gt;

&lt;p&gt;Before React Testing Library appeared, most of us were writing tests with Enzyme.&lt;/p&gt;

&lt;p&gt;Enzyme had a completely different approach to testing React components.&lt;/p&gt;

&lt;p&gt;We could play with the React components' internal state and test some implementation details.&lt;/p&gt;

&lt;p&gt;After finding a component in the DOM, we could do pretty much anything with it.&lt;/p&gt;

&lt;p&gt;Call &lt;code&gt;state()&lt;/code&gt; to modify its state (super bad idea) or &lt;code&gt;props()&lt;/code&gt; to check what props it received:&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;// This is most likely what we did in Enzyme&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mount&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="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MyComponent&lt;/span&gt;
      &lt;span class="na"&gt;includedProp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Success!"&lt;/span&gt;
      &lt;span class="na"&gt;excludedProp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"I'm not included"&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MyComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;includedProp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Success!&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;The power provided by Enzyme got out of control, and many of us started using it to &lt;a href="https://akoskm.com/testing-implementation-details" rel="noopener noreferrer"&gt;test implementation details&lt;/a&gt;, which should be avoided at all costs. It makes the tests fragile and the codebase more difficult to change.&lt;/p&gt;

&lt;p&gt;We won't go over the Jest and React Testing Library setup here because I provided a working &lt;a href="https://codesandbox.io/s/distracted-cdn-lbdmk9?file=/src/Profile/index.test.tsx" rel="noopener noreferrer"&gt;Codesandbox&lt;/a&gt; where you can both see the setup and experiment with the code.&lt;/p&gt;

&lt;p&gt;Let's jump into it and see how we could test the props on a simple component.&lt;/p&gt;

&lt;p&gt;Here's a Profile component that displays some data about our user:&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="nx"&gt;PermissionsContainer&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;../PermissionsContainer&lt;/span&gt;&lt;span class="dl"&gt;"&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;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;firstName&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;lastName&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;age&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="nl"&gt;profileId&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profileId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&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="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="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&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;firstName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;, &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;age&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;span&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;PermissionsContainer&lt;/span&gt; &lt;span class="na"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;profileId&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="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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Let's say we want to make sure that the &lt;code&gt;PermissionsContainer&lt;/code&gt; receives the same &lt;code&gt;profileId&lt;/code&gt; as the &lt;code&gt;Profile&lt;/code&gt; component, but without actually running any code from &lt;code&gt;PermissionsContainer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we write a simple test like the one below, it will render the &lt;code&gt;PermissionsContainer&lt;/code&gt; and run any code inside of it:&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;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&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;@testing-library/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;Profile&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;./index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Profile&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="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;renderProfile&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="nf"&gt;render&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;Profile&lt;/span&gt;
        &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"John"&lt;/span&gt;
        &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Doe"&lt;/span&gt;
        &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1234-fake-5678-uuid"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders app&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/John Doe/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;This is not what we want!&lt;/p&gt;

&lt;p&gt;So to mock the &lt;code&gt;PermissionContainer&lt;/code&gt; module that &lt;code&gt;Profile&lt;/code&gt; imports we're going to use &lt;a href="https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options" rel="noopener noreferrer"&gt;&lt;code&gt;jest.mock&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To mock the entire function component, so it's a no-op, we would do something like this:&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="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./PermissionsContainer&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;But we can also tell the mock what &lt;code&gt;./PermissionsContainer&lt;/code&gt; should return when it's imported by specifying the second parameter.&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="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./PermissionContainer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is PermissionsContainer&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;When &lt;code&gt;Profile&lt;/code&gt; imports &lt;code&gt;./PermissionsContainer&lt;/code&gt; instead of receiving the actual module, it will receive this function: &lt;code&gt;() =&amp;gt; 'This is PermissionsContainer'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;See what happens in the DOM when we mock the module:&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;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&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;@testing-library/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;Profile&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;./index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./PermissionContainer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is PermissionsContainer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Profile&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="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;renderProfile&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="nf"&gt;render&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;Profile&lt;/span&gt;
        &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"John"&lt;/span&gt;
        &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Doe"&lt;/span&gt;
        &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1234-fake-5678-uuid"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders app&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// screen.debug is going to print the current DOM into the console&lt;/span&gt;
    &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/John Doe/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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 see in the console the output of debug:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        John

        Doe
        , 
        35
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      This is PermissionsContainer
    &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;&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;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&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;We specified a simple function &lt;code&gt;() =&amp;gt; 'This is PermissionsContainer'&lt;/code&gt; as the default export of the &lt;code&gt;PermissionsContainer&lt;/code&gt; module, and when the module got imported and was run by React, it produced a simple string.&lt;/p&gt;

&lt;p&gt;We're on track!&lt;/p&gt;

&lt;p&gt;The code inside &lt;code&gt;PermissionsContainer&lt;/code&gt; is not running anymore, but we'd still like to check what props it receives!&lt;/p&gt;

&lt;p&gt;All we have to do is update our mock implementation to handle the parameters it receives:&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="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./PermissionsContainer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;profileId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`This is PermissionsContainer profileId:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;profileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now we should see the following output after returning the test:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&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="nt"&gt;div&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="nt"&gt;div&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="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        John

        Doe
        , 
        35
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      This is PermissionsContainer profileId:1234-fake-5678-uuid
    &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;&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;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&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;Now we could write a test that checks if this specific prop value is printed in the DOM:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders Container with the correct props&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is PermissionsContainer profileId:1234-fake-5678-uuid&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;toBeInTheDocument&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 this is how we check if &lt;code&gt;PermissionsContainer&lt;/code&gt; receives the correct prop from its parent component without actually running &lt;code&gt;PermissionsContainer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Got any questions? Are you moving from Enzyme to jest?&lt;/p&gt;

&lt;p&gt;Let me know in the comments below if you're facing any difficulties!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to type React Props with TypeScript</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Fri, 06 Jan 2023 06:39:01 +0000</pubDate>
      <link>https://dev.to/akoskm/how-to-type-react-props-with-typescript-1p4</link>
      <guid>https://dev.to/akoskm/how-to-type-react-props-with-typescript-1p4</guid>
      <description>&lt;p&gt;Typing React Props with TypeScript can be done in different ways, but not each way will provide you with an equal level of type safety.&lt;/p&gt;

&lt;p&gt;First, let's take a look at a simple component we'll use to demonstrate the two methods, but without typing the props first:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuue697enxz02cb8piglf.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%2Fuue697enxz02cb8piglf.png" width="795" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I always set &lt;code&gt;"noImplicitAny": true,&lt;/code&gt; so my editor warns me about the untyped props.&lt;/p&gt;

&lt;p&gt;Let's type them with React's &lt;code&gt;FC&lt;/code&gt; interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  React.FC
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;React.FC&lt;/code&gt; is a generic type in the React module that defines a functional component in TypeScript. It is a shorthand for describing a functional component that has a prop type.&lt;/p&gt;

&lt;p&gt;In our example, &lt;code&gt;ProfileCard&lt;/code&gt; is a functional component that expects to receive props with a &lt;code&gt;name&lt;/code&gt; string and an &lt;code&gt;age&lt;/code&gt; number. These props are typed using the &lt;code&gt;Props&lt;/code&gt; interface, and the component is typed as &lt;code&gt;React.FC&amp;lt;Props&amp;gt;&lt;/code&gt;, which specifies that it is a functional component with the specified prop types:&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%2Fntk6hnnonx1yomnjzilf.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%2Fntk6hnnonx1yomnjzilf.png" width="674" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we see, the errors disappeared because through &lt;code&gt;React.FC&amp;lt;Props&amp;gt;&lt;/code&gt;, we specified the types for both the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;age&lt;/code&gt; props.&lt;/p&gt;

&lt;h2&gt;
  
  
  props: Prop
&lt;/h2&gt;

&lt;p&gt;Another, even more straightforward way to type React props is to specify the object type inside the function's signature.&lt;/p&gt;

&lt;p&gt;Below we're simply telling react that this object has a specific type:&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%2F9mthpcm13yo2iclggbwv.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%2F9mthpcm13yo2iclggbwv.png" width="680" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's see the differences between the two regarding type safety.&lt;/p&gt;

&lt;h1&gt;
  
  
  Differences in type safety
&lt;/h1&gt;

&lt;p&gt;When using &lt;code&gt;React.FC&lt;/code&gt;, if you define a default value with a different type than the type in &lt;code&gt;Props&lt;/code&gt;, it'll merge the two types and assign that as the new type for the prop.&lt;/p&gt;

&lt;p&gt;You might expect the below code to fail the TypeScript compilation, but it won't.&lt;/p&gt;

&lt;p&gt;Here's the new type &lt;code&gt;React.FC&lt;/code&gt; created for &lt;code&gt;name&lt;/code&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%2F09xbbyt67pzn9c3is1lo.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%2F09xbbyt67pzn9c3is1lo.png" width="743" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, if you use the &lt;code&gt;props: Prop&lt;/code&gt; method to type your props, you get an error right away:&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%2Fzy4znhn08qq3q7cgj9ca.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%2Fzy4znhn08qq3q7cgj9ca.png" width="750" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Because of this difference, I always prefer the &lt;code&gt;props: Prop&lt;/code&gt; way to define props in React.&lt;/p&gt;

&lt;p&gt;But keep in mind, if you are using TypeScript in your entire codebase, a wrongly typed default value might pop up an error in the component that ends up using it.&lt;/p&gt;

&lt;p&gt;Here's an example of that:&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%2Frdun9qtvq8y4b9qksqyr.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%2Frdun9qtvq8y4b9qksqyr.png" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The TypeScript compiler warns us that &lt;code&gt;ProfileName&lt;/code&gt; expects a name with the type of &lt;code&gt;string&lt;/code&gt; and not with the type of &lt;code&gt;string | boolean&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>designpatterns</category>
      <category>python</category>
      <category>tutorial</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Stop Doing Coding Tutorials</title>
      <dc:creator>Akos</dc:creator>
      <pubDate>Fri, 18 Nov 2022 09:18:34 +0000</pubDate>
      <link>https://dev.to/akoskm/stop-doing-coding-tutorials-2642</link>
      <guid>https://dev.to/akoskm/stop-doing-coding-tutorials-2642</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclaimer I don't want to gatekeep and tell you what tutorials are worth reading. In fact, I welcome all and every kind of tutorial, keep writing them!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is for those who are reading them and feel they're not progressing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;About a year ago I learned about the idea of tutorial purgatory or tutorial hell. I'm confident, I can set you on the right course to escape it.&lt;/p&gt;




&lt;p&gt;Completing tutorials feels good. They're a lot shorter than a book, ideal for a daily dose of dopamine.&lt;/p&gt;

&lt;p&gt;They're mostly free, further encouraging you to grab them, and it can be hard to stop because there's the reward when you complete them!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Finally, I learned how to use Array.map, enough for the day! - said by no one ever&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why there are so many tutorials?
&lt;/h3&gt;

&lt;p&gt;Tutorials exist for different reasons. Some people write tutorials for a living. They write one every single day. Is it the 20th "Getting started with X", "Learning Y in 10 minutes", or the 100th spin on "Why you should be using Z right now"?&lt;/p&gt;

&lt;p&gt;Some people write tutorials only to practice writing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You don't have to read all the tutorials out there on trivial topics like how to use &lt;code&gt;Array.map&lt;/code&gt;, that's ridiculous.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm not talking here about tutorials discussing how to integrate different tech or platforms that sometimes indeed need a tutorial because the official docs simply cant explain the integration with every possible tech out there.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to do instead?
&lt;/h3&gt;

&lt;p&gt;You can create plenty by following the official documentation.&lt;/p&gt;

&lt;p&gt;Most of the libraries you want to work with already have a simple "Getting Started" page:&lt;/p&gt;

&lt;p&gt;React 👉 &lt;a href="https://reactjs.org/docs/getting-started.html"&gt;https://reactjs.org/docs/getting-started.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Others are packed with examples, comprehensive documentation, screencasts, free courses:&lt;/p&gt;

&lt;p&gt;TailwindCSS 👉 &lt;a href="https://tailwindcss.com/docs"&gt;https://tailwindcss.com/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are free resources. They're up to date, coming from the developers of the library itself. Packed not only with examples but best practices and patterns you should follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;As a beginner, these pages should be your bread &amp;amp; butter.&lt;/p&gt;

&lt;p&gt;The next step is basically just using this tech to build something.&lt;/p&gt;

&lt;p&gt;I believe this is just as important as reading up on tech as not even more important.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Remember always build something with the tech you just learned, otherwise, it'll be just another completed tutorial on your pile of completed tutorials.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let me know in the comments what was the last thing you've learned and what did you build with it?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
