<?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>Forem: Leonardo Montini</title>
    <description>The latest articles on Forem by Leonardo Montini (@balastrong).</description>
    <link>https://forem.com/balastrong</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg</url>
      <title>Forem: Leonardo Montini</title>
      <link>https://forem.com/balastrong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/balastrong"/>
    <language>en</language>
    <item>
      <title>Automate UI Bug Fixing with Chrome MCP Server and Copilot</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Mon, 24 Nov 2025 20:03:04 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/automate-ui-bug-fixing-with-chrome-mcp-server-and-copilot-2131</link>
      <guid>https://forem.com/playfulprogramming/automate-ui-bug-fixing-with-chrome-mcp-server-and-copilot-2131</guid>
      <description>&lt;p&gt;I recently had a look at the Chrome MCP server and it looks really cool. So, let me show you a quick example of what it can do.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/7M7O-tuAkN8"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

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

&lt;p&gt;First of all, if you don't have it installed yet, there are instructions available. Like every other MCP server, you just need to have somewhere in your client settings the configuration string.&lt;br&gt;
&lt;/p&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;"mcpServers"&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;"chrome-devtools"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chrome-devtools-mcp@latest"&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="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;I'm using VS Code, which has a command in the CLI for this. You can just open a new terminal, paste the command, and that's basically it.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--add-mcp&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"chrome-devtools","command":"npx","args":["chrome-devtools-mcp@latest"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In any case, full instructions are in the &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp/" rel="noopener noreferrer"&gt;Chrome MCP server documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to make sure it is installed, you can open your user configuration (&lt;code&gt;F1&lt;/code&gt; =&amp;gt; &lt;code&gt;MCP: Open User Configuration&lt;/code&gt;) and find the server there. Also, as soon as you open a new chat, you can click the tools icon and find "Chrome tools" listed.&lt;/p&gt;

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

&lt;p&gt;I have an application running on localhost, but there's a small UI issue. If you hover over a specific card, the text of some badges becomes unreadable. That's the bug we're going to validate and fix through the Chrome MCP server tools.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;^ this screen has been taken by the MCP server itself!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Back to our project, I can ask the AI to navigate to the page with the bug and take action. This was my prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Navigate here with the chrome-devtools mcp: &lt;a href="http://localhost:3000/events/js-event-2026" rel="noopener noreferrer"&gt;http://localhost:3000/events/js-event-2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll notice that if you hover on the cards on similar events, some tags are hard to read. Verify and fix that&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The Fix in Action
&lt;/h2&gt;

&lt;p&gt;Once the request is made, it starts launching the Chrome MCP server. A Chrome window appears, controlled entirely by the agent.&lt;/p&gt;

&lt;p&gt;The first step it takes is to navigate to the page. Then, it takes a snapshot. This isn't just a screenshot, but a deconstructed view of the page where unique IDs are assigned to each text, link, or button. This allows the agent to understand the structure of the page by using way less tokens than a full HTML dump.&lt;/p&gt;

&lt;p&gt;Here's an example:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;take_snapshot&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;
&lt;span class="err"&gt;##&lt;/span&gt; &lt;span class="nx"&gt;Page&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;
&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_0&lt;/span&gt; &lt;span class="nx"&gt;RootWebArea&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JS Event - ConfHub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000/events/js-event-2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_1&lt;/span&gt; &lt;span class="nx"&gt;banner&lt;/span&gt;
    &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_2&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ConfHub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_3&lt;/span&gt; &lt;span class="nx"&gt;StaticText&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ConfHub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_4&lt;/span&gt; &lt;span class="nx"&gt;navigation&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_5&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Submit Event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000/events/submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_6&lt;/span&gt; &lt;span class="nx"&gt;StaticText&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Submit Event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_7&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Communities&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It then identifies the correct element, in this case, the "React Conference 2026" card (via its &lt;code&gt;uid=1_31&lt;/code&gt;), and decides to hover over it. I allow the action, and Chrome performs the hover automatically. It then takes a screenshot to visualize the problem within the chat context.&lt;/p&gt;

&lt;p&gt;This is what the chat log looks like at this point, after the agent ran the tools provided by the MCP server:&lt;/p&gt;

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

&lt;p&gt;After confirming the issue, the agent searches for the relevant component. it identifies the mistake, (in this case by checking the badge component) and applies a fix.&lt;/p&gt;
&lt;h2&gt;
  
  
  Validation
&lt;/h2&gt;

&lt;p&gt;To ensure the fix works, the agent goes back to the page. Since the project has Hot Module Reload the changes should be immediate, but to be safe, it refreshes the page. It then attempts to hover again over the card.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;^ yes, this screen has been taken by the MCP server too, to validate the fix!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bug is now be fixed! The text is readable 👀&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this demo (as you can &lt;a href="https://youtu.be/7M7O-tuAkN8" rel="noopener noreferrer"&gt;see in the video&lt;/a&gt;), I was allowing each tool call manually to maintain full control and explain them step by step. However, imagine letting this run on a cloud agent with all tools executable autonomously. It's impressive that an independent agent can navigate, interact, take screenshots, and validate fixes without constant human intervention.&lt;/p&gt;

&lt;p&gt;The result will be a pull request containing not just the code changes, but also the validation screenshots, making the review process much easier.&lt;/p&gt;

&lt;p&gt;That was just a quick showcase of one feature I recently tested on the Chrome DevTools MCP server. If you're curious about other MCP servers you want me to try, just let me know. Tell me the name of the server and I'm going to give it a try.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





</description>
      <category>mcp</category>
      <category>githubcopilot</category>
      <category>ai</category>
      <category>vscode</category>
    </item>
    <item>
      <title>How GitHub Copilot Uses MCP Tools Behind the Scenes</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Tue, 18 Nov 2025 20:16:16 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/how-github-copilot-uses-mcp-tools-behind-the-scenes-11nk</link>
      <guid>https://forem.com/playfulprogramming/how-github-copilot-uses-mcp-tools-behind-the-scenes-11nk</guid>
      <description>&lt;p&gt;Before we dive into the details, here is the video version of this article if you prefer to watch it first and then come back here for a slower, written walkthrough. If you prefer to read, you can safely skip it and continue with the examples below.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/U_ahtUrubuQ"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Let's begin with an easy example to make things concrete.&lt;/p&gt;

&lt;p&gt;You open a new chat and ask Copilot a simple but very practical question: "is there any pull request waiting for me?"&lt;/p&gt;

&lt;p&gt;After a brief pause and some behind-the-scenes magic, Copilot replies with the exact list of pull requests that need your attention. No manual search, no switching tabs. In the rest of this article, we'll unpack what actually happened behind the scenes.&lt;/p&gt;

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

&lt;p&gt;In this article, we'll look at how that "magic" of MCP Tools really works, using the GitHub MCP server as an example.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a simple question to the right tool
&lt;/h2&gt;

&lt;p&gt;In the pull request example above, when you ask Copilot about your pull requests, it is not "guessing" the answer as it can't know by itself. Instead, it is using a tool provided by the GitHub MCP server to search your repositories.&lt;/p&gt;

&lt;p&gt;Installing an MCP server is basically telling the LLM client where the server lives and how to talk to it. For example, a server might be hosted at a URL like &lt;code&gt;https://api.githubcopilot.com/mcp/&lt;/code&gt;, and the client can talk to it over HTTP.&lt;/p&gt;

&lt;p&gt;But the location of the server is only part of the story. What really matters is &lt;strong&gt;which tools&lt;/strong&gt; that server exposes.&lt;/p&gt;

&lt;p&gt;From the GitHub Chat UI, if you click on the Tools button, for each server you see the list of registered tools together with their descriptions. In the GitHub MCP, for example, you'll find a tool that searches pull requests.&lt;/p&gt;

&lt;p&gt;That's the one Copilot used to answer my question about open PRs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Copilot discovers the tools
&lt;/h2&gt;

&lt;p&gt;So far we've focused on what the tools do, the next step is understanding how Copilot becomes aware of them in the first place.&lt;/p&gt;

&lt;p&gt;The answer is in the Model Context Protocol (MCP) specification.&lt;/p&gt;

&lt;p&gt;As soon as the client starts up, for example when VS Code launches, the MCP client asks each registered server for a list of tools.&lt;/p&gt;

&lt;p&gt;The server replies with a list that includes, for each tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The tool name&lt;/li&gt;
&lt;li&gt;A description&lt;/li&gt;
&lt;li&gt;The input schema (which parameters it expects)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This list is then made available to the language model. In other words, Copilot can see a catalog like "there is a &lt;code&gt;search_pull_requests&lt;/code&gt; tool that takes a &lt;code&gt;query&lt;/code&gt;, an &lt;code&gt;owner&lt;/code&gt;, and a &lt;code&gt;repository&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;From there, the model can decide &lt;strong&gt;which&lt;/strong&gt; tool to call and &lt;strong&gt;how&lt;/strong&gt; to fill in those inputs.&lt;br&gt;
&lt;/p&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:Balastrong/confhub is:open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Balastrong"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"confhub"&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;Copilot chooses the values for these fields based on your question, the current repository context, and other metadata. Then it sends that payload to the MCP server.&lt;/p&gt;

&lt;p&gt;The server runs its own logic, talks to external APIs if needed, and returns a structured output.&lt;br&gt;
&lt;/p&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;"total_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"incomplete_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3592203585&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"locked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Modernize create community form with card-based layout and visual elements"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The create community form ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-05T17:57:06Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-08T15:46:28Z"&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="err"&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;h2&gt;
  
  
  Turning raw tool output into answers
&lt;/h2&gt;

&lt;p&gt;The response coming back from the MCP server is not what you see in the chat.&lt;/p&gt;

&lt;p&gt;The tool returns structured data: objects, arrays, fields with IDs, titles, URLs, and so on. This is extremely useful for the model, but not necessarily friendly for humans.&lt;/p&gt;

&lt;p&gt;When Copilot receives that output, it treats it as &lt;strong&gt;additional context&lt;/strong&gt;, not as the final answer. With this extra data, the model can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick only the relevant fields&lt;/li&gt;
&lt;li&gt;Format them nicely&lt;/li&gt;
&lt;li&gt;Explain what is going on in natural language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the step where raw tool output becomes the clear answer in your chat, powered by real data from the MCP server.&lt;/p&gt;

&lt;p&gt;At this point, you know how Copilot finds the right tool and turns its output into something readable. But there is still an interesting question left.&lt;/p&gt;
&lt;h2&gt;
  
  
  How does Copilot know who you are?
&lt;/h2&gt;

&lt;p&gt;In the earlier example, the pull request search tool needs to know &lt;strong&gt;which&lt;/strong&gt; user and repository it should work with.&lt;/p&gt;

&lt;p&gt;If you look at the MCP output tab, you can see that the Copilot chat is initialized with your GitHub username and other pieces of repository context.&lt;/p&gt;

&lt;p&gt;Out of curiosity, you can even ask Copilot directly: "what is my GitHub username?" It can answer based on the repository context and the information injected into the session.&lt;/p&gt;

&lt;p&gt;More specifically, Copilot chat adds details like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your GitHub username&lt;/li&gt;
&lt;li&gt;The repository you are working in&lt;/li&gt;
&lt;li&gt;The current branch&lt;/li&gt;
&lt;li&gt;And other metadata needed by tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how the language model "knows" who you are inside Copilot chat: not by guessing, but because the client explicitly shares that information as part of the context.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why understanding this matters
&lt;/h2&gt;

&lt;p&gt;It is easy to look at these features and think that everything is pure magic.&lt;/p&gt;

&lt;p&gt;But when you understand that there is a clear protocol, a list of servers, a list of tools, input schemas, and structured outputs, everything becomes much more tangible.&lt;/p&gt;

&lt;p&gt;Knowing how things move behind the scenes gives you more control. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better understand what Copilot is capable of&lt;/li&gt;
&lt;li&gt;Reason about which tools might be useful for your workflow&lt;/li&gt;
&lt;li&gt;Design or configure MCP servers that expose exactly the tools you need&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Magic is cool, but knowledge is your real power.&lt;/p&gt;

&lt;p&gt;I'm planning to create more content exploring how VS Code, Copilot, and MCP work together, going beyond the "wow" moment and into the practical details.&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope this answered some of your questions about how Copilot uses MCP tools.&lt;/p&gt;

&lt;p&gt;See you in the next one!&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





</description>
      <category>github</category>
      <category>githubcopilot</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
    <item>
      <title>Codemotion Milan 2025: Why Large Tech Conferences Matter</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Wed, 22 Oct 2025 15:52:00 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/codemotion-milan-2025-why-large-tech-conferences-matter-5e5j</link>
      <guid>https://forem.com/playfulprogramming/codemotion-milan-2025-why-large-tech-conferences-matter-5e5j</guid>
      <description>&lt;p&gt;I have wanted to write about tech conferences for a while, and last week I was at Codemotion in Milan. Perfect excuse to finally do it.&lt;/p&gt;

&lt;p&gt;If you follow me here you know I usually share tutorials and technical content, but there is another huge part of our craft that I discovered only a few years ago: conferences. They open new opportunities and new ways of thinking that you will miss if you only focus on the tech. That would be a shame.&lt;/p&gt;

&lt;p&gt;While I am still wearing the conference badge, here is why I think you should go to large tech events, what to expect, the pros, and yes, the cons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch the video
&lt;/h3&gt;

&lt;p&gt;This article is actually an edited version of me rambling for ten minutes, you can watch it here:&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Xmqzx2qX6Eg"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet people
&lt;/h2&gt;

&lt;p&gt;The biggest advantage is simple: meet people. It may sound weird at a technical conference with folks on stage talking about code, but the highest value often comes from the hallway track.&lt;/p&gt;

&lt;p&gt;Even as an attendee in the audience you get to meet a lot of amazing people. At Codemotion this year there were roughly 3000 attendees in the same venue, so there is plenty of room to find interesting conversations. As developers we might not love chit chat, but conversations at conferences are nothing like daily standups or status meetings. It is a different vibe, and among thousands of people you will surely find someone you click with.&lt;/p&gt;

&lt;p&gt;These events are also designed to help you connect, not only to listen or deliver great talks. One thing I love at Codemotion is the Tech Expert booths: tables with experts on specific topics. You walk up, start from a shared interest like React or machine learning, others join, and it quickly becomes a small group discussion. It is a great way to meet new people.&lt;/p&gt;

&lt;p&gt;I usually go with friends I met at work or at other events, but even if you go alone you will find many ways to make new ones. In large conferences you will keep seeing the same faces across editions and your relationships get stronger. You never know how much value can come from those interactions.&lt;/p&gt;

&lt;p&gt;Talking to people lets you see the world from different points of view. We all live in a company bubble, which is fine and inevitable, but hearing from other developers, PMs, and folks in the industry broadens your perspective. It helps shape your opinions. The more you know, the better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard skills? Curiosity!
&lt;/h2&gt;

&lt;p&gt;On paper the selling point of conferences should be the technical content. You go for the schedule of interesting talks. Sure, you can pick up hard skills from a session with an expert on stage, but for most talks the biggest takeaway is not learning a new syntax on the spot.&lt;/p&gt;

&lt;p&gt;It is about getting inspired. A 30 to 40 minute session is not a course or a masterclass. You are not expected to walk out with deep understanding. Instead, you leave with a spark. You hear the right keywords, see a demo, and your curiosity switches on. Then when you get home you open your laptop and try it yourself. That is where the real learning happens. The talk gives you pointers and context so your research is faster and more focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keynotes
&lt;/h2&gt;

&lt;p&gt;Large events also bring in great keynotes. On day two at Codemotion we had Scott Chacon, co-founder of GitHub, on stage talking about the why behind things. Hearing stories from people who actually built something meaningful is a huge source of inspiration. You get a glimpse into their thought process and collect advice you can apply to your own career.&lt;/p&gt;

&lt;p&gt;And the coolest part is that it is not a YouTube video. The person is there, in the same venue. If you are lucky you can even meet them, shake hands, and have a quick chat. In the era of AI generated content, real life human interactions keep gaining value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many parallel tracks
&lt;/h2&gt;

&lt;p&gt;Not everything is perfect. The thing I like the least about large conferences is when there are too many parallel tracks. It is intentional and inevitable, but when there are six or seven tracks at the same time it gets hard to choose.&lt;/p&gt;

&lt;p&gt;It is a happy problem to have many great options, but sometimes there are three talks you want to attend in the same slot and you can only pick one. Recordings help, but let's be honest: the magic is being there live. I do watch recorded talks, especially from events I could not attend, but for the ones I skip on site I rarely catch up later. You should, though. If recordings are available, watch them to fill the gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Large crowd
&lt;/h2&gt;

&lt;p&gt;Crowds can be a downside too. Venues are usually huge, so if the area near the stages is packed you can step outside and breathe. There is almost always space.&lt;/p&gt;

&lt;p&gt;The tricky part is meeting someone specific. With so many people you will need to text and coordinate. It happened this year that I wanted to meet a couple of folks and never bumped into them. A simple tip: post on LinkedIn or your socials that you will be at the conference so friends and connections can plan to meet you at a specific spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tickets and travel
&lt;/h2&gt;

&lt;p&gt;Travel time and ticket costs are real tradeoffs. Big events tend to be pricey, but if you know you are going keep an eye on early bird tickets. They are usually much cheaper. The same applies to travel and accommodation: planning ahead saves money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you go?
&lt;/h2&gt;

&lt;p&gt;Wrapping up: large tech conferences get a big yes from me. You know the pros and the cons. Go there, learn, network, and most importantly have fun. We are there to meet people, grow, and enjoy the experience.&lt;/p&gt;

&lt;p&gt;This one was about large events like Codemotion Milan. The world of tech events also includes medium and small conferences. I attended a couple of those this month, and one where I was a speaker, so I might write about them next. Stay tuned!&lt;/p&gt;




&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;




</description>
      <category>techtalks</category>
      <category>career</category>
    </item>
    <item>
      <title>ReactJS Day 2025: TanStack Start &amp; Real World Experiences</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Mon, 20 Oct 2025 18:32:45 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/reactjs-day-2025-tanstack-start-real-world-experiences-16b9</link>
      <guid>https://forem.com/playfulprogramming/reactjs-day-2025-tanstack-start-real-world-experiences-16b9</guid>
      <description>&lt;p&gt;When I'm writing this I'm still on the train back from &lt;a href="https://www.reactjsday.it/" rel="noopener noreferrer"&gt;ReactJSDay&lt;/a&gt;, the largest conference on ReactJS in Italy, reflecting on something that happened from the audience.&lt;/p&gt;

&lt;p&gt;I gave my talk about TanStack Start, showcasing why I like this full stack framework and how it is powered by TanStack Router, my favourite routing library as of today.&lt;/p&gt;

&lt;p&gt;At the end of the session I got a question about how much effort would it take to migrate a very large React Router codebase to TanStack Router. My answer has been honest: I’ve been lucky enough to always start fresh with TanStack Router or only migrate smaller project so I didn’t have a direct experience to share. By the book it is indeed doable (and there’s a migration guide in the docs), but that was it from my side.&lt;/p&gt;

&lt;p&gt;However, during lunch there was a board where attendees could reserve a spot for a lightning talk and to my surprise I’ve seen a proposal about "how we migrated a huge codebase from React Router to TanStack Router".&lt;/p&gt;

&lt;p&gt;The attendee showed up on stage, opening up with "I heard the question from this morning talk, we did that migration on my team and I wanted to share how it went" and I absolutely loved it!&lt;/p&gt;

&lt;p&gt;My talk was relatively easy, I had my slides, all demos prepared on vscode (thanks Elio for &lt;a href="https://demotime.show/" rel="noopener noreferrer"&gt;Demo Time&lt;/a&gt;, such an amazing tool for speakers) but showing up with a spontaneous and unprepared lighting talk, sharing about a real world experience, is just something else.&lt;/p&gt;

&lt;p&gt;I feel bad I didn’t take a picture of the lightning talks board and I only remember the attendee is called Linus, if you get to see this post please let’s connect!&lt;/p&gt;

&lt;p&gt;Thank you Grusp for having me on stage, Claranet Italia for supporting my DevRel journey and everyone who attended the event, such a magical one!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2cv34rqulfdww4s7yiht.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2cv34rqulfdww4s7yiht.jpeg" alt="Me talking"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;




</description>
      <category>react</category>
      <category>community</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>TanStack Router: Go to Previous page after Sign In</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Tue, 16 Sep 2025 14:36:21 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/tanstack-router-go-to-previous-page-after-sign-in-51fp</link>
      <guid>https://forem.com/playfulprogramming/tanstack-router-go-to-previous-page-after-sign-in-51fp</guid>
      <description>&lt;p&gt;Welcome back to the TanStack Router series, today going double digits with &lt;a href="https://www.youtube.com/playlist?list=PLOQjd5dsGSxJilh0lBofeY8Qib98kzmF5" rel="noopener noreferrer"&gt;chapter 10&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Let's fix a very common UX issue when implementing authentication flows. You navigate to a page then you're forced to sign in, but then you get redirected to the homepage. Maybe you set some filters or data, now everything is gone.&lt;/p&gt;

&lt;p&gt;In this article I will show two approaches to redirect users back to the page they were on right before signing in, keeping any query parameters intact. First the classic explicit redirect, then an alternative that can capture the previous location automatically.&lt;/p&gt;

&lt;p&gt;Video version here: &lt;a href="https://youtu.be/MXffKeNfOvQ" rel="noopener noreferrer"&gt;https://youtu.be/MXffKeNfOvQ&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one sentence
&lt;/h2&gt;

&lt;p&gt;You sign in and get redirected to the homepage, losing the page and search parameters you had before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: explicit redirect via search param
&lt;/h2&gt;

&lt;p&gt;This is the straightforward and reliable solution. We explicitly pass a &lt;code&gt;redirectTo&lt;/code&gt; param everywhere the user can navigate to the sign in page and we use it to go back after signing in.&lt;/p&gt;

&lt;p&gt;Our Sign In form component will look like this, it defaults to the homepage or redirects to the provided URL after a successful sign in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SignInForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&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;signIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Your sign in logic here&lt;/span&gt;

    &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* form fields */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign In&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But where does &lt;code&gt;redirectTo&lt;/code&gt; come from? Let's add it as a search parameter to the sign in route.&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;createFileRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&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;SignInForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/components/auth/sign-in-form&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;Layout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/components/layout&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;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sign-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RouteComponent&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;redirectTo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSearch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SignInForm&lt;/span&gt; &lt;span class="na"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;redirectTo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What is that &lt;code&gt;validateSearch&lt;/code&gt; doing? It uses Zod to parse the search parameters and extract &lt;code&gt;redirectTo&lt;/code&gt;, defaulting it to &lt;code&gt;/&lt;/code&gt; if not provided. Then in the component we read it with &lt;code&gt;Route.useSearch()&lt;/code&gt; and pass it down to the &lt;code&gt;SignInForm&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to learn more about handling search parameters with TanStack Router, check out my previous article: &lt;a href="https://leonardomontini.dev/tanstack-router-query-params/" rel="noopener noreferrer"&gt;Handling Query Parameters&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next steps is to ensure that every link to the sign in page includes the &lt;code&gt;redirectTo&lt;/code&gt; parameter. Here's an example in the &lt;code&gt;Header&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&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;ButtonLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../button-link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ButtonLink&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/sign-in"&lt;/span&gt; &lt;span class="na"&gt;search&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;redirectTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&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;
        Sign In
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ButtonLink&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&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;blockquote&gt;
&lt;p&gt;What is &lt;code&gt;ButtonLink&lt;/code&gt;? It is just a styled wrapper around the &lt;code&gt;Link&lt;/code&gt; component from TanStack Router. You can use &lt;code&gt;Link&lt;/code&gt; directly if you prefer or learn how to create your own custom link component in a video I made about it: &lt;a href="https://youtu.be/-kmf3ZYlduU" rel="noopener noreferrer"&gt;https://youtu.be/-kmf3ZYlduU&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With this, after a successful sign in you will land back on the original page including all the search parameters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tradeoff of the explicit approach
&lt;/h2&gt;

&lt;p&gt;You must remember to add the &lt;code&gt;redirectTo&lt;/code&gt; param in every link to the sign in page. Not a big issue though, and you can wrap it in a helper or custom hook like a "go to sign in" utility, but it is still one more thing to do.&lt;/p&gt;
&lt;h2&gt;
  
  
  Approach 2: capture previous location automatically
&lt;/h2&gt;

&lt;p&gt;I also experimented with a small hook that tracks the previous location without passing any search parameters to the sign in page. I'm not entirely sure how stable and reliable this is, but it worked well in my tests so I thought it was worth sharing:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePreviousLocation&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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;previousLocation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPreviousLocation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;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="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;return&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onResolved&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;fromLocation&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;setPreviousLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromLocation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="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;previousLocation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This hook subscribes to the router's navigation events and captures the "from" location after each navigation. It stores it in state so it can be used later.&lt;/p&gt;

&lt;p&gt;With this alone you can forget about everything we already said, you'll no longer need to pass &lt;code&gt;redirectTo&lt;/code&gt; in each navigation or handle search params. Just use the hook in your sign in form:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SignInForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&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;previousLocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePreviousLocation&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;signIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Your sign in logic here&lt;/span&gt;

    &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;previousLocation&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* form fields */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign In&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Combine both for a robust UX
&lt;/h2&gt;

&lt;p&gt;The two approaches might also work well together. You can prefer the explicit &lt;code&gt;redirectTo&lt;/code&gt; when provided, and fall back to the previously captured location when it is not. This covers both deliberate redirects and default behavior without extra effort.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SignInForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&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;previousLocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePreviousLocation&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;signIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Your sign in logic here&lt;/span&gt;

    &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;previousLocation&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* form fields */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign In&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This article focuses on TanStack Router, but the same idea works in TanStack Start too.&lt;/li&gt;
&lt;li&gt;If you are using authenticated routes or guards, you might also like this related article: &lt;a href="https://leonardomontini.dev/tanstack-router-authenticated-guards/" rel="noopener noreferrer"&gt;Authenticated Routes and Guards&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That is it. With either the explicit redirect or the previous location approach, you can make your sign in flow return users to exactly where they left off, preserving query parameters and their context.&lt;/p&gt;

&lt;p&gt;If you have different ideas or improvements, let me know in a comment.&lt;/p&gt;

&lt;p&gt;Code is taken from the &lt;a href="https://confhub.tech/" rel="noopener noreferrer"&gt;ConfHub project&lt;/a&gt;, you can find it here: &lt;a href="https://github.com/Balastrong/confhub" rel="noopener noreferrer"&gt;https://github.com/Balastrong/confhub&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>4 Free Methods to use LLM APIs in Development</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Tue, 09 Sep 2025 10:16:55 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/4-free-methods-to-use-llm-apis-in-development-45f6</link>
      <guid>https://forem.com/playfulprogramming/4-free-methods-to-use-llm-apis-in-development-45f6</guid>
      <description>&lt;p&gt;You might be in the situation I was the other day: I wanted to develop a small AI feature for learning purposes on my &lt;a href="//confhub.tech"&gt;side project&lt;/a&gt;, but I didn’t want to pay for an api key.&lt;/p&gt;

&lt;p&gt;So I did some research: let me show you 4 different ways I found to do that for free, with also the possibility of switching between a wide range of models, so you can pick the best for your usecase.&lt;/p&gt;

&lt;p&gt;My first attempt obviously went to Ollama to run models locally, then I had a go at the free tiers of some hosted providers.&lt;/p&gt;

&lt;p&gt;You can hear me talking about those and showing some steps and demos in this &lt;a href="https://youtu.be/87HrBpOZeUE" rel="noopener noreferrer"&gt;YouTube video&lt;/a&gt; or you can keep reading below.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/87HrBpOZeUE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code setup
&lt;/h2&gt;

&lt;p&gt;Let's begin with some good news: all the four methods work with the exact same code as luckily they all support the OpenAI SDK. The only change will be setting the right values in your environment variables.&lt;/p&gt;

&lt;p&gt;If you're curious you can find &lt;a href="https://github.com/Balastrong/confhub/blob/main/src/services/ai.api.ts" rel="noopener noreferrer"&gt;here the actual code&lt;/a&gt; I'm using in my demo, including the system prompt and my silly attemps to force the model to behave and answer as I want.&lt;/p&gt;

&lt;p&gt;Anyway, I don't want to add any unnecessary complexity to this article so I'll just show a summary that you can use as a starting point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_TOKEN&lt;/span&gt;&lt;span class="o"&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;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_ENDPOINT&lt;/span&gt;&lt;span class="o"&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&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;response&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a helpful assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;
  
  
  1. Ollama - Run models locally
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homepage&lt;/strong&gt;: &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;https://ollama.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the first thing I tried as I wanted to see how it works to run models locally. Long story short: they made it so easy it's like pulling docker images. There's literally a &lt;code&gt;ollama pull&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;You can install the app from the website then you'll find the &lt;code&gt;ollama&lt;/code&gt; command line tool in your path. Actually when I did it a couple of weeks ago the app was almost empty, now I see they started adding features like a UI where you can chat with your installed models (in addition to talking with the in the terminal) and some other nice touches.&lt;/p&gt;

&lt;p&gt;At this point you might want to have some models, right? You can find a list in their &lt;a href="https://ollama.com/search" rel="noopener noreferrer"&gt;Models&lt;/a&gt; page then open a terminal and run &lt;code&gt;ollama pull &amp;lt;model-name&amp;gt;&lt;/code&gt; to download it. Before picking a model have a look at the size (might be many GB) and get ready to have slow responses if your machine is not powerful enough.&lt;/p&gt;

&lt;p&gt;You can see at any time what models you have with &lt;code&gt;ollama ls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Back to our quest of learning how to use LLMs in our code, now that you have Ollama up and running with at least a model downloaded, you can set these values for your environment variables:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LLM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama
&lt;span class="nv"&gt;LLM_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:11434/v1
&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your_downloaded_model&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You're good to go!&lt;/p&gt;
&lt;h2&gt;
  
  
  2. GitHub Models - Hosted
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homepage&lt;/strong&gt;: &lt;a href="https://github.com/marketplace?type=models" rel="noopener noreferrer"&gt;https://github.com/marketplace?type=models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Tier&lt;/strong&gt;: &lt;a href="https://docs.github.com/en/github-models/use-github-models/prototyping-with-ai-models#rate-limits" rel="noopener noreferrer"&gt;https://docs.github.com/en/github-models/use-github-models/prototyping-with-ai-models#rate-limits&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ollama was fun but I wasn't using really smart models and at some point I wanted to push my AI feature to production so... I couldn't just say "works on my machine with Ollama", I had to find a hosted solution. For free.&lt;/p&gt;

&lt;p&gt;The first service I tried was GitHub Models. It basically offers some of the most recent models with a free tier (you don't even need a Copilot subscription) that you can test already in the browser.&lt;/p&gt;

&lt;p&gt;You can also use them in your app and you just need a single API key: a GitHub Personal Access Token (PAT).&lt;/p&gt;

&lt;p&gt;You can generate one from your developer settings, but on the Models marketplace you'll find a direct link to make it literally one click. It should also work from here: &lt;a href="https://github.com/settings/personal-access-tokens/new?description=Used+to+call+GitHub+Models+APIs+to+easily+run+LLMs%3A+https%3A%2F%2Fdocs.github.com%2Fgithub-models%2Fquickstart%23step-2-make-an-api-call&amp;amp;name=GitHub+Models+token&amp;amp;user_models=read" rel="noopener noreferrer"&gt;https://github.com/settings/personal-access-tokens/new&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have the token, set it in your environment variables and again you're good to go!&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LLM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your_github_pat&amp;gt;
&lt;span class="nv"&gt;LLM_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://models.github.ai/inference
&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;model_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In my app I just set these variables on Netlify to make the AI feature work on my production site.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Open Router - Hosted
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homepage&lt;/strong&gt;: &lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;https://openrouter.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Tier&lt;/strong&gt;: &lt;a href="https://openrouter.ai/docs/api-reference/limits" rel="noopener noreferrer"&gt;https://openrouter.ai/docs/api-reference/limits&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was happy with GitHub Models but for the sake of a good research I wanted to try multiple providers. Open Router is another hosted solution that offers a free tier but on some selected models.&lt;/p&gt;

&lt;p&gt;You can find them all by &lt;a href="https://openrouter.ai/models?q=%3Afree" rel="noopener noreferrer"&gt;filtering the list&lt;/a&gt; for &lt;code&gt;:free&lt;/code&gt; in the model name.&lt;/p&gt;

&lt;p&gt;Once you sign up you can get your API Key from the &lt;a href="https://openrouter.ai/settings/keys" rel="noopener noreferrer"&gt;settings&lt;/a&gt; and put it in your environment variables as usual:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LLM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your_open_router_api_key&amp;gt;
&lt;span class="nv"&gt;LLM_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://openrouter.ai/api/v1
&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;model_name:free&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Groq - Hosted
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homepage&lt;/strong&gt;: &lt;a href="https://groq.com/" rel="noopener noreferrer"&gt;https://groq.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Tier&lt;/strong&gt;: &lt;a href="https://console.groq.com/docs/rate-limits#rate-limits" rel="noopener noreferrer"&gt;https://console.groq.com/docs/rate-limits#rate-limits&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first thing I said when a colleague told me about Groq is "wait isn't it called Grok?" and no it wasn't a typo, they're two very different things.&lt;/p&gt;

&lt;p&gt;Groq is a hardware company that runs LLMs on their own chips.&lt;/p&gt;

&lt;p&gt;Similarly to Open Router, with a Groq API key you can access a selection of supported models and switch between them by changing the model name in your environment variables.&lt;/p&gt;

&lt;p&gt;You can get the key after signing up, but you shouldn't be surprised now after reading the previous sections. Luckily, most of these providers work in a very similar way.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LLM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your_groq_api_key&amp;gt;
&lt;span class="nv"&gt;LLM_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.groq.com/openai/v1
&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;model_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;These approaches I tried were all giving the possiblity to switch between different models, which is great to find the best one for your usecase.&lt;/p&gt;

&lt;p&gt;In any case, all LLMs providers usually have their own free tiers. There's plenty of options you can pick directly from the vendor, be it OpenAI, Anthropic, Google, etc.&lt;/p&gt;

&lt;p&gt;When it's time to go to production... well, that's another story.&lt;/p&gt;

&lt;p&gt;What are you using for development? And what are you using in production? Let's discuss in the comments!&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>llm</category>
      <category>node</category>
      <category>github</category>
    </item>
    <item>
      <title>TanStack Start: light, dark, and system theme without flickers</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Mon, 25 Aug 2025 12:33:00 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/tanstack-start-light-dark-and-system-theme-without-flickers-5f1d</link>
      <guid>https://forem.com/playfulprogramming/tanstack-start-light-dark-and-system-theme-without-flickers-5f1d</guid>
      <description>&lt;p&gt;Having multiple themes is a common request on (web) applications nowadays, at least having light, dark, and system (define dark/light automatically from the user's system).&lt;/p&gt;

&lt;p&gt;If your application has also some sort of server-side rendering, this seemingly simple request might quickly become more complex than expected, in particular once you start seeing issues like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app initially loads with the wrong theme (FOUC: Flash Of Unstyled Content)&lt;/li&gt;
&lt;li&gt;You refresh the page and the theme is gone&lt;/li&gt;
&lt;li&gt;The user changes the system theme and the app doesn't follow&lt;/li&gt;
&lt;li&gt;Some weird errors are logged as client-only APIs are called on the server&lt;/li&gt;
&lt;li&gt;Hydration issues everywhere&lt;/li&gt;
&lt;li&gt;... the list goes on&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I recently took some time to implement a robust approach for a TanStack Start application (&lt;a href="https://github.com/Balastrong/start-theme-demo" rel="noopener noreferrer"&gt;code here&lt;/a&gt;) and I recorded a video where I explain step by step all the moving parts, you can watch it here and keep this written article as reference for later.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/NoxvbjkyLAg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  The two theme types
&lt;/h3&gt;

&lt;p&gt;Let's begin with the types definition. I like to distinguish between what the user chooses and what the app actually renders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Exclude&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;UserTheme&lt;/code&gt; is the user's explicit choice, while &lt;code&gt;AppTheme&lt;/code&gt; is the resolved theme that the app actually uses for rendering.&lt;/p&gt;
&lt;h3&gt;
  
  
  Storage that doesn't break SSR
&lt;/h3&gt;

&lt;p&gt;Let's persist the user's choice through localStorage.&lt;/p&gt;

&lt;p&gt;Wait... but isn't it a client only API? Yes it is, the usual architectural choice is between localStorage and cookies. I'll get a bit more into detail at the end of the article if you're curious, but for now let's go with the localStorage approach.&lt;/p&gt;

&lt;p&gt;Rule number 1 is: never touch &lt;code&gt;window&lt;/code&gt; or &lt;code&gt;localStorage&lt;/code&gt; when running on the server. There's another interesting rule but I'll tell you later... ok no let me put it now, when doing the first render (on the server) you can't rely on js or you'll get hydration errors and weird flashes. We'll see that in practice in the theme switcher.&lt;/p&gt;

&lt;p&gt;here's the approach through some utility methods: a safe getter that returns 'system' on the server and validates values on the client; and a setter that no‑ops on the server.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStoredUserTheme&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeStorageKey&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;stored&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;themes&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;stored&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;stored&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setStoredTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeStorageKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Resolving the system theme
&lt;/h3&gt;

&lt;p&gt;Browsers expose the OS preference via &lt;code&gt;matchMedia('(prefers-color-scheme: dark)')&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSystemTheme&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AppTheme&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&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;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;light&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;With that alone if the user changes preference (for example in the OS settings) while your page is loaded, the app won't reflect that change until a full reload occurs. The cool thing is that you can &lt;em&gt;subscribe&lt;/em&gt; to that.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupPreferredListener&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;mediaQuery&lt;/span&gt; &lt;span class="o"&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;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&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;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleThemeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mediaQuery&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;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mediaQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&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;Why is this function returning a cleanup function? No mistery, we're going to use it inside a useEffect, it's the cleanup function for the event listener to avoid memory leaks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Applying the theme to the DOM
&lt;/h3&gt;

&lt;p&gt;The DOM definition of the theme is on a class in the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element, either &lt;code&gt;light&lt;/code&gt;, &lt;code&gt;dark&lt;/code&gt;. If it's &lt;code&gt;system&lt;/code&gt;, we'll also set that in the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleThemeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;root&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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;dark&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;system&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;newTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userTheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;getSystemTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userTheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTheme&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;userTheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&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;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The ThemeProvider
&lt;/h3&gt;

&lt;p&gt;Probably the most common usecase for React Context, the ThemeProvider component makes it easy to access and update the theme throughout your application.&lt;/p&gt;

&lt;p&gt;On mount, set the initial theme from storage and wire the system listener only if &lt;code&gt;userTheme === 'system'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When setting a new theme: update state, persist to storage, and re‑apply classes to &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The implementation might be something like:&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;type&lt;/span&gt; &lt;span class="nx"&gt;ThemeContextProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;appTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;ThemeContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ThemeContextProps&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ThemeProviderProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ThemeProviderProps&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;userTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUserTheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserTheme&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;getStoredUserTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userTheme&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;setupPreferredListener&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;userTheme&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;appTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userTheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;getSystemTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userTheme&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;setTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newUserTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;setUserTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newUserTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStoredTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newUserTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;handleThemeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newUserTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeContext&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;appTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ScriptOnce&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;themeScript&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;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ThemeContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ThemeContext&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;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;useTheme must be used within a ThemeProvider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;context&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;
  
  
  The crucial inline script: no FOUC
&lt;/h3&gt;

&lt;p&gt;If you're already into the cookie vs localStorage debate, you know that in order to make this work you need to inject a tiny inline script that runs immediately, before hydration, to set the correct class on the root element. If you also have a sharp eye you noticed that &lt;code&gt;&amp;lt;ScriptOnce children={themeScript} /&amp;gt;&lt;/code&gt; in the previous snippet.&lt;/p&gt;

&lt;p&gt;The easiest way in TanStack Start to inject this inline script is to use the &lt;code&gt;ScriptOnce&lt;/code&gt; component, which allows you to run a script only once during the initial render.&lt;/p&gt;

&lt;p&gt;One tiny annoyance with inline scripts is that you write them as plain strings... so here's a magic trick to write it as a proper js function, enjoying linters and all kind of IDE support, then putting it the toString version inside an IIFE&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;themeScript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;themeFn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storedTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&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;validTheme&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;light&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;dark&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;system&lt;/span&gt;&lt;span class="dl"&gt;'&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;storedTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;storedTheme&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&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;validTheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;systemTheme&lt;/span&gt; &lt;span class="o"&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;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;light&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systemTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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;systemTheme&lt;/span&gt; &lt;span class="o"&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;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;light&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systemTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;themeFn&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="s2"&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;With this tiny bit of logic, that is converted to a string, you make sure &lt;em&gt;as soon as possible&lt;/em&gt; to add the correct css class.&lt;/p&gt;

&lt;p&gt;What happens without this? You'll get a flash: the page loads in the default style, then flips after React mounts. The inline script prevents that by writing the class during the initial HTML paint.&lt;/p&gt;

&lt;p&gt;For clarity, here's the "too late" version using only &lt;code&gt;useEffect&lt;/code&gt; so you can compare the behavior:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;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;handleThemeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userTheme&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;You do this anywhere in your app? You get a FOUC. That's why we did the inline script magic.&lt;/p&gt;
&lt;h3&gt;
  
  
  Let CSS, not JS, drive the toggle UI
&lt;/h3&gt;

&lt;p&gt;Here's the rule number 2 I mentioned earlier.&lt;/p&gt;

&lt;p&gt;Because initial values settle before React, UI that depends on JS state will likely flicker as the server renders something (e.g. the icon for the light theme) but then on the client it gets overridden by the actual state and replaced with the dark theme... because you're using the dark theme, right?&lt;/p&gt;

&lt;p&gt;The safest approach is to let CSS decide visibility based on the root classes. With Tailwind v4 you can use &lt;code&gt;:not()&lt;/code&gt; and class selectors to keep it simple.&lt;/p&gt;

&lt;p&gt;Here's an 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;themeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;System&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ThemeToggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&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;getNextTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;themes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;currentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;themes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userTheme&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;nextIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;themes&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;themes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nextIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getNextTheme&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"w-28"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"not-system:light:inline hidden"&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;themeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&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="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ml-1"&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;themeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&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;"not-system:dark:inline hidden"&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;themeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&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="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ml-1"&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;themeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&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;"system:inline hidden"&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;themeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&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="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ml-1"&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;themeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can &lt;code&gt;userTheme&lt;/code&gt; the theme coming from the hook at any &lt;em&gt;other&lt;/em&gt; time, for example to cycle between themes on user click, but on the &lt;em&gt;initial render&lt;/em&gt; you can't. CSS will drive your button.&lt;/p&gt;
&lt;h2&gt;
  
  
  TanStack Start primitives: clientOnly and createIsomorphicFn
&lt;/h2&gt;

&lt;p&gt;To avoid manual &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt; checks, you can use the Start utilities so you can define client‑only logic or dual client/server logic without sprinkling conditions everywhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;clientOnly(fn)&lt;/code&gt;: throws on server, runs on client&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createIsomorphicFn({ server, client })&lt;/code&gt;: given the isomorphic nature of some functions, lets you define different behaviors on client and server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're perfect for storage helpers and DOM‑touching functions, look at how expressive the code becomes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStoredUserTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createIsomorphicFn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server&lt;/span&gt;&lt;span class="p"&gt;(():&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(():&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeStorageKey&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;stored&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;themes&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;stored&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;stored&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setStoredTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clientOnly&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeStorageKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Validate with Zod
&lt;/h2&gt;

&lt;p&gt;Instead of hand‑checking strings from storage, define a Zod enum with a &lt;code&gt;catch('system')&lt;/code&gt;. Then call &lt;code&gt;schema.parse(value)&lt;/code&gt; and you're guaranteed a valid &lt;code&gt;UserTheme&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserThemeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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;dark&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;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&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;AppThemeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;UserThemeSchema&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;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;AppThemeSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStoredUserTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createIsomorphicFn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server&lt;/span&gt;&lt;span class="p"&gt;(():&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(():&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeStorageKey&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;UserThemeSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&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;setStoredTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clientOnly&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validatedTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserThemeSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeStorageKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validatedTheme&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;
  
  
  Cookies vs LocalStorage (and when to use them)
&lt;/h2&gt;

&lt;p&gt;To be honest I don't have clear evidence that one approach is absolutely superior than the other as they both come with their own trade-offs. In most cases it doesn't matter that much anyway, just pick the approach that seems more reasonable and you'll be fine.&lt;/p&gt;

&lt;p&gt;The localStorage approach lives in the browser only which is good but requires js to run (on hydration) and you have to do those CSS tricks to control the initial render. Besides, the server has no knowledge of the user's preference.&lt;/p&gt;

&lt;p&gt;The cookie approach makes the server aware of the theme, but it also means the browser has to deal with the server for each theme change that should just be a client function.&lt;/p&gt;

&lt;p&gt;In any case, on the same repo you can find in the commit history a version with the cookie approach: &lt;a href="https://github.com/Balastrong/start-theme-demo/tree/077010bee3ca25ba775a4d452d55244cf8971637" rel="noopener noreferrer"&gt;https://github.com/Balastrong/start-theme-demo/tree/077010bee3ca25ba775a4d452d55244cf8971637&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrap‑up
&lt;/h2&gt;

&lt;p&gt;So here's the full flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep UserTheme ('light'|'dark'|'system') separate from AppTheme ('light'|'dark') and derive the latter.&lt;/li&gt;
&lt;li&gt;Use safe storage helpers that default on the server and validate localStorage values on the client.&lt;/li&gt;
&lt;li&gt;Write classes on document.documentElement (light/dark and optional system) whenever the theme changes.&lt;/li&gt;
&lt;li&gt;Provide userTheme, appTheme, and setTheme via Context and listen to prefers-color-scheme when on system.&lt;/li&gt;
&lt;li&gt;Inject a tiny inline script to set the initial html class before hydration to eliminate FOUC.&lt;/li&gt;
&lt;li&gt;Let CSS driven by root classes control the toggle UI so it renders correctly on first paint.&lt;/li&gt;
&lt;li&gt;Optionally use TanStack Start clientOnly/createIsomorphicFn and Zod enums to simplify and validate logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before going, here some useful links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live Demo: &lt;a href="https://tanstack-start-theme-demo.netlify.app/" rel="noopener noreferrer"&gt;https://tanstack-start-theme-demo.netlify.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Repository: &lt;a href="https://github.com/Balastrong/start-theme-demo" rel="noopener noreferrer"&gt;https://github.com/Balastrong/start-theme-demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Walkthrough Video: &lt;a href="https://youtu.be/NoxvbjkyLAg" rel="noopener noreferrer"&gt;https://youtu.be/NoxvbjkyLAg&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now some homework for you, feel free to leave a star to the repo and like the video and... enjoy!&lt;/p&gt;

&lt;p&gt;Also any comment or feedback, please, let me know!&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>react</category>
      <category>ssr</category>
      <category>tailwindcss</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I vibe coded an online visitors counter for my blog</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Wed, 16 Jul 2025 09:56:21 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/i-vibe-coded-an-online-visitors-counter-for-my-blog-i31</link>
      <guid>https://forem.com/playfulprogramming/i-vibe-coded-an-online-visitors-counter-for-my-blog-i31</guid>
      <description>&lt;p&gt;You know that old-style "X users online" counter on a website? I've recently seen it on &lt;a href="https://roe.dev/" rel="noopener noreferrer"&gt;roe.dev&lt;/a&gt;'s blog and I though: it shouldn't be too difficult for a naive implementation, let's vibe code it!&lt;/p&gt;

&lt;p&gt;The stack: my blog is a static site built with Astro and hosted on Netlify, so I needed a way to track active visitors without a full backend. The goal was to create a simple counter that shows how many people are currently browsing the site, updating in real-time, without any annoying flickering.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Netlify Server Functions
&lt;/h2&gt;

&lt;p&gt;The main engine for this whole thing is Netlify's server functions. After all, I just needed a simple endpoint to ping when a visitor comes in, which also returns the current count of active users.&lt;/p&gt;

&lt;p&gt;I asked Copilot to write the logic in javascript and with a couple of iteration I already had a working demo on my local machine.&lt;/p&gt;

&lt;p&gt;Bonus points: since it recognized I was in a Netlify project, it automatically added the function to the right folder and set up the correct export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage on Redis
&lt;/h2&gt;

&lt;p&gt;Ok this might have been overkill, but I wanted a quick solution to store the data since the netlify function runs in a serverless environment, meaning it doesn't have persistent storage.&lt;/p&gt;

&lt;p&gt;I just googled "free redis hosting" and found Upstash, created an account and already had a Redis instance.&lt;/p&gt;

&lt;p&gt;Time to write a quick prompt for Copilot to use Redis as storage. Since I mentioned I wanted to track not who's exactly online but how many users visited in the past 30 minutes, it suggested a TTL (time to live) of 30 minutes for each key. Cool, I don't even need to worry about cleaning up old data.&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;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`online:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;anonymizedIp&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="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;As easy as that, there's the &lt;code&gt;ex&lt;/code&gt; option that sets the expiration time in seconds.&lt;/p&gt;

&lt;p&gt;Reading values is also simple, just count the keys with a pattern like &lt;code&gt;online:*&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;online:*&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keys&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Avoid flickering
&lt;/h2&gt;

&lt;p&gt;Once the backend was working, the next step was getting that counter to show up nicely on my static site without any annoying flickering.&lt;/p&gt;

&lt;p&gt;The problem is that since the page is static, the "dynamic" part (the counter) needs to be fetched after the page loads. If I just rendered the counter with the initial value, it would show zero until the fetch completed, which is not ideal. Oh, and this on each page load or navigation!&lt;/p&gt;

&lt;p&gt;So here's the plan, use 1 as default value to immediately show something, then fetch the actual count in the background and update it once we have the real number.&lt;/p&gt;

&lt;p&gt;With that alone I had the jump from 1 to the real number at each navigation though, so the first naive solution that came to my mind was to store the value in local storage and read it on each page load. This way, the counter would show the last known value immediately, and then update in the background.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local debugging
&lt;/h2&gt;

&lt;p&gt;Just type &lt;code&gt;netlify dev&lt;/code&gt; and you can run your Netlify functions locally, as easy as that, really!&lt;/p&gt;
&lt;h2&gt;
  
  
  So what?
&lt;/h2&gt;

&lt;p&gt;I'm sure there's a million better ways to build this tiny feature, but it's been fun to use AI to quickly build something that kinda works.&lt;/p&gt;

&lt;p&gt;Ah by the way, this wasn't a sponsored post (I wish it was!) but I think finding the right tools and services is relevant these days, in particular now that writing code goes much faster with AI.&lt;/p&gt;

&lt;p&gt;If you want to see the live result just head over &lt;a href="https://leonardomontini.dev/" rel="noopener noreferrer"&gt;https://leonardomontini.dev/&lt;/a&gt; or have a look at the video where I walk through the whole process and code:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/HAOMTH4uCkE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>githubcopilot</category>
      <category>astro</category>
      <category>netlify</category>
    </item>
    <item>
      <title>Modernizing a large multi-team application with Micro Frontends</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Fri, 27 Jun 2025 13:32:49 +0000</pubDate>
      <link>https://forem.com/claranet/modernizing-a-large-multi-team-application-with-micro-frontends-36dm</link>
      <guid>https://forem.com/claranet/modernizing-a-large-multi-team-application-with-micro-frontends-36dm</guid>
      <description>&lt;p&gt;Here’s a quite common scenario: you have a web application, built years ago, with deeply intertwined modules and dependencies. To make things worse, there are layers of legacy code built on top of each other, and multiple teams are dealing with the same codebase daily.&lt;/p&gt;

&lt;p&gt;Each team struggles to implement new features without stepping on each other’s toes, app-wide updates are a long and dangerous process and updating only a portion is just adding one more layer of tech debt and inconsistencies.&lt;/p&gt;

&lt;p&gt;The result? Slower development cycles, frustrated developers, and a product that struggles to keep up with user expectations and the market in general.&lt;/p&gt;

&lt;p&gt;One of our clients has been in a similar situation, here’s how we’re taking care of that with Micro Frontends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modernizing with Micro Frontends
&lt;/h2&gt;

&lt;p&gt;The main React application has almost 10 years and is bundled with an old version of Webpack while the new micro frontends are React apps running on Vite.&lt;/p&gt;

&lt;p&gt;The approach is to gradually extract smaller applications from the old system, using the long-time domain and project knowledge to properly know where to set the boundaries.&lt;/p&gt;

&lt;p&gt;Each smaller project has a unique goal in the system and is assigned to a dedicated and autonomous team. This brings a lot of advantages as each team can independently develop, deploy, and maintain their own portion of the system.&lt;/p&gt;

&lt;p&gt;Communication between the systems is mostly through custom events.&lt;/p&gt;

&lt;p&gt;With that in place, micro frontends enable incremental upgrades, allowing the legacy codebase to coexist with the new technologies. The teams can replace old modules one by one rather than rewriting the entire application all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downsides you better not ignore
&lt;/h2&gt;

&lt;p&gt;Before going all-in into your next huge refactor to adopt micro frontends, there are some downsides you must understand.&lt;/p&gt;

&lt;p&gt;To begin with, to reduce complexity of your old legacy system you’re going to adopt an architecture that for sure fixes some of the flaws but also brings some more complexity in other areas of your application. To name a few, passing data between components of a single app might be trivial nowadays but sharing the state and communicating between micro frontends might not be that obvious. There’s plenty of solutions, but the team must get used to them.&lt;/p&gt;

&lt;p&gt;Separating all the moving parts might seem a simplification but now you must handle each one of them independently, including deployment and versioning. It’s an aspect you might see as a benefit, but it undeniably adds complexity.&lt;/p&gt;

&lt;p&gt;Consistency can also be a challenge if not handled properly, as autonomous teams can easily drift and lose sight of the bigger picture. A common outcome can be large differences in UI components giving an unpolished look at the final product.&lt;/p&gt;

&lt;h2&gt;
  
  
  DOs and DON’Ts
&lt;/h2&gt;

&lt;p&gt;A few tips to guide your implementation to maximise the benefits and avoiding some common mistakes. Some of these are common sense, some we banged our heads on during the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  DOs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Do define clear boundaries for each micro frontend&lt;/strong&gt;&lt;br&gt;
We all should have learned the lesson from microservices, using the wrong criteria to break down the monolith and set boundaries will inevitably do more harm than good. Make sure each micro frontend represents a distinct feature or domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do ensure independent deployment&lt;/strong&gt;&lt;br&gt;
Micro frontends should be deployable independently. If you end up in the situation where you consistently can no longer deploy one sub-system by itself, you’re coupling too much and losing the benefits of having separate frontends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Do use shared libraries&lt;/strong&gt;&lt;br&gt;
Leverage shared libraries for common functionality like design systems, authentication, or API clients to reduce duplication of logic and inconsistencies. Module federation easily allows setting up shared libraries, use it.&lt;br&gt;
This should also help in making sure the user experience remains consistent through a centralized design system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Do start small&lt;/strong&gt;&lt;br&gt;
Begin with a single feature or module to test the waters. Gradually expand the micro frontend architecture as you gain confidence and refine your processes.&lt;br&gt;
If you’re transitioning from an old and large system this is even more relevant to avoid unexpected issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  DON'Ts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Don’t overcomplicate your architecture&lt;/strong&gt;&lt;br&gt;
Once you unlock the power of splitting into separate apps you might enjoy it too much. You can’t look at micro frontends as if they were simple folders in your monolithic app. Splitting and organizing components into separate folders is as easy as (auto) updating the imports, doing the same over many micro frontends can be way more painful.&lt;br&gt;
Avoid immediately splitting your application into too many micro frontends without a strong rationale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Don’t ignore cross-team collaboration&lt;/strong&gt;&lt;br&gt;
While micro frontends promote autonomy, teams must still collaborate to ensure consistency and avoid duplication. Regular communication and alignment are essential. Some code and logic will be duplicated, that’s fine, just make sure it’s intentional and under control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Don’t use different frameworks for the sake of it&lt;/strong&gt;&lt;br&gt;
Although micro frontends allow flexibility in technology choices, using entirely different frameworks (React for a module and Angular for another) can complicate integration and increase onboarding time for new developers.&lt;br&gt;
Just because you can doesn’t mean you should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don’t forget testing&lt;/strong&gt;&lt;br&gt;
Each micro frontend must be thoroughly tested both independently and as part of the larger application. Skipping integration tests can result in unexpected issues when modules interact with each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Don’t forget about versioning&lt;/strong&gt;&lt;br&gt;
Proper version control is critical for shared libraries and APIs used across micro frontends. Ensure backward compatibility to avoid breaking changes that disrupt other modules. If you end up having breaking changes all the time, maybe moving over to micro frontend wasn’t the right choice (or you’re doing it wrong!)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Don’t skip documentation&lt;/strong&gt;&lt;br&gt;
Document the architecture, module boundaries, communication protocols, and deployment processes thoroughly. This ensures smooth onboarding for new developers and reduces confusion over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Micro frontends are indeed an interesting and valuable architecture bringing several benefits, as in our case it allowed us to gradually upgrade the system without the risks of a full rewrite by keeping the teams almost independent from each other.&lt;/p&gt;

&lt;p&gt;However, it’s worth mentioning that it’s not always the best solution and it should be considered only if some conditions are met (application size, team(s) size, technical complexity, etc...). Smaller applications or teams may not benefit from the added overhead of managing multiple micro frontends, and simpler architectures might suffice for less complex systems.&lt;/p&gt;

&lt;p&gt;This architecture doesn’t magically get rid of complexity, but it shifts it to other places trying to make it more manageable. If you and your team are not ready to manage that new kind of complexity, think again if this is really what you’re looking for.&lt;/p&gt;

&lt;p&gt;As always, bad adoption of good practices can do more harm than good, do your research thoroughly and if you think you’ve got what it takes, then go for it and enjoy the flexibility of micro frontends!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>development</category>
    </item>
    <item>
      <title>How we'll measure performance of our DevRel activities</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Mon, 23 Jun 2025 14:31:13 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/how-well-measure-performance-of-our-devrel-activities-3ibo</link>
      <guid>https://forem.com/playfulprogramming/how-well-measure-performance-of-our-devrel-activities-3ibo</guid>
      <description>&lt;p&gt;▶️ &lt;em&gt;This article is extracted from a video transcript, slightly tweaked for better readability. Watch the original: &lt;a href="https://youtu.be/OabGHQ133zg" rel="noopener noreferrer"&gt;https://youtu.be/OabGHQ133zg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Welcome to the second update of me trying to launch a DevRel program in the company I work for. Today’s topic is going to be: metrics.&lt;/p&gt;

&lt;p&gt;So here’s the idea. Overall, right now we’re trying to launch the program. We're doing some activities, creating content, and pretty much everything we can to start and get the wheel spinning. But on a parallel track, something else we already had to take care of is trying to answer a really simple question: okay, cool, but is this thing actually working? How can we tell if the activities or the time spent doing those are actually worth the effort?&lt;/p&gt;

&lt;p&gt;I think today’s update is going to be around the difference between &lt;strong&gt;output&lt;/strong&gt; and &lt;strong&gt;outcome&lt;/strong&gt;. They might seem similar, but they’re actually two very different things.&lt;/p&gt;

&lt;p&gt;Let’s begin with output. Output is kind of easy to measure. You can say, "Yeah, during these first six months we’ve written 10 articles, we created that amount of LinkedIn posts, we had so many views, so many interactions." So you already have kind of a number you can put in an Excel sheet and compare. But is it actually relevant? That’s another interesting question.&lt;/p&gt;

&lt;p&gt;How much time are we spending creating the articles? Is that a measure to know the success of the project? Well, actually, no, it’s not. It is indeed something interesting to measure and keep track of, but the goal is not to focus on the output, as I said, but on the outcome.&lt;/p&gt;

&lt;p&gt;The idea is: okay, we wrote 10 articles, cool. What impact did those articles generate? That’s what matters. The most interesting metric we agreed on with management is lead generation. So it’s not how much time we spent or how much effort we put into creating the articles, but how many leads did the article generate? That’s a really relevant metric that measures if the project is actually working.&lt;/p&gt;

&lt;p&gt;However, this doesn’t mean at all that the output isn’t relevant. We still want to keep track of the effort, how much time we put into creating the content, and the output: how much content we can actually generate, how many articles. I keep saying articles, but it may be webinars, it may be talks we deliver at conferences, everything has to be measured and tracked. But that’s not how we measure the success of the project. The project is measured through the outcome.&lt;/p&gt;

&lt;p&gt;And even if we agree that outcome is what we want to measure to keep track of success, where should we put our thresholds? Like I said, lead generation, okay, awesome. How many leads do we want to take in order to consider the project successful? One? Five? Ten? And only leads? Do we want to keep track of actual customers? How many conversions do we have? Do we want 10%? 50%? 90%?&lt;/p&gt;

&lt;p&gt;That’s also another challenge that we’re facing right now, at least in this early phase, as we hardly have any other numbers or references to use. In our case, at least for our company, it’s the first time we’re trying to do something like this. So it’s even difficult to say, "We want three leads in the next two months." What does even three mean? Or five? Or ten or twenty?&lt;/p&gt;

&lt;p&gt;That’s also another really interesting topic we’re just exploring. As time goes by, we might want to set some thresholds and see, in six months, how far are we from the threshold? Did we do twice that number? Did we do half that number? And even at that point, we should be smart enough to say, "Okay, we did twice our goal, was it because we were so good and we did a lot of generation? Or did we do twice our goal because we were just kind of naive and picked the wrong number, a goal that was just too easy to reach?"&lt;/p&gt;

&lt;p&gt;Or, if we under-delivered, did we under-deliver because we didn’t create enough content? Because our content wasn’t good enough? Because it just didn’t work? Or simply because we were way too optimistic in the beginning and picked a number that was impossible to reach, and even if we did our best, we just didn’t get there?&lt;/p&gt;

&lt;p&gt;It’s going to be fun to do this kind of analysis in the next few months, and that’s obviously another topic I want to bring here to the channel. Maybe at the beginning of next year I’m going to say, "Hey, we had this metric, this is what we reached, and this is our analysis after all the work was done."&lt;/p&gt;

&lt;p&gt;Wrapping up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep track of the effort&lt;/strong&gt;, to know how much time you’re spending doing the activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep track of the output&lt;/strong&gt;, to measure your firepower, how many things you can actually do and deliver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep track of the outcome&lt;/strong&gt;, the results you can achieve. That is the category of metrics that will, in fact, measure the success of your program.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s pretty much how we think right now. I hope it’s the right direction. In any case, if you have any kind of advice about measuring developer relations activities, please let me know in the comments. As always, I’m happy to have a chat.&lt;/p&gt;

&lt;p&gt;That’s it. That was the end of today’s video. Thanks for watching, wish me luck again, and see you in the next update. See you!&lt;/p&gt;




&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>devrel</category>
      <category>career</category>
    </item>
    <item>
      <title>TanStack Router: How to protect routes with an authentication guard</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Sun, 22 Jun 2025 14:49:00 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/tanstack-router-how-to-protect-routes-with-an-authentication-guard-1laj</link>
      <guid>https://forem.com/playfulprogramming/tanstack-router-how-to-protect-routes-with-an-authentication-guard-1laj</guid>
      <description>&lt;p&gt;Some routes in your application require authentication or other conditions to be met before users can access them. TanStack Router provides a powerful guard mechanism to handle these scenarios through the &lt;code&gt;beforeLoad&lt;/code&gt; function, which allows you to intercept route navigation and enforce access control rules before any components are rendered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting a route
&lt;/h2&gt;

&lt;p&gt;When you define a route with &lt;code&gt;createFileRoute&lt;/code&gt; you have can specify a function in the &lt;code&gt;beforeLoad&lt;/code&gt; property. This function will be called before the route is loaded, allowing you to perform checks and potentially redirect the user if they don't meet the required conditions.&lt;/p&gt;

&lt;p&gt;An 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createFileRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&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;isAuthenticated&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;beforeLoad&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="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="nf"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;With this setup, the page will not be loaded at all if the user is not authenticated (e.g. the component will not be rendered). Instead, the user will be redirected to the home page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Protecting multiple routes
&lt;/h2&gt;

&lt;p&gt;With the previous example you're already protecting the &lt;code&gt;/profile&lt;/code&gt; route, and potentialy every other child route as each &lt;code&gt;beforeLoad&lt;/code&gt; function is called when traversing the route tree.&lt;/p&gt;

&lt;p&gt;However, in some cases you might not have a single route (and its children) to protect, but a series of sibling routes. While you can indeed replicate the same logic in each route's &lt;code&gt;beforeLoad&lt;/code&gt;, there are better ways to handle this.&lt;/p&gt;

&lt;p&gt;An approach is to follow the same pattern you would do to create shared layouts and add the guard logic in there. For example, your route tree could look like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;__root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Inside the &lt;code&gt;(authenticated)&lt;/code&gt; folder you can find three "regular" routes, but also a &lt;code&gt;route.tsx&lt;/code&gt; file, a special name that will not define a specific route but will be used as common layout for all the routes inside the folder. This is where you can add the guard logic:&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;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;beforeLoad&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="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="nf"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;
  
  
  Shared layouts - optional
&lt;/h3&gt;

&lt;p&gt;Notice how this route doesn't have a &lt;code&gt;component&lt;/code&gt; property. This is optional as we're using the route only to group the authentication logic (so we don't need it on each sibling route) and not to render a specific component.&lt;/p&gt;

&lt;p&gt;Once you have this setup though, this would be the first step to have a common layout (not part of this article) but all you really need is to define a &lt;code&gt;component&lt;/code&gt; and make sure to add the &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt; component inside it (imported from ), so the child routes can be rendered.&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;component&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      This text will show on all sibling and child routes
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Pathless route (folder)
&lt;/h3&gt;

&lt;p&gt;Also notice how the authenticated routes are grouped in a folder named &lt;code&gt;(authenticated)&lt;/code&gt; with its name enclosed in parentheses. This is a convention in TanStack Router to indicate that this folder will not appear in the URL, this is optional but often used so you can have &lt;code&gt;your-site-com/profile&lt;/code&gt; instead of &lt;code&gt;your-site-com/authenticated/profile&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  More on guards and authentication
&lt;/h2&gt;

&lt;p&gt;This was just a short extract with the core concepts, you can find an extensive and step by step guide on the following video.&lt;/p&gt;

&lt;p&gt;The video includes how to integrate with a real authentication service and how to use hooks through Router's context to access the authentication state in your routes and components.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/O6dS0_IvvK0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;



&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>react</category>
      <category>typescript</category>
      <category>tanstack</category>
    </item>
    <item>
      <title>I'm starting a Developer Relations program, somehow 🤷</title>
      <dc:creator>Leonardo Montini</dc:creator>
      <pubDate>Tue, 17 Jun 2025 12:49:06 +0000</pubDate>
      <link>https://forem.com/playfulprogramming/im-starting-a-developer-relations-program-somehow-4mhm</link>
      <guid>https://forem.com/playfulprogramming/im-starting-a-developer-relations-program-somehow-4mhm</guid>
      <description>&lt;p&gt;▶️ &lt;em&gt;Video version on YouTube: &lt;a href="https://youtu.be/ERWzk5iOAiU" rel="noopener noreferrer"&gt;https://youtu.be/ERWzk5iOAiU&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hey everybody, look at that! I finally had a chance of using the &lt;a href="https://www.linkedin.com/posts/leonardo-montini_i-played-around-with-this-idea-for-a-few-activity-7340291979529879552-Gk9f?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAB_ZOWoBOvhlbuFkrnqQEjGCAkyVBRwqKf8" rel="noopener noreferrer"&gt;LinkedIn template with the airplanes&lt;/a&gt;, and that's the announcement number one.&lt;/p&gt;

&lt;p&gt;But actually, the announcement number two is that I just added DevRel to my job title. So here's a story.&lt;/p&gt;

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

&lt;p&gt;If you follow me, you know that I kind of have a passion for sharing content and sharing knowledge. The idea was to match this passion to the other passion that I have, that is: getting a salary from a company I work for. How can I make these two different worlds live next to each other, or at least have some kind of intersection between my work as a front-end developer and the passion of sharing content?&lt;/p&gt;

&lt;p&gt;That's pretty much trying to launch a developer relations program right here at the company I'm currently working for.&lt;/p&gt;

&lt;p&gt;But here's the fun part: I have no idea how to actually run a proper developer relations program, as sharing videos on my personal channel on YouTube is kind of a different story. And it's also the first time for my company to have an official DevRel program. It's going to be fun.&lt;/p&gt;

&lt;p&gt;You may ask, why am I even sharing this here on my personal channels? Someone else might be in a similar position, working in a company that might benefit from a DevRel program, but they don't know how to start. And I also don't know how to start. That's awesome!&lt;/p&gt;

&lt;p&gt;The idea is to share here the experiments we try, the lessons we learn every time we fail at something, and every time we succeed at something.&lt;/p&gt;

&lt;p&gt;So if you're interested, if you're curious and want to follow my DevRel journey, I got this idea of sharing even more videos about that in the near future.&lt;/p&gt;

&lt;p&gt;This doesn't mean that I'm going to quit talking about JavaScript. That's still what pays most of my salary, and I will still be a developer for quite some time. But there it is, one more interest that I have, one more challenge that I'm going to face, and one more topic that I can bring here.&lt;/p&gt;

&lt;p&gt;My primary goal, or my personal and romantic way of seeing this kind of new experiment, is to try to contaminate my colleagues and get most of them into this passion for sharing knowledge.&lt;/p&gt;

&lt;p&gt;So how can we, the employees and the company, find a match and kind of win together? The idea is that everyone who's passionate about something can have the chance of sharing knowledge about that specific topic, and in return, the company gets some more visibility and can demonstrate the expertise.&lt;/p&gt;

&lt;p&gt;If in the company we have some employees that are super skilled and super specialized in some specific technologies, or on how to solve specific problems, it also kind of benefits the company in proving that they have that kind of expertise.&lt;/p&gt;

&lt;p&gt;Because here's one more fun, tiny challenge in my journey: my current company, &lt;a href="https://www.claranet.it/" rel="noopener noreferrer"&gt;Claranet Italy&lt;/a&gt;, is not a product company. We're a consultancy company. We sell our expertise in solving our customers' needs.&lt;/p&gt;

&lt;p&gt;But I still want to believe that everyone, the company and the employees, can benefit from kind of a DevRel program.&lt;/p&gt;

&lt;p&gt;All of that just to say that if you're curious, stay tuned, wish me luck, and hopefully see you soon in the next update about this new developer relations journey.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and see you soon. Bye!&lt;/p&gt;




&lt;p&gt;Thanks for reading this article, I hope you found it interesting!&lt;/p&gt;

&lt;p&gt;Let's connect more: &lt;a href="https://leonardomontini.dev/newsletter" rel="noopener noreferrer"&gt;https://leonardomontini.dev/newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️&lt;br&gt;
You can find it here:&lt;br&gt;
&lt;a href="https://www.youtube.com/c/@DevLeonardo?sub_confirmation=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FYouTube%3A%2520Dev%2520Leonardo-FF0000%3Fstyle%3Dfor-the-badge%26logo%3Dyoutube%26logoColor%3Dwhite" alt="YouTube"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to follow me to get notified when new articles are out ;)&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__879086"&gt;
    &lt;a href="/balastrong" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&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%2F879086%2Fc23e7353-0873-45cc-a4fb-9bce7de113d5.jpg" alt="balastrong image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/balastrong"&gt;Leonardo Montini&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/balastrong"&gt;Awarded GitHub Star since 2023 ⭐️ and Microsoft MVP since 2024 🔷 I talk about Open Source, GitHub, and Web Development. 
I also run a YouTube channel called DevLeonardo, see you there!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>devrel</category>
      <category>career</category>
    </item>
  </channel>
</rss>
