<?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: Ozan</title>
    <description>The latest articles on DEV Community by Ozan (@ozantunca).</description>
    <link>https://dev.to/ozantunca</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%2F263605%2Fa9ecb3eb-2ef0-489c-b427-412fea7551c0.jpeg</url>
      <title>DEV Community: Ozan</title>
      <link>https://dev.to/ozantunca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ozantunca"/>
    <language>en</language>
    <item>
      <title>Preview Deployments with Firebase Hosting &amp; GitHub Actions</title>
      <dc:creator>Ozan</dc:creator>
      <pubDate>Thu, 26 Feb 2026 23:21:17 +0000</pubDate>
      <link>https://dev.to/ozantunca/preview-deployments-with-firebase-hosting-github-actions-27ag</link>
      <guid>https://dev.to/ozantunca/preview-deployments-with-firebase-hosting-github-actions-27ag</guid>
      <description>&lt;p&gt;When I briefly worked with Peec AI, one of the first things I noticed that could greatly improve the developer experience was adding preview deployments to their toolbox. They already had code reviews, but the process felt incomplete without being able to click around a real, live version of the actual changes.&lt;/p&gt;

&lt;p&gt;This becomes even more critical in the age of vibe coding.&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%2Fg9coe03uwqok4usknk3r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9coe03uwqok4usknk3r.jpg" alt="Multiple screen control room" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Preview Deployments Matter
&lt;/h2&gt;

&lt;p&gt;If you've used Vercel or Netlify, you know this experience is built-in. Every pull request gets deployed to a separate environment, ideally with its own subdomain, so the result of the code changes can be showcased easily.&lt;/p&gt;

&lt;p&gt;For growing teams, the benefits compound quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual verification&lt;/strong&gt; — Catch UI bugs that diffs can never reveal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster feedback loops&lt;/strong&gt; — Designers and PMs can review without setting up the project locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agent workflows&lt;/strong&gt; — When an agent like Cursor opens a PR, a human can visually verify the result before approving&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel development&lt;/strong&gt; — Multiple PRs can be live simultaneously without stepping on each other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No staging bottleneck&lt;/strong&gt; — Teams no longer wait for a single shared staging environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Firebase Hosting doesn't give you this out of the box, but with GitHub Actions and Firebase's Preview Channels feature, you can build the exact same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;Here's the full lifecycle we'll automate:&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%2Ff7rjgahkw4omv9uqg2d7.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%2Ff7rjgahkw4omv9uqg2d7.png" alt="PR lifecycle diagram: on PR open a preview channel is deployed and the URL is commented on the PR; on PR close the channel is deleted" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PR lifecycle diagram: on PR open a preview channel is deployed and the URL is commented on the PR; on PR close the channel is deleted&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A PR is opened or updated → a preview deployment is created (or updated)&lt;/li&gt;
&lt;li&gt;A bot comments the preview URL directly on the PR&lt;/li&gt;
&lt;li&gt;The PR is closed or merged → the preview deployment is automatically deleted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every commit to the PR branch refreshes the preview. No manual steps, no "can you deploy this to staging?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Firebase project with Hosting enabled&lt;/li&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;li&gt;Firebase CLI installed (&lt;code&gt;npm install -g firebase-tools&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A Firebase service account (we'll generate this)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Setting Up Firebase Hosting
&lt;/h3&gt;

&lt;p&gt;If you haven't enabled Firebase Hosting yet, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase init hosting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts to set your public directory and configure as a single-page app if needed. Commit the generated &lt;code&gt;firebase.json&lt;/code&gt; and &lt;code&gt;.firebaserc&lt;/code&gt; to your repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Generating a Service Account
&lt;/h3&gt;

&lt;p&gt;GitHub Actions needs permission to deploy to Firebase on your behalf. Go to your Firebase Console → Project Settings → Service Accounts → Generate new private key.&lt;/p&gt;

&lt;p&gt;Download the JSON file, then add its entire contents as a GitHub secret:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Name it &lt;code&gt;FIREBASE_SERVICE_ACCOUNT&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Paste the raw JSON content as-is into the secret value. Do not base64-encode it or wrap it in quotes — the action reads it as a plain string and handles parsing internally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. The Preview Deployment Workflow
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/preview.yml&lt;/code&gt; in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Preview Deployment&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;firebase-preview-${{ github.event.pull_request.number }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy_preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute preview channel ID&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;channel&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;PR_NUM="${{ github.event.pull_request.number }}"&lt;/span&gt;
          &lt;span class="s"&gt;BRANCH="${{ github.event.pull_request.head.ref }}"&lt;/span&gt;
          &lt;span class="s"&gt;BRANCH_20="${BRANCH:0:20}"&lt;/span&gt;
          &lt;span class="s"&gt;RAW_ID="pr${PR_NUM}-${BRANCH_20}"&lt;/span&gt;
          &lt;span class="s"&gt;CHANNEL_ID=$(echo "$RAW_ID" | sed 's/[^a-zA-Z0-9_.-]/_/g')&lt;/span&gt;
          &lt;span class="s"&gt;echo "channel_id=$CHANNEL_ID" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Firebase Preview Channel&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FirebaseExtended/action-hosting-deploy@v0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;firebaseServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FIREBASE_SERVICE_ACCOUNT }}&lt;/span&gt;
          &lt;span class="na"&gt;channelId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.channel.outputs.channel_id }}&lt;/span&gt;
          &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7d&lt;/span&gt;
          &lt;span class="na"&gt;disableComment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post or update PR comment with preview URL&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success() &amp;amp;&amp;amp; github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PREVIEW_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy.outputs.details_url }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const PREVIEW_MARKER = '&amp;lt;!-- firebase-hosting-preview --&amp;gt;';&lt;/span&gt;
            &lt;span class="s"&gt;const previewUrl = process.env.PREVIEW_URL || '';&lt;/span&gt;
            &lt;span class="s"&gt;const deployedAt = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');&lt;/span&gt;
            &lt;span class="s"&gt;const body = `## Firebase Hosting Preview\n\n${PREVIEW_MARKER}\n\n**Preview:** [${previewUrl}](${previewUrl})\n\nLast deployed at **${deployedAt}**`;&lt;/span&gt;

            &lt;span class="s"&gt;const { data: comments } = await github.rest.issues.listComments({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
            &lt;span class="s"&gt;const botComment = comments.find(c =&amp;gt; c.body &amp;amp;&amp;amp; c.body.includes(PREVIEW_MARKER));&lt;/span&gt;
            &lt;span class="s"&gt;if (botComment) {&lt;/span&gt;
              &lt;span class="s"&gt;await github.rest.issues.updateComment({&lt;/span&gt;
                &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
                &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
                &lt;span class="s"&gt;comment_id: botComment.id,&lt;/span&gt;
                &lt;span class="s"&gt;body,&lt;/span&gt;
              &lt;span class="s"&gt;});&lt;/span&gt;
            &lt;span class="s"&gt;} else {&lt;/span&gt;
              &lt;span class="s"&gt;await github.rest.issues.createComment({&lt;/span&gt;
                &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
                &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
                &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
                &lt;span class="s"&gt;body,&lt;/span&gt;
              &lt;span class="s"&gt;});&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;concurrency&lt;/code&gt; block cancels any in-progress deploy for the same PR when a new commit is pushed, preventing race conditions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;permissions: pull-requests: write&lt;/code&gt; is required for the comment step to work. Without it, you'll get a 403.&lt;/li&gt;
&lt;li&gt;The channel ID is computed from both the PR number and branch name (e.g. &lt;code&gt;pr42-fix-auth-bug&lt;/code&gt;), making channels readable in the Firebase console. Special characters in branch names are sanitized since Firebase rejects them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disableComment: 'true'&lt;/code&gt; turns off the action's built-in comment so we can post a custom one via &lt;code&gt;github-script&lt;/code&gt; . This gives full control over the comment format and uses a hidden HTML marker to find and edit the existing comment rather than posting a new one each time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expires: 7d&lt;/code&gt; means the channel auto-expires after 7 days even if the cleanup workflow fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fas8vk8ibs0fitwordgo7.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%2Fas8vk8ibs0fitwordgo7.png" alt="GitHub bot comment on a pull request showing the Firebase preview deployment URL" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub bot comment on a pull request showing the Firebase preview deployment URL&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cleanup on PR Close
&lt;/h3&gt;

&lt;p&gt;When a PR is closed or merged, we want to remove the preview channel. Create &lt;code&gt;.github/workflows/preview-cleanup.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Preview Cleanup&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;closed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cleanup_preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compute preview channel ID&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;channel&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;PR_NUM="${{ github.event.pull_request.number }}"&lt;/span&gt;
          &lt;span class="s"&gt;BRANCH="${{ github.event.pull_request.head.ref }}"&lt;/span&gt;
          &lt;span class="s"&gt;BRANCH_20="${BRANCH:0:20}"&lt;/span&gt;
          &lt;span class="s"&gt;RAW_ID="pr${PR_NUM}-${BRANCH_20}"&lt;/span&gt;
          &lt;span class="s"&gt;CHANNEL_ID=$(echo "$RAW_ID" | sed 's/[^a-zA-Z0-9_.-]/_/g')&lt;/span&gt;
          &lt;span class="s"&gt;echo "channel_id=$CHANNEL_ID" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete Firebase Preview Channel&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FirebaseExtended/action-hosting-deploy@v0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;firebaseServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FIREBASE_SERVICE_ACCOUNT }}&lt;/span&gt;
          &lt;span class="na"&gt;channelId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.channel.outputs.channel_id }}&lt;/span&gt;
          &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;expires: 1d&lt;/code&gt; is a belt-and-suspenders move, even if the action has any hiccups, the channel disappears within a day.&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%2Fivomspt5jjy7jkurb6us.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%2Fivomspt5jjy7jkurb6us.png" alt="Firebase Hosting console showing multiple preview channels" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firebase Hosting console showing multiple preview channels&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips &amp;amp; Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Include the branch name in the channel ID.&lt;/strong&gt; Channel names must be unique per project. Using &lt;code&gt;pr{number}-{branch}&lt;/code&gt; (e.g. &lt;code&gt;pr42-fix-auth-bug&lt;/code&gt;) keeps them human-readable in the Firebase console and idempotent, the same PR always maps to the same channel. Just make sure to sanitize the branch name since Firebase rejects characters like &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase Hosting has a channel limit.&lt;/strong&gt; Free plans are capped at 7 preview channels per project. If your team runs many concurrent PRs, either upgrade to the Blaze plan or use a shorter &lt;code&gt;expires&lt;/code&gt; window (e.g. &lt;code&gt;3d&lt;/code&gt;) to keep things from piling up. If you’re already on the paid plan, &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache your build.&lt;/strong&gt; The biggest time cost is usually the build step, not the Firebase deploy itself. Using &lt;code&gt;cache: 'npm'&lt;/code&gt; on the &lt;code&gt;setup-node&lt;/code&gt; action shaves meaningful time off every run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build environment variables.&lt;/strong&gt; If your app needs environment variables at build time, expose them in the workflow's &lt;code&gt;env:&lt;/code&gt; block. Preview deployments should point to a staging API, not production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bot comment stays clean.&lt;/strong&gt; The custom &lt;code&gt;github-script&lt;/code&gt; step uses a hidden HTML marker (&lt;code&gt;&amp;lt;!-- firebase-hosting-preview --&amp;gt;&lt;/code&gt;) to find and edit the existing comment on each push, rather than posting a new one every time. You get exactly one comment per PR with the always-current preview URL and timestamp.&lt;/p&gt;

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

&lt;p&gt;Getting Firebase Hosting preview deployments working takes more legwork than Vercel or Netlify, but the end result is identical: every PR gets a live URL, every stakeholder can review without a local setup, and cleanup is automatic.&lt;/p&gt;

&lt;p&gt;For Peec AI, this changed how we do code reviews. Engineers spend less time on "can you deploy this so I can check?" and more time on the actual review. And as we lean more on AI agents to open PRs, having a visual verification step before merging has become non-negotiable.&lt;/p&gt;

&lt;p&gt;The two workflow files are concise enough to drop into any existing Firebase project in under 15 minutes. Give it a try or simply point your AI agent to this article and let it do the rest.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>serverless</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Explaining Docker in Front-End Terms</title>
      <dc:creator>Ozan</dc:creator>
      <pubDate>Sun, 09 Aug 2020 23:19:21 +0000</pubDate>
      <link>https://dev.to/ozantunca/explaining-docker-in-front-end-terms-2fh5</link>
      <guid>https://dev.to/ozantunca/explaining-docker-in-front-end-terms-2fh5</guid>
      <description>&lt;p&gt;We, front-end developers, are used to dealing with buzzwords and the ever-increasing number of technologies to learn. For years, we’ve been bombarded with library after library — and each of these is combined with numerous frameworks with their contradicting approaches.&lt;/p&gt;

&lt;p&gt;If you’ve been in the industry for more than a couple of years, odds are your skin has already started getting thicker from all the fancy words the industry is throwing at us. We hear about Docker, Kubernetes, containerization, and all the others. They all sound like pretty complicated concepts but don’t feel intimated. In this article, I’m going to explain the one you hear the most.&lt;/p&gt;

&lt;p&gt;This article is for front-end developers who want to learn what the fuss with Docker is all about and would like to see how they can utilize Docker to improve their daily work.&lt;/p&gt;

&lt;p&gt;I don’t expect you to have more knowledge than any average front-end developer would. Mind you, this article is more of a theoretical explanation of Docker’s main features and use cases rather than a practical tutorial about how to implement them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminology
&lt;/h3&gt;

&lt;p&gt;Let’s start with a quick round of terminology before I start explaining everything in detail.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Container:&lt;/strong&gt; A container is a standard unit of software that packages up code — and all its dependencies — so the application runs quickly and reliably from one computing environment to another.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image:&lt;/strong&gt; An image is an unchangeable, static file that includes executable code — and all its dependencies — except the operating system. When an image is executed, it creates containers that run the code inside the image using the files inside that image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containerization:&lt;/strong&gt; The process of encapsulating executable code inside containers and running those containers in a virtual environment, such as the cloud.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker is a containerization solution, so we’ll need to start by explaining what containers are and how they work in detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  So What Are Containers Anyway?
&lt;/h3&gt;

&lt;p&gt;You can think of a container as a kind of a virtual machine or an iframe. Much like an iframe, a container’s purpose is to isolate the processes and code executions inside it from external interference.&lt;/p&gt;

&lt;p&gt;In the front-end world, we use iframes** **when we want to isolate external resources from our website for many reasons. Sometimes this is to ensure that there isn’t any unwanted clash of CSS or JavaScript execution; other times it is to enforce a security layer between the host and the imported code.&lt;/p&gt;

&lt;p&gt;For example, we place advertisement units inside iframes because they’re often built by separate teams or even separate companies, and deployed independently from the team that manages the host website. In such cases, it is nearly impossible to manage CSS and JS clashes between the two sides.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LP_dG_Rk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ojc0nvfdyx4rkzmj4qs2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LP_dG_Rk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ojc0nvfdyx4rkzmj4qs2.png" alt="Advertisement on a website"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another use case would be to enforce security. The PayPal button you see below is placed in an iframe to ensure that the host website cannot access any information you have on your PayPal account. It cannot even click that button for you. So even if the website you’re paying is hacked, your PayPal will be safe as long as PayPal itself is safe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_ZzLkBdU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/90gugor1ts458ipwo1rr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_ZzLkBdU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/90gugor1ts458ipwo1rr.png" alt="Payment screen from [guardian.co.uk](http://guardian.co.uk)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker’s initial use cases are the same. You get to isolate two apps from each other’s processes, files, memory, and more, even if they’re running on the same physical machine. For instance, if a database is running inside a Docker container, another app cannot access that database’s files unless the database container wants it to.&lt;/p&gt;

&lt;h4&gt;
  
  
  So a Docker container is a virtual machine?
&lt;/h4&gt;

&lt;p&gt;Kind of — but not exactly.&lt;/p&gt;

&lt;p&gt;Virtual machines run their own operating systems. This allows you to run macOS, Linux, and Windows all on the same computer, which is amazing but not very performant since the boundaries of these operating systems have to be very precisely defined to prevent possible conflict.&lt;/p&gt;

&lt;p&gt;But for most intents and purposes, the containers don’t need completely separate operating systems. They just need isolation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6g5tDCql--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/t8nfoq42zuc1cyz654qc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6g5tDCql--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/t8nfoq42zuc1cyz654qc.png" alt="Virtual Machine vs Container"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what Docker does is use kernel-level isolation on Linux to isolate the resources of an app while giving it the functionalities of the underlying operating system. Containers share the operating system but keep their isolated resources.&lt;/p&gt;

&lt;p&gt;That means much better resource management and smaller image sizes. Because once you leave the resource management to Docker, it ensures the containers do not use more RAM and CPU than they need, whereas if you used a virtual machine, you would need to dedicate a specific amount of resources to virtual machines, whether they always use them or not.&lt;/p&gt;

&lt;p&gt;There we go: We now know the basics of what Docker is and what Docker containers are. But isolation is just the start. Once we get these performant and isolated containers and a powerful resource manager (Docker) to manage them, we’re able to take it the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reproducible Containers
&lt;/h3&gt;

&lt;p&gt;Another thing Docker does very well is to give us a way to declaratively rebuild our containers.&lt;/p&gt;

&lt;p&gt;All we need is a Dockerfile to define how Docker should build our containers, and we know that we’ll get the same container every time, regardless of the underlying hardware or the operating system. Think about how complicated it is to implement a responsive design across all the desktop and mobile devices. Wouldn’t you love it if it was possible to define what you need and get it everywhere without a headache? That’s what Docker is trying to accomplish.&lt;/p&gt;

&lt;p&gt;Before we go into a real-life use case, let’s quickly go over Docker’s life cycle to understand what happens when.&lt;/p&gt;

&lt;h4&gt;
  
  
  A Docker container’s life cycle
&lt;/h4&gt;

&lt;p&gt;It all starts with a Dockerfile that defines how we want Docker to build the images that the containers will be based on. Note the flow below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oaXZySJ6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/af8mbcttzqdhasmmbrss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oaXZySJ6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/af8mbcttzqdhasmmbrss.png" alt="Docker basics"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker uses Dockerfile to build images. It fetches the files, executes the commands, does whatever is defined in the Dockerfile, and saves the result in a static file that we call an image. Docker then uses this image and creates a container to execute a predefined code, using the files inside that image. So a usual life cycle would go like below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dn3me8nl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/n0ctwitepmm2az7r469d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dn3me8nl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/n0ctwitepmm2az7r469d.png" alt="Docker Lifecycle"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s unwrap this with a real use case.&lt;/p&gt;

&lt;h4&gt;
  
  
  Running tests on continuous integration (CI)
&lt;/h4&gt;

&lt;p&gt;A common use case for Docker in front-end development is running unit or end-to-end tests on continuous integration before deploying the new code to the production. Running them locally is great when writing the code, but it is always better to run them on an isolated environment to ensure that your code works everywhere regardless of the computer setup.&lt;/p&gt;

&lt;p&gt;Also, we all have that one teammate who always skips the tests and just pushes the code. So a CI setup is also good to keep everyone in check. Below is a very basic container setup that will run your tests when you run the container:&lt;/p&gt;


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


&lt;p&gt;Let’s go over the commands there to understand what is happening.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FROM&lt;/code&gt; is used for defining a base image to build on. There are a lot of images already available in the public Docker registry. FROM node:12 goes to the public registry, grabs an image with Node.js installed, and brings it to us.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COPY&lt;/code&gt; is used for copying files from the host machine to the container. Remember that the container has an isolated file system. By default, it doesn’t have access to any files on our computer. We run &lt;code&gt;COPY . /app&lt;/code&gt; to copy the files from the current directory to the /app directory inside the container. You can choose any target directory. This /app here is just an example.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WORKDIR&lt;/code&gt; is basically the cd command we know from UNIX-based systems. It sets the current working directory.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RUN&lt;/code&gt; is quite straight forward. It runs the following command inside the container we’re building.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; is kind of similar to RUN. It runs the following command inside the container as well. But instead of running it on &lt;em&gt;build&lt;/em&gt; time, it runs the command in &lt;em&gt;run&lt;/em&gt; time. Whatever command you provide to CMD will be the command that’s going be run after the container is started.&lt;/p&gt;

&lt;p&gt;This is all it takes for our Dockerfile to build the template of a container that will set up a Node.js environment and run npm test.&lt;/p&gt;

&lt;p&gt;Of course, this use case is just one of the many use cases containers have. In a modern software-architecture setup, most server-side services either already run inside containers or the engineers have plans to migrate to that architecture. Now we’re going to talk about perhaps the most important problem these images help us solve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalability
&lt;/h3&gt;

&lt;p&gt;This is something we front-end developers often overlook. That’s because even though the back-end code runs on only a few servers for all the users, the code we write runs in a separate machine for every user we have. They even buy those machines (personal computers, smartphones, etc.) that they run our code on. This is an amazing luxury that we front-end developers have that the back-end developers do not.&lt;/p&gt;

&lt;p&gt;On the server-side of things, scalability is a real issue that requires a lot of planning over the infrastructure architecture and the budget. Cloud technologies made creating new machine instances a lot easier, but it’s still the developer’s job to make their code work on a completely new machine.&lt;/p&gt;

&lt;p&gt;That’s where our consistently reproducible containers come in handy. Thanks to the image Docker has built for us, we can deploy as many containers as we want (or we can afford to pay for). No more creating a new virtual machine, installing all the dependencies, transferring the code, setting the network permissions, and many more steps we used to take just to get a server running. We already have all of that done inside an image.&lt;/p&gt;

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

&lt;p&gt;Docker has certainly revolutionized the way we develop and deploy software in the last few years. I hope I’ve been able to shed a light upon the reasons for its popularity.&lt;/p&gt;

&lt;p&gt;Containerization and the mindset it brought with it will, without a question, continue to impact how we build software in the upcoming years.&lt;/p&gt;

&lt;h4&gt;
  
  
  Further Reading
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.docker.com/get-started/"&gt;Docker Docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/@nagarwal/lifecycle-of-docker-container-d2da9f85959"&gt;Lifecycle of Docker Container&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Right Way to Create Function Components in React With TypeScript</title>
      <dc:creator>Ozan</dc:creator>
      <pubDate>Fri, 07 Aug 2020 23:49:11 +0000</pubDate>
      <link>https://dev.to/ozantunca/the-right-way-to-create-function-components-in-react-with-typescript-47i5</link>
      <guid>https://dev.to/ozantunca/the-right-way-to-create-function-components-in-react-with-typescript-47i5</guid>
      <description>&lt;p&gt;Recently, I had this pull request that triggered a short discussion with my teammates. Someone noticed that I used a different method to write types for the function components I wrote whereas there were other methods used in the codebase.&lt;/p&gt;

&lt;p&gt;In this bite-sized React article, I'll show you the right way to do it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Function components from a type-oriented perspective
&lt;/h3&gt;

&lt;p&gt;Functions, in general, are programmatic tools that take some input, process it, and return some output. Function components essentially work the same way. They take properties and convert them into UI elements. You can see below a super basic function component example from &lt;a href="https://reactjs.org/docs/components-and-props.html"&gt;reactjs.org&lt;/a&gt; using plain JavaScript.&lt;/p&gt;


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


&lt;p&gt;By rewriting this component with TypeScript we aim to make sure that we&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the correct properties with their correct types&lt;/li&gt;
&lt;li&gt;get a value of the correct type returned from the function&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Common (and wrong) way to type function components
&lt;/h4&gt;

&lt;p&gt;A method I see often that is used by developers is to define only the type for the component's props and ignore the return value. It looks like this:&lt;/p&gt;


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


&lt;p&gt;This is all good and well, considering TypeScript is smart enough to implicitly recognize the return type. But it can fail you if your function component returns different values based on some conditions. Not to mention that it causes friction between various function components.&lt;/p&gt;




&lt;h4&gt;
  
  
  The right way
&lt;/h4&gt;

&lt;p&gt;The right way to define types for function components would be using React's own type definition &lt;code&gt;React.FunctionComponent&lt;/code&gt; or &lt;code&gt;React.FC&lt;/code&gt;. In that case, we would refactor the above code to the one below:&lt;/p&gt;


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


&lt;p&gt;This version uses React's own type definition which includes the definition for the return type and also &lt;code&gt;props.children&lt;/code&gt; attribute.&lt;/p&gt;




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

&lt;p&gt;Consistency in a codebase is very important. It keeps code changes clean and makes onboarding easier. Try to be consistent in your code conventions and use the standard methods when possible.&lt;/p&gt;

&lt;p&gt;If you're interested in more React tips like this, consider subscribing to my weekly newsletter. I promise to make it worth your while.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
