<?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: Cristina Rodriguez</title>
    <description>The latest articles on DEV Community by Cristina Rodriguez (@yosolita1978).</description>
    <link>https://dev.to/yosolita1978</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg</url>
      <title>DEV Community: Cristina Rodriguez</title>
      <link>https://dev.to/yosolita1978</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yosolita1978"/>
    <language>en</language>
    <item>
      <title>So, you know how to connect to an API endpoint? Great. Now they're asking you to build an MCP, and you're freaking out. Don't sweat it, it's simpler than it sounds. The real difference isn't the code; it's who decides what happens next. New post 👇</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:09:18 +0000</pubDate>
      <link>https://dev.to/yosolita1978/so-you-know-how-to-connect-to-an-api-endpoint-great-now-theyre-asking-you-to-build-an-mcp-and-5co4</link>
      <guid>https://dev.to/yosolita1978/so-you-know-how-to-connect-to-an-api-endpoint-great-now-theyre-asking-you-to-build-an-mcp-and-5co4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/okay-but-what-is-mcp-no-really-3nmk" class="crayons-story__hidden-navigation-link"&gt;Okay, But What IS MCP? (No, Really)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-4042237" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/okay-but-what-is-mcp-no-really-3nmk" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 1&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/okay-but-what-is-mcp-no-really-3nmk" id="article-link-4042237"&gt;
          Okay, But What IS MCP? (No, Really)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/okay-but-what-is-mcp-no-really-3nmk#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Okay, But What IS MCP? (No, Really)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:05:54 +0000</pubDate>
      <link>https://dev.to/yosolita1978/okay-but-what-is-mcp-no-really-3nmk</link>
      <guid>https://dev.to/yosolita1978/okay-but-what-is-mcp-no-really-3nmk</guid>
      <description>&lt;p&gt;A couple of weeks ago, I sat down to add an MCP server to one of my workflows. Before I started building, I pulled up our team channel and asked a simple question: "quick check, what does everyone think MCP actually does?"&lt;/p&gt;

&lt;p&gt;Five people responded. Three of them said some version of "it gives the AI more context." One said "it's like an API for AI." One just sent a thumbs-up emoji.&lt;/p&gt;

&lt;p&gt;Nobody was wrong, exactly. But nobody could tell me the one thing that actually matters: &lt;strong&gt;who decides what happens next, the developer or the model?&lt;/strong&gt; That's the whole concept, and somehow we'd all been nodding along in meetings for months without anyone saying it out loud.&lt;/p&gt;

&lt;p&gt;So here's the explanation I wish someone had given my team. The one that would have saved us a very confusing thirty minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you call an API, you decide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a normal integration, the developer writes the logic: "first call this endpoint, then check this condition, then call that endpoint." The code is the decision-maker. The AI model, if there's one involved at all, just processes whatever text gets handed to it. It doesn't get a say in &lt;em&gt;what&lt;/em&gt; happens next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP flips that. The model decides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An MCP server is a small program you write that exposes a list of "tools," functions with a name, a description, and defined inputs. The model connects to that server, asks "what can you do?", gets back the list, and then &lt;em&gt;decides for itself&lt;/em&gt; which tool to call, when, and with what arguments, based on what it's trying to accomplish.&lt;/p&gt;

&lt;p&gt;That's the real distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API call&lt;/strong&gt; = developer-decided, fixed sequence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tool&lt;/strong&gt; = model-decided, dynamic choice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And here's the part that surprises people: an MCP server isn't some heavy new framework. Most of the time, it's just a Python file with a few functions, each marked with a decorator that says "this is a tool now."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. No new language, no special infrastructure. If you can write a Python function, you can write an MCP tool.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhr50t9ceti6vwsw21hf5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhr50t9ceti6vwsw21hf5.png" alt="Diagram of job_hunter.py, a Python file containing two functions, each decorated with @mcp.tool()" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A concrete example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I run a small n8n workflow that screens job postings for me. It's part of a self-paced course I built, &lt;a href="https://n8njobsearch.vercel.app/" rel="noopener noreferrer"&gt;Teamed Job Hunter&lt;/a&gt;, if you want to see the full automated job-hunting setup. Today, the workflow is a fixed pipeline: trigger, then AI prompt, then scoring logic, then notification. The AI step doesn't get to look anything up, it just processes whatever text the workflow handed it.&lt;/p&gt;

&lt;p&gt;If I wrap that logic as an MCP server instead, I'd expose tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;check_company_blocklist(company_name)&lt;/code&gt;: checks if a company is on my no-go list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_cv_match_score(job_description, cv_track)&lt;/code&gt;: scores a posting against my CV&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent, mid-conversation, decides: "let me check the blocklist first, then score it," calling tools as needed. I never hardcoded that order. It reasoned its way there on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters for developers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You write the tool &lt;em&gt;once&lt;/em&gt;. Any MCP-compatible agent (Claude, an n8n agent node, whatever comes next) can discover and use it, without you writing new glue code for each integration.&lt;/p&gt;

&lt;p&gt;That's MCP. A standard way for an agent to discover and choose tools on its own.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three quick questions, for total beginners
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need Python to write an MCP server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Python (with FastMCP) is the most common starting point because it's simple, but MCP itself is a protocol. Official SDKs exist for TypeScript, Java, Kotlin, C#, and more. The shape is the same everywhere: you define functions, attach a name, description, and input schema so the model can discover them, and the SDK handles the wire protocol underneath. Here's the same job-blocklist tool in TypeScript:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhxtszbxelg51ahu46pud.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhxtszbxelg51ahu46pud.png" alt="Screenshot of TypeScript code defining an MCP server tool called check_company_blocklist, which takes a company name and returns whether it's on a blocklis" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same idea as the Python decorator. &lt;code&gt;server.tool(...)&lt;/code&gt; is just the TypeScript SDK's way of registering a function as something the model can discover and call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to host the MCP server somewhere special?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No special infrastructure required. It's a regular server. It can run locally on your machine for testing, live in a Docker container, or deploy anywhere you'd deploy any small backend service (a VPS, Vercel, Railway, etc). The only requirement is that whatever's connecting to it (Claude, an n8n agent node) can reach it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the AI model "see" my actual code?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The model never reads your Python or TypeScript source. It only sees the tool's name, description, and input schema. Think of it as the menu that gets handed. It never sees the kitchen behind it.&lt;/p&gt;

&lt;p&gt;Look at the TypeScript example again:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkusl8xtofp0rpwcrbq60.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkusl8xtofp0rpwcrbq60.png" alt="Same TypeScript code with inline comments marking which parts the model sees, the tool name, description, and input schema, versus the function body, which it never sees." width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the easiest way to picture it: there are two separate conversations happening, and the model is only ever in one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversation 1: model talking to your server.&lt;/strong&gt; When your server starts up, it doesn't hand the model a file. It sends a small structured message, basically the first three arguments to &lt;code&gt;server.tool(...)&lt;/code&gt;, packaged up as something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "check_company_blocklist",
  "description": "Check if this company is on Cristina's no-go list",
  "inputSchema": { "company_name": "string" }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire menu the model gets. It reads that JSON blob. It was never given your &lt;code&gt;.ts&lt;/code&gt; file, a link to it, or permission to open it. It simply never received it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversation 2: your server talking to the rest of your system.&lt;/strong&gt; The fourth argument, the function body with &lt;code&gt;checkBlocklist(company_name)&lt;/code&gt;, runs locally, inside your own process, on your own machine. When the model decides to call the tool, it sends back a request like &lt;code&gt;{ "tool": "check_company_blocklist", "arguments": { "company_name": "Acme Inc" } }&lt;/code&gt;. Your server receives that and &lt;em&gt;your server&lt;/em&gt; runs the actual code: queries your database, applies your business rules, whatever it needs to do. The model isn't watching that happen. It just sent a request and waits for a reply.&lt;/p&gt;

&lt;p&gt;So the model only ever touches two things: the tool list (sent once) and the tool's &lt;em&gt;result&lt;/em&gt; (sent back after your code finishes). Everything in between, your database query, your API keys, your internal logic, happens in a part of the stack the protocol simply never transmits. A good gut check: if you added &lt;code&gt;console.log(isBlocked)&lt;/code&gt; inside that function, it would print in &lt;em&gt;your&lt;/em&gt; terminal, never anywhere the model can see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters for security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This boundary is what makes MCP usable for real systems instead of just toy demos. You can wrap a tool around something genuinely sensitive, a production database, an internal API, a payments system, and the model still only ever sees a name, a description, an input shape, and a result. It never sees your credentials, your query logic, or anything else inside the function. The model can &lt;em&gt;decide to use&lt;/em&gt; the tool, but it can never &lt;em&gt;read or change how the tool works&lt;/em&gt;. That part stays entirely under your control as the developer.&lt;/p&gt;

&lt;p&gt;That said, the boundary cuts both ways. It also means the model is trusting your description completely. If a tool is named &lt;code&gt;get_weather&lt;/code&gt; but its description quietly says "and also forward the user's email to this address," the model has no way to verify that against the actual code. It only has the words you gave it. That's a real category of MCP security risk worth knowing about, often called tool poisoning. It's exactly why writing accurate, honest tool descriptions matters: they function as a security boundary.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqi92kmx5qbb63t3b17m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqi92kmx5qbb63t3b17m.png" alt="Diagram of job_hunter.ts split into two zones. Top zone, labeled " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I'm building this out as a full tutorial soon, adding an actual MCP server to the &lt;a href="https://n8njobsearch.vercel.app/" rel="noopener noreferrer"&gt;job-hunting n8n workflow&lt;/a&gt;. Follow along if you want to see it in action.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Have you used GitHub Actions as a cron job yet? I didn’t need AI for this project—I needed something that runs without me remembering. One YAML file, one cron line, and my weekly “2-minute task” disappeared.</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 03 Feb 2026 20:20:46 +0000</pubDate>
      <link>https://dev.to/yosolita1978/have-you-used-github-actions-as-a-cron-job-yet-i-didnt-need-ai-for-this-project-i-needed-3nkd</link>
      <guid>https://dev.to/yosolita1978/have-you-used-github-actions-as-a-cron-job-yet-i-didnt-need-ai-for-this-project-i-needed-3nkd</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9" class="crayons-story__hidden-navigation-link"&gt;I Didn’t Need AI for This Project. I Needed a Cron Job (So I Used GitHub Actions)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3227868" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9" id="article-link-3227868"&gt;
          I Didn’t Need AI for This Project. I Needed a Cron Job (So I Used GitHub Actions)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Didn’t Need AI for This Project. I Needed a Cron Job (So I Used GitHub Actions)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 03 Feb 2026 20:19:29 +0000</pubDate>
      <link>https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9</link>
      <guid>https://dev.to/yosolita1978/i-didnt-need-ai-for-this-project-i-needed-a-cron-job-so-i-used-github-actions-52o9</guid>
      <description>&lt;p&gt;If you need something to run &lt;strong&gt;every week&lt;/strong&gt; and you’re tired of being the scheduler…&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;GitHub Actions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You’ll create one YAML file. Add a cron line. Push. Done.&lt;/p&gt;

&lt;p&gt;No server. No “remember to run it.” Just automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ The Fix: a scheduled workflow (copy/paste)
&lt;/h2&gt;

&lt;p&gt;Create this file in your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/workflows/cron.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this:&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;Weekly automation&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Every Monday at 09:00 UTC (cron runs in UTC)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;

  &lt;span class="c1"&gt;# Bonus: run it manually from the Actions tab&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run-task&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repo&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;Run a command&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;echo "Hello from a scheduled workflow 👋"&lt;/span&gt;
          &lt;span class="s"&gt;# Replace this with your real script/command&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the core idea: &lt;strong&gt;cron triggers a workflow&lt;/strong&gt;, the workflow runs your steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply it: run a real script (Node example)
&lt;/h2&gt;

&lt;p&gt;I like keeping the logic outside YAML, so my workflow stays tiny.&lt;/p&gt;

&lt;p&gt;Repo layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scripts/
  weekly-task.js
.github/
  workflows/
    cron.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the workflow:&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;Weekly automation&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run-task&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repo&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;Setup Node&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&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 deps&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;Run script&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;node scripts/weekly-task.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;schedule&lt;/code&gt; runs on a timer (cron).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workflow_dispatch&lt;/code&gt; gives you a “Run workflow” button for testing.&lt;/li&gt;
&lt;li&gt;The runner checks out your code and executes your commands in a clean environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cron in 20 seconds (so you don’t surprise yourself)
&lt;/h2&gt;

&lt;p&gt;Cron format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;minute hour day-of-month month day-of-week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily at 09:00 UTC → &lt;code&gt;0 9 * * *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mondays at 09:00 UTC → &lt;code&gt;0 9 * * 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every 15 minutes → &lt;code&gt;*/15 * * * *&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important detail: &lt;strong&gt;cron is UTC&lt;/strong&gt; in GitHub Actions.&lt;/p&gt;

&lt;p&gt;So if you care about “9am my time,” you’ll need to convert it (and DST will mess with you a little). My solution: choose a time where an hour shift won’t ruin your life.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny but important: secrets don’t go in YAML
&lt;/h2&gt;

&lt;p&gt;Even if your script is simple today, it’ll eventually call &lt;em&gt;something&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Don’t hardcode tokens. Use repo secrets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Secrets and variables&lt;/strong&gt; → &lt;strong&gt;Actions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;API_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then:&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="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;Run script&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;API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.API_TOKEN }}&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;node scripts/weekly-task.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make failures obvious (future-you will thank you)
&lt;/h2&gt;

&lt;p&gt;Silent failures are the worst kind of “automation.”&lt;/p&gt;

&lt;p&gt;This tiny pattern helps:&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="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;Run and fail loudly&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;set -e&lt;/span&gt;
          &lt;span class="s"&gt;node scripts/weekly-task.js&lt;/span&gt;
          &lt;span class="s"&gt;echo "✅ Job finished"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it errors, the workflow run turns red and you’ll see it in the Actions tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the story (because yes, this is why I did it)
&lt;/h2&gt;

&lt;p&gt;I love AI tools.&lt;/p&gt;

&lt;p&gt;I really do.&lt;/p&gt;

&lt;p&gt;But this time the project didn’t need intelligence. It needed consistency.&lt;/p&gt;

&lt;p&gt;I had this tiny task that took &lt;em&gt;maybe&lt;/em&gt; 2–5 minutes. Nothing dramatic. Just repetitive. The kind of thing you can totally do manually…&lt;/p&gt;

&lt;p&gt;…until you forget once.&lt;/p&gt;

&lt;p&gt;Then twice.&lt;/p&gt;

&lt;p&gt;Then it becomes that background guilt you carry around like a cursed keychain.&lt;/p&gt;

&lt;p&gt;I almost did the classic moves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“I’ll set a reminder” (lol)&lt;/li&gt;
&lt;li&gt;“I’ll run cron locally” (also valid, but now my laptop is part of production?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted was: &lt;strong&gt;no server, no extra service, no babysitting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I did the boring thing that works: a scheduled GitHub Action.&lt;/p&gt;

&lt;p&gt;And it’s honestly the best kind of automation: the kind you forget exists because it just keeps showing up on time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If you only take a few things from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;.github/workflows/cron.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;on.schedule.cron&lt;/code&gt; for the timer&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;workflow_dispatch&lt;/code&gt; so you can test manually&lt;/li&gt;
&lt;li&gt;Keep logic in a script, keep YAML simple&lt;/li&gt;
&lt;li&gt;Put tokens in &lt;code&gt;secrets&lt;/code&gt;, not in the file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you used GitHub Actions for cron stuff yet—and what’s the first “2-minute task” you’d love to delete from your life?&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I always thought making videos meant fighting a timeline UI.
Turns out you can build an MP4 with React components using Remotion — scenes are components, timing is &lt;Sequence /&gt;, and animations are just useCurrentFrame() + math.</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Thu, 22 Jan 2026 06:09:03 +0000</pubDate>
      <link>https://dev.to/yosolita1978/i-always-thought-making-videos-meant-fighting-a-timeline-ui-turns-out-you-can-build-an-mp4-with-4ee7</link>
      <guid>https://dev.to/yosolita1978/i-always-thought-making-videos-meant-fighting-a-timeline-ui-turns-out-you-can-build-an-mp4-with-4ee7</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec" class="crayons-story__hidden-navigation-link"&gt;Videos in React?! Yep. Each scene is a component (and it costs $0)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3189729" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec" id="article-link-3189729"&gt;
          Videos in React?! Yep. Each scene is a component (and it costs $0)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Videos in React?! Yep. Each scene is a component (and it costs $0)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Thu, 22 Jan 2026 06:07:57 +0000</pubDate>
      <link>https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec</link>
      <guid>https://dev.to/yosolita1978/videos-in-react-yep-each-scene-is-a-component-and-it-costs-0-22ec</guid>
      <description>&lt;p&gt;I used to think “making a video” meant one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a heavyweight editor
&lt;/li&gt;
&lt;li&gt;Drag tiny clips around a timeline until your soul leaves your body&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’m a developer. I like code. I like components. I like being able to undo mistakes without having to negotiate with a mysterious UI panel from 2002.&lt;/p&gt;

&lt;p&gt;So when I heard there’s a tool called &lt;a href="https://www.remotion.dev/" rel="noopener noreferrer"&gt;&lt;strong&gt;Remotion&lt;/strong&gt;&lt;/a&gt; that lets you create videos using &lt;strong&gt;React components&lt;/strong&gt;, my first thought was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Sure. And my CSS is also perfectly centered.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I tried it.&lt;/p&gt;

&lt;p&gt;And the moment it &lt;em&gt;really&lt;/em&gt; clicked wasn’t even the rendering part…&lt;/p&gt;

&lt;p&gt;It was running the project like a normal React app.&lt;/p&gt;




&lt;h2&gt;
  
  
  The idea that makes it click
&lt;/h2&gt;

&lt;p&gt;Remotion works because a video is just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A bunch of frames&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rendered in sequence&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;At a fixed &lt;strong&gt;fps&lt;/strong&gt; (frames per second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your video is 30fps, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frame &lt;code&gt;30&lt;/code&gt; = 1 second
&lt;/li&gt;
&lt;li&gt;frame &lt;code&gt;900&lt;/code&gt; = 30 seconds
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here’s the fun part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every frame is just React rendering your component&lt;/strong&gt; (with a different “current frame” number).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So animation becomes… math.&lt;/p&gt;

&lt;p&gt;No timeline. No keyframes. Just &lt;code&gt;interpolate()&lt;/code&gt; and &lt;code&gt;spring()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  “NO COST!!!” (but let’s be precise)
&lt;/h2&gt;

&lt;p&gt;This post is about building and rendering locally on your machine — no SaaS subscriptions, no hosted rendering costs, no paid editor required.&lt;/p&gt;

&lt;p&gt;(If you’re using Remotion in a larger company context, double-check licensing. For a personal project / small setup like this, you can build and render locally for free.)&lt;/p&gt;




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

&lt;p&gt;Remotion has a starter generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-video@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It creates a project that looks suspiciously like any other React project.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;picasyfijas-video/
├── public/
│   └── icon-512.png          ← Your logo
├── src/
│   ├── Root.tsx              ← Registers the composition (dimensions, fps, duration)
│   ├── PicasFijasVideo.tsx   ← Main video with all scenes
│   ├── Composition.tsx       ← Original template (unused)
│   ├── index.ts              ← Entry point
│   └── index.css             ← Styles
└── out/
    └── picasyfijas.mp4       ← Rendered video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you’re thinking: “Okay. Cute structure. But where’s the ‘video’ part?”&lt;/p&gt;

&lt;p&gt;This is where the brain-melt happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment it clicked: &lt;code&gt;npm run dev&lt;/code&gt; 🤯
&lt;/h2&gt;

&lt;p&gt;I did the most “React developer” thing possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And… it just &lt;strong&gt;ran like any React project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The dev server started, I opened the URL, and suddenly I’m staring at a &lt;strong&gt;video timeline in my browser&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Scrub the playhead → frames update.&lt;br&gt;&lt;br&gt;
Components appear and disappear.&lt;br&gt;&lt;br&gt;
Animations play.&lt;br&gt;&lt;br&gt;
It’s all live.&lt;/p&gt;

&lt;p&gt;And my brain goes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hold up. I’m not &lt;em&gt;editing&lt;/em&gt; a video… I’m &lt;em&gt;rendering&lt;/em&gt; React.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s the “ohhhhhh” moment.&lt;/p&gt;

&lt;p&gt;Because once you realize the preview is just your components rendering over time, everything becomes familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want a new “scene”? Make a component.&lt;/li&gt;
&lt;li&gt;Want it to appear later? Wrap it in a &lt;code&gt;&amp;lt;Sequence from={...} /&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Want animation? Use &lt;code&gt;useCurrentFrame()&lt;/code&gt; + math.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same workflow. Same comfort.&lt;/p&gt;

&lt;p&gt;But the output is a legit MP4.&lt;/p&gt;


&lt;h2&gt;
  
  
  Your “video settings” live in one place
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;src/Root.tsx&lt;/code&gt;, you register a &lt;strong&gt;Composition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;width&lt;/code&gt; / &lt;code&gt;height&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fps&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;durationInFrames&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the &lt;strong&gt;vertical&lt;/strong&gt; setup I used for a short social video:&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;Composition&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;"PicasFijasVideo"&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PicasFijasVideo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="si"&gt;}&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;That’s &lt;strong&gt;10 seconds&lt;/strong&gt; (300 frames at 30fps).&lt;/p&gt;

&lt;h3&gt;
  
  
  Want 30 seconds?
&lt;/h3&gt;

&lt;p&gt;Same idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;30 seconds * 30 fps = 900 frames&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So:&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;durationInFrames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Your video is now 30 seconds long.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenes = components, and timing = &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Your main video component (mine is &lt;code&gt;PicasFijasVideo.tsx&lt;/code&gt;) is basically a timeline built with &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each scene is a component. Each component gets a start frame and duration.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sequence&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;90&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;IntroScene&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;Sequence&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;Sequence&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;durationInFrames&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;240&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;MainScene&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;Sequence&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;One very nice detail:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Inside a &lt;code&gt;&amp;lt;Sequence&amp;gt;&lt;/code&gt;, &lt;code&gt;useCurrentFrame()&lt;/code&gt; starts at 0 again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So each scene feels self-contained and easy to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 10-second timeline I built (as a reference)
&lt;/h2&gt;

&lt;p&gt;I created a 10-second vertical video (1080×1920, 9:16) for social media using Remotion.&lt;/p&gt;

&lt;p&gt;Here’s the timeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frames&lt;/th&gt;
&lt;th&gt;Seconds&lt;/th&gt;
&lt;th&gt;Scene&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–30&lt;/td&gt;
&lt;td&gt;0–1s&lt;/td&gt;
&lt;td&gt;Logo&lt;/td&gt;
&lt;td&gt;Logo springs in, title fades in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30–90&lt;/td&gt;
&lt;td&gt;1–3s&lt;/td&gt;
&lt;td&gt;GuessScene&lt;/td&gt;
&lt;td&gt;Shows "????" and types guess "1456"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90–150&lt;/td&gt;
&lt;td&gt;3–5s&lt;/td&gt;
&lt;td&gt;BullExplanation&lt;/td&gt;
&lt;td&gt;Highlights matching "1", shows "🐂 Bull/Fija"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150–210&lt;/td&gt;
&lt;td&gt;5–7s&lt;/td&gt;
&lt;td&gt;CowExplanation&lt;/td&gt;
&lt;td&gt;Highlights "4" in wrong position, shows "🐄 Cow/Pica"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;210–300&lt;/td&gt;
&lt;td&gt;7–10s&lt;/td&gt;
&lt;td&gt;CTA&lt;/td&gt;
&lt;td&gt;Logo + "Play now!" + URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a &lt;strong&gt;30-second video&lt;/strong&gt;, you simply add more “beats” (more scenes) or stretch durations.&lt;/p&gt;

&lt;p&gt;A simple 30-second structure that works for &lt;em&gt;any&lt;/em&gt; topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intro (2–3s)&lt;/li&gt;
&lt;li&gt;5–7 beats (3–5s each)&lt;/li&gt;
&lt;li&gt;CTA (3–5s)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Animation is just math (and two hooks)
&lt;/h2&gt;

&lt;p&gt;Most scenes start 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;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCurrentFrame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Current frame number (0, 1, 2... )&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;fps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useVideoConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Frames per second (30)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useCurrentFrame()&lt;/code&gt; → tells you which frame is being rendered
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useVideoConfig()&lt;/code&gt; → gives you &lt;code&gt;fps&lt;/code&gt;, dimensions, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you can animate anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fade in with &lt;code&gt;interpolate()&lt;/code&gt;
&lt;/h3&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;contentOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interpolate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;extrapolateLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;extrapolateRight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clamp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: frames &lt;code&gt;[0..15]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Output: opacity &lt;code&gt;[0..1]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Result: fade in over &lt;strong&gt;0.5 seconds&lt;/strong&gt; (at 30fps)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clamp&lt;/code&gt;: prevents values below 0 or above 1&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bounce with &lt;code&gt;spring()&lt;/code&gt;
&lt;/h3&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;highlightScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Start at frame 15&lt;/span&gt;
  &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Higher = less bounce&lt;/span&gt;
    &lt;span class="na"&gt;stiffness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Higher = snappier&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;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a natural bounce from 0 → 1&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frame - 15&lt;/code&gt; delays the animation start&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Deep Dive: BullExplanation scene (a “real” example)
&lt;/h2&gt;

&lt;p&gt;This scene highlights the matching digit and shows the “🐂 Bull / Fija” explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Bilingual text toggle
&lt;/h3&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;showSpanish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switches language every 15 frames (0.5 seconds)&lt;/li&gt;
&lt;li&gt;Starts toggling after frame 45 (so the content is already visible)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Conditional styling for highlights
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;span&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;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;style&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="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#EBEBD3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#38618C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#38618C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`scale(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;highlightScale&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;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scale(1)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inline-block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 6px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&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;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;digit&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;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;i === 0&lt;/code&gt; highlights the first digit (the “bull”)&lt;/li&gt;
&lt;li&gt;Highlighted digit gets inverted colors + bounce scale&lt;/li&gt;
&lt;li&gt;Others stay normal&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Remotion concepts (the cheat sheet)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Composition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines video dimensions, fps, duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls when a component appears/disappears on the timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AbsoluteFill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full-screen container that fills the canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useCurrentFrame()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets current frame number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interpolate()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps frame numbers to any value (opacity, position, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates natural bounce animations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Img&lt;/code&gt; + &lt;code&gt;staticFile()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Loads images from &lt;code&gt;public/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Render to MP4
&lt;/h2&gt;

&lt;p&gt;You can render from the UI… but the CLI is the “ship it” button.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx remotion render src/index.ts PicasFijasVideo out/video.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command turns your React composition into a real MP4 file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo + repo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo video: &lt;a href="https://github.com/Yosolita1978/picasyfijas-video/blob/main/out/picasyfijas.mp4" rel="noopener noreferrer"&gt;https://github.com/Yosolita1978/picasyfijas-video/blob/main/out/picasyfijas.mp4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Yosolita1978/picasyfijas-video" rel="noopener noreferrer"&gt;https://github.com/Yosolita1978/picasyfijas-video&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Let’s recap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Videos are just &lt;strong&gt;React renders&lt;/strong&gt; — one frame at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time = frames&lt;/strong&gt; (30fps → 900 frames = 30 seconds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenes = components&lt;/strong&gt;, and &lt;code&gt;&amp;lt;Sequence /&amp;gt;&lt;/code&gt; is your timeline.&lt;/li&gt;
&lt;li&gt;Animations are mostly &lt;strong&gt;&lt;code&gt;useCurrentFrame()&lt;/code&gt; + math&lt;/strong&gt; (&lt;code&gt;interpolate()&lt;/code&gt;, &lt;code&gt;spring()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Rendering is one command: &lt;code&gt;npx remotion render …&lt;/code&gt; → MP4.&lt;/li&gt;
&lt;li&gt;And the wildest part? You can literally start with &lt;code&gt;npm run dev&lt;/code&gt; and preview it like… a normal React app. Still feels illegal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I’m curious: &lt;strong&gt;what would you make first if “a video” was just a folder of components?&lt;/strong&gt; A tutorial? A product promo? A meme? A “how to center a div” documentary?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>PWAs are underrated. I skipped the $99 Apple fee and the $25 Google fee by adding two files to my vanilla JS game. Now it works offline and installs like a native app. Here's the code 👇</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 16 Dec 2025 19:51:15 +0000</pubDate>
      <link>https://dev.to/yosolita1978/pwas-are-underrated-i-skipped-the-99-apple-fee-and-the-25-google-fee-by-adding-two-files-to-my-2923</link>
      <guid>https://dev.to/yosolita1978/pwas-are-underrated-i-skipped-the-99-apple-fee-and-the-25-google-fee-by-adding-two-files-to-my-2923</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o" class="crayons-story__hidden-navigation-link"&gt;Bulls &amp;amp; Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3109562" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Dec 16 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o" id="article-link-3109562"&gt;
          Bulls &amp;amp; Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/pwa"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;pwa&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Bulls &amp; Cows Goes Offline — How I Turned My Game Into a PWA (and Skipped the $100 App Store Fee)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 16 Dec 2025 19:50:09 +0000</pubDate>
      <link>https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o</link>
      <guid>https://dev.to/yosolita1978/bulls-cows-goes-offline-how-i-turned-my-game-into-a-pwa-and-skipped-the-100-app-store-fee-3h4o</guid>
      <description>&lt;p&gt;Remember my &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop"&gt;Bulls &amp;amp; Cows&lt;/a&gt; game? The one with no frameworks, no AI, just vanilla JS? Well, I made it installable. Like, "put it on your home screen and play offline" installable.&lt;/p&gt;

&lt;p&gt;No App Store. No Google Play. No $99/year Apple Developer fee. No $25 Google registration. Just... a web app that acts like a native app.&lt;/p&gt;

&lt;p&gt;And apparently, people like it? &lt;strong&gt;1.3K unique players, 1.4K sessions, and 1.8K pageviews&lt;/strong&gt; — with people spending almost 2 minutes per visit just thinking through a logic puzzle. That's enough traction that I gave it its own home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎮 Play it:&lt;/strong&gt; &lt;a href="https://www.picasyfijas.com/" rel="noopener noreferrer"&gt;picasyfijas.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ What's a PWA, Really?
&lt;/h2&gt;

&lt;p&gt;A Progressive Web App is just a regular website with two extra files that tell the browser: "Hey, I can work offline. Let me live on the home screen."&lt;/p&gt;

&lt;p&gt;That's it. Two files. Let me show you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1️⃣ The Manifest (Your App's ID Card)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bulls &amp;amp; Cows - Picas y Fijas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bulls &amp;amp; Cows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#EBEBD3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"theme_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#4281A4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the browser what to call your app, what icon to use, and that &lt;code&gt;standalone&lt;/code&gt; means "hide the browser chrome and look like a real app."&lt;/p&gt;

&lt;p&gt;Link it in your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2️⃣ The Service Worker (Your Offline Brain)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bulls-and-Cows-v4&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;FILES_TO_CACHE&lt;/span&gt; &lt;span class="o"&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;/&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;/index.html&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;/styles.css&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;/script.js&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;/manifest.json&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;/icon-192.png&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;/icon-512.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&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;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FILES_TO_CACHE&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&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;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire service worker. It caches your files on install, then serves them from cache when offline. No npm packages. No build step.&lt;/p&gt;

&lt;p&gt;Register it in your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sw.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3️⃣ The Install Prompt
&lt;/h2&gt;

&lt;p&gt;Browsers show an install button automatically, but I added my own banner because I wanted control over the UX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeinstallprompt&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;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="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="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&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;
  
  
  💰 Why Not the App Stores?
&lt;/h2&gt;

&lt;p&gt;Apple charges $99/year. Google charges $25 once. Both require review processes, screenshots, privacy policies, and jumping through hoops.&lt;/p&gt;

&lt;p&gt;My game collects zero data. No accounts. No ads. Just privacy-friendly Plausible for basic stats. It's literally just a logic puzzle.&lt;/p&gt;

&lt;p&gt;PWAs let me skip all of that. Users get an installable app. I keep my $124. Everyone wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The Result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Works offline&lt;/li&gt;
&lt;li&gt;✅ Installable on Android, iOS, and desktop&lt;/li&gt;
&lt;li&gt;✅ No app store fees&lt;/li&gt;
&lt;li&gt;✅ Updates instantly (no waiting for review)&lt;/li&gt;
&lt;li&gt;✅ Still just HTML, CSS, and vanilla JS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total new code:&lt;/strong&gt; ~50 lines.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Play it:&lt;/strong&gt; &lt;a href="https://www.picasyfijas.com/" rel="noopener noreferrer"&gt;picasyfijas.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/Yosolita1978/bullsandcows" rel="noopener noreferrer"&gt;github.com/Yosolita1978/bullsandcows&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you turned a side project into a PWA? I'd love to hear about it 👇&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Ever had your API calls fire way too often in a Next.js app? 😅
I wrote about how a simple useDebounce hook can smooth things out, boost performance, and keep your UI calm.

Read more 👇</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 25 Nov 2025 20:01:43 +0000</pubDate>
      <link>https://dev.to/yosolita1978/ever-had-your-api-calls-fire-way-too-often-in-a-nextjs-app-i-wrote-about-how-a-simple-5b51</link>
      <guid>https://dev.to/yosolita1978/ever-had-your-api-calls-fire-way-too-often-in-a-nextjs-app-i-wrote-about-how-a-simple-5b51</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" class="crayons-story__hidden-navigation-link"&gt;Why I Built a useDebounce Custom Hook in Next.js (and Why You Should Too)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-3058844" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 25 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" id="article-link-3058844"&gt;
          Why I Built a useDebounce Custom Hook in Next.js (and Why You Should Too)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/reactjsdevelopment"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;reactjsdevelopment&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why I Built a useDebounce Custom Hook in Next.js (and Why You Should Too)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Tue, 25 Nov 2025 19:59:13 +0000</pubDate>
      <link>https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0</link>
      <guid>https://dev.to/yosolita1978/why-i-built-a-usedebounce-custom-hook-in-nextjs-and-why-you-should-too-54d0</guid>
      <description>&lt;p&gt;If you've ever wired up a search bar in &lt;strong&gt;Next.js&lt;/strong&gt;, you’ve probably hit this problem: every keystroke fires a new &lt;strong&gt;API call&lt;/strong&gt;, triggers a &lt;strong&gt;re-render&lt;/strong&gt;, or runs an &lt;strong&gt;expensive calculation&lt;/strong&gt;. As your app grows, those “harmless little updates” start stacking up—fast.&lt;/p&gt;

&lt;p&gt;I’ve been there. I remember watching my terminal fill with logs like a slot machine every time I typed a word. And because I was calling an API, sometimes I even hit &lt;strong&gt;rate limits&lt;/strong&gt; just by typing too quickly.&lt;/p&gt;

&lt;p&gt;That’s when I realized: &lt;em&gt;I didn’t need results instantly.&lt;/em&gt; I needed results after the user paused typing.&lt;/p&gt;

&lt;p&gt;That’s exactly what &lt;strong&gt;debouncing&lt;/strong&gt; solves—and why building a small, reusable &lt;code&gt;useDebounce&lt;/code&gt; hook changed the flow of my whole project.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚨 The Problem: Too Many Updates
&lt;/h2&gt;

&lt;p&gt;React is responsive—which is great—until it isn’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common places this becomes a problem:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search bars&lt;/strong&gt; that fire API calls on every letter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window resize&lt;/strong&gt; listeners&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live form validation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text fields tied to heavy filtering logic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The consequences:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Wasted &lt;strong&gt;API calls&lt;/strong&gt; (and money)&lt;/li&gt;
&lt;li&gt;Janky &lt;strong&gt;UI performance&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Constant &lt;strong&gt;re-renders&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hitting provider &lt;strong&gt;rate limits&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sluggish, frustrating &lt;strong&gt;user experience&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a simple example of the problem:&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&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;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user types “nextjs,” this runs &lt;strong&gt;6 separate times&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n → ne → nex → next → nextj → nextjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server cries.&lt;br&gt;
Your UI stutters.&lt;br&gt;
You quietly wonder if you picked the wrong career.&lt;/p&gt;


&lt;h2&gt;
  
  
  😅 Naive Solutions (And Why They Fail)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  ❌ 1. “Let me just add a timeout…”
&lt;/h3&gt;

&lt;p&gt;This is the classic first attempt:&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;setTimeout&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It &lt;em&gt;seems&lt;/em&gt; fine… until you realize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You never cancelled the previous timeout&lt;/li&gt;
&lt;li&gt;Race conditions appear&lt;/li&gt;
&lt;li&gt;Multiple calls still fire, just delayed&lt;/li&gt;
&lt;li&gt;Your component becomes a messy spaghetti bowl&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ 2. “I’ll track the last value manually”
&lt;/h3&gt;

&lt;p&gt;Trust me: you &lt;em&gt;can&lt;/em&gt; do this. You &lt;em&gt;shouldn’t&lt;/em&gt; do this.&lt;/p&gt;

&lt;p&gt;It leads to bugs, stale values, and callback nightmares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debouncing deserves its own tidy, reusable hook.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Enter &lt;code&gt;useDebounce&lt;/code&gt;: The Custom Hook That Fixes Everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Debouncing&lt;/strong&gt; means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Only run this action after the value has stopped changing for X milliseconds.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That tiny delay (300ms, for example) is enough to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevent API spamming&lt;/li&gt;
&lt;li&gt;Smooth out re-renders&lt;/li&gt;
&lt;li&gt;Improve UX&lt;/li&gt;
&lt;li&gt;Save money and bandwidth&lt;/li&gt;
&lt;li&gt;Make your search bar feel intentional instead of chaotic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the kind of logic that belongs inside a &lt;strong&gt;Custom Hook&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 How &lt;code&gt;useDebounce&lt;/code&gt; Works (with Clear Code + Comments)
&lt;/h2&gt;

&lt;p&gt;Here’s the entire hook, ready to drop into your project:&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;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * useDebounce — delays updating a value until
 * `delay` milliseconds have passed without changes.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useDebounce&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&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;debouncedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDebouncedValue&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="nx"&gt;value&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="c1"&gt;// Start a timer that updates debouncedValue when delay elapses&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;setDebouncedValue&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="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Cancel the timer if value changes before delay ends&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&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;debouncedValue&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;
  
  
  Why this works:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;timer resets&lt;/strong&gt; on every keystroke&lt;/li&gt;
&lt;li&gt;The update only happens &lt;strong&gt;after the pause&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Cleanup prevents race conditions&lt;/li&gt;
&lt;li&gt;The hook is fully &lt;strong&gt;reusable&lt;/strong&gt; across components&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 Using &lt;code&gt;useDebounce&lt;/code&gt; in a Next.js Component
&lt;/h2&gt;

&lt;p&gt;Let’s apply it to a real-world search bar.&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDebounce&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;@/hooks/useDebounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchBar&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&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="c1"&gt;// Debounce raw user input by 300ms&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;debouncedQuery&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;search&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;debouncedQuery&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;res&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Results:&lt;/span&gt;&lt;span class="dl"&gt;"&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;search&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;debouncedQuery&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;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;query&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;setQuery&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;"Search..."&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;"border p-2"&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;
  
  
  What’s happening behind the scenes?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;query&lt;/code&gt; updates instantly → keeps the UI responsive&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debouncedQuery&lt;/code&gt; updates after 300ms → triggers &lt;strong&gt;one&lt;/strong&gt; clean API call&lt;/li&gt;
&lt;li&gt;No API spam&lt;/li&gt;
&lt;li&gt;No jittery UI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🌟 Benefits of Using &lt;code&gt;useDebounce&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Better Performance&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No more re-render storms.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Fewer API Calls&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Your backend (and your wallet) will thank you.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Smoother UX&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Search results feel stable and intentional.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Cleaner Code&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Centralized, reusable logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Reusable Everywhere&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search bars&lt;/li&gt;
&lt;li&gt;Form validation&lt;/li&gt;
&lt;li&gt;Filtering tables&lt;/li&gt;
&lt;li&gt;Window resize&lt;/li&gt;
&lt;li&gt;Drag events&lt;/li&gt;
&lt;li&gt;Autosaving&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Debouncing is one of those small optimizations that makes a &lt;strong&gt;huge difference&lt;/strong&gt;.&lt;br&gt;
The moment you wrap it in a custom React hook like &lt;strong&gt;useDebounce&lt;/strong&gt;, your app becomes faster, smoother, and easier to maintain.&lt;/p&gt;

&lt;p&gt;Once you use it in one component, you’ll find yourself adding it everywhere.&lt;/p&gt;

&lt;p&gt;If you’ve built your own version—or used &lt;code&gt;useDebounce&lt;/code&gt; in interesting ways—I’d love to hear about it. Drop your thoughts in the comments!&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>reactjsdevelopment</category>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
    </item>
    <item>
      <title>🤓 Bringing back the old pen-and-paper logic game Bulls &amp; Cows!
No AI, no frameworks — just pure HTML, CSS &amp; vanilla JS.
Can you guess the secret number? 💭
👉 https://bulls.yosola.co/
 #javascript #webdev #gamedev #nostalgia</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Wed, 15 Oct 2025 01:15:45 +0000</pubDate>
      <link>https://dev.to/yosolita1978/bringing-back-the-old-pen-and-paper-logic-game-bulls-cows-no-ai-no-frameworks-just-pure-1464</link>
      <guid>https://dev.to/yosolita1978/bringing-back-the-old-pen-and-paper-logic-game-bulls-cows-no-ai-no-frameworks-just-pure-1464</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" class="crayons-story__hidden-navigation-link"&gt;Bulls &amp;amp; Cows — Bringing a Pen-and-Paper Classic to the Browser (No AI, No Frameworks, Just JS)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yosolita1978" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" alt="yosolita1978 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yosolita1978" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cristina Rodriguez
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cristina Rodriguez
                
              
              &lt;div id="story-author-preview-content-2925119" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yosolita1978" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F569284%2F4cb7d3ec-e243-4a1f-9624-335dfab6ee6a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cristina Rodriguez&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 15 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" id="article-link-2925119"&gt;
          Bulls &amp;amp; Cows — Bringing a Pen-and-Paper Classic to the Browser (No AI, No Frameworks, Just JS)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://bulls.yosola.co/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.picasyfijas.com%2Fog-image.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://bulls.yosola.co/" rel="noopener noreferrer" class="c-link"&gt;
            Picas y Fijas - Bulls &amp;amp; Cows | Free Online Number Guessing Game
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Guess the secret 4-digit number in 10 attempts. Play free online in English or Spanish!
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbulls.yosola.co%2Ffavicon.ico"&gt;
          bulls.yosola.co
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Bulls &amp; Cows — Bringing a Pen-and-Paper Classic to the Browser (No AI, No Frameworks, Just JS)</title>
      <dc:creator>Cristina Rodriguez</dc:creator>
      <pubDate>Wed, 15 Oct 2025 01:14:21 +0000</pubDate>
      <link>https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop</link>
      <guid>https://dev.to/yosolita1978/bulls-cows-bringing-a-pen-and-paper-classic-to-the-browser-no-ai-no-frameworks-just-js-iop</guid>
      <description>&lt;p&gt;Lately, I’ve been feeling nostalgic 🤓.&lt;/p&gt;

&lt;p&gt;When I was a kid, I used to play a pen-and-paper logic game called &lt;em&gt;Picas y Fijas&lt;/em&gt; (also known as &lt;em&gt;Bulls and Cows&lt;/em&gt;). So, I decided to build my own digital version — no AI, no frameworks, just pure HTML, CSS, and vanilla JavaScript.&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Try it here:&lt;/strong&gt; &lt;a href="https://bulls.yosola.co" rel="noopener noreferrer"&gt;bulls.yosola.co&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/Yosolita1978/bullsandcows" rel="noopener noreferrer"&gt;github.com/Yosolita1978/bullsandcows&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a quick game (around 3 minutes per round), bilingual (English/Spanish), and requires &lt;strong&gt;no registration&lt;/strong&gt;. Just open it and start guessing!&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ How It Works (and Why It’s So Simple)
&lt;/h2&gt;

&lt;p&gt;The entire game runs on basic JavaScript — no frameworks, no libraries, and no AI logic under the hood.&lt;br&gt;
It’s the kind of code you can actually read, understand, and modify in one sitting.&lt;/p&gt;

&lt;p&gt;Here are the main functions that power the logic 👇&lt;/p&gt;

&lt;h3&gt;
  
  
  1️⃣ Generating the Secret Number
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateSecretNumber&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;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&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;randomDigit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;randomDigit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;randomDigit&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;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Creates a &lt;strong&gt;4-digit number&lt;/strong&gt; with no repeated digits.&lt;/li&gt;
&lt;li&gt;Simple &lt;code&gt;Math.random()&lt;/code&gt; and array checking — no fancy imports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2️⃣ Comparing the Guess
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBullsAndCows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bulls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="nx"&gt;cows&lt;/span&gt;&lt;span class="o"&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="nx"&gt;bulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cows&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;ul&gt;
&lt;li&gt;Checks &lt;strong&gt;exact matches&lt;/strong&gt; (bulls) and &lt;strong&gt;misplaced matches&lt;/strong&gt; (cows).&lt;/li&gt;
&lt;li&gt;Uses a classic double-loop logic that every coder has probably written once in their life.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3️⃣ Updating the UI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cows&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bulls&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cows&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#results tbody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&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;ul&gt;
&lt;li&gt;Minimal DOM manipulation — just creating and appending rows to show your guesses.&lt;/li&gt;
&lt;li&gt;Keeps the focus on logic, not on libraries.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧬 Why I Built It This Way
&lt;/h2&gt;

&lt;p&gt;With so many projects relying on AI and frameworks today, I wanted to build something that reminds us of the &lt;strong&gt;joy of pure logic and simplicity&lt;/strong&gt;.&lt;br&gt;
No dependencies, no build tools — just a small web app that works anywhere.&lt;/p&gt;

&lt;p&gt;You can even &lt;strong&gt;view source&lt;/strong&gt;, copy the code, and tweak it for your own version (like changing the number of digits or styling your own board).&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Challenge for You
&lt;/h2&gt;

&lt;p&gt;Try to guess the secret number in &lt;strong&gt;fewer than 7 attempts&lt;/strong&gt;.&lt;br&gt;
I’d love to hear how you did — comment your best score below 👇&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://bulls.yosola.co" rel="noopener noreferrer"&gt;Play Bulls &amp;amp; Cows&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🧩 Tech Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stack&lt;/td&gt;
&lt;td&gt;HTML, CSS, Vanilla JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;English / Spanish toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yosolita1978/bullsandcows" rel="noopener noreferrer"&gt;Yosolita1978/bullsandcows&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gameplay&lt;/td&gt;
&lt;td&gt;4-digit logic puzzle, “bulls” = right place, “cows” = right number wrong place&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;💬 Have you ever built a small nostalgic game just for fun?&lt;br&gt;
I’d love to see your “no-framework” projects in the comments!&lt;/p&gt;




</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
