DEV Community: Dave Gray The latest articles on DEV Community by Dave Gray (@gitdagray). https://dev.to/gitdagray 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%2F1208763%2Ff28f3ec9-2223-4fbf-b8ae-ed2640c8cfd3.png DEV Community: Dave Gray https://dev.to/gitdagray en Bye Copilot - How to Create a Local AI Coding Assistant for Free Dave Gray Mon, 24 Jun 2024 00:00:00 +0000 https://dev.to/gitdagray/bye-copilot-how-to-create-a-local-ai-coding-assistant-for-free-4572 https://dev.to/gitdagray/bye-copilot-how-to-create-a-local-ai-coding-assistant-for-free-4572 <p><strong>TLDR:</strong> Create your own local AI Coding Assistant that integrates with VS Code.</p> <h2> AI Coding Assistants </h2> <p>I didn't jump onboard the AI Coding Assistant train at first. </p> <p>However, I now open up a chat with <a href="proxy.php?url=https://chatgpt.com/">ChatGPT</a> as often as I do MDN.</p> <p>I've found it's often a quicker reference for exactly what I need. </p> <p>I have considered paying the monthly fee for <a href="proxy.php?url=https://github.com/features/copilot">GitHub Copilot</a>, but what if you could use open source large language models (LLMs) to create your own AI Coding Assistant? </p> <p>Now you can.</p> <h2> Install Ollama </h2> <p>Start by going to <a href="proxy.php?url=https://ollama.com/">Ollama.com</a> and downloading the version for your operating system. </p> <p>Note: As I write this, the Windows version is still considered a "preview". After installing on Windows, I had to restart my computer for <code>ollama</code> to be available at the command line.</p> <p>Open a terminal window and type <code>ollama --help</code> to confirm you have Ollama installed and your computer can find it.</p> <h2> Pick an Open Source LLM </h2> <p>Visit the <a href="proxy.php?url=https://evalplus.github.io/leaderboard.html">EvalPlus Leaderboard</a> where the performance of many models are compared. </p> <p>There are currently a couple of options in the EvalPlus Top 5 to consider: <code>DeepSeek-Coder-v2</code> and <code>CodeQwen1.5</code>. </p> <p>Before choosing, go back to <a href="proxy.php?url=https://ollama.com/">Ollama.com</a> and search the models to look at their details. </p> <p>I personally decided to go with <a href="proxy.php?url=https://ollama.com/library/codeqwen">codeqwen</a>. It is a 4.2GB download and <code>deepseek-coder-v2</code> is 8.9GB.</p> <p>You can try out several LLMs if you want to.</p> <p>After choosing at least one, copy the <code>ollama</code> command and run it in your terminal window to download your LLM of choice. </p> <p>For example, the command on the <a href="proxy.php?url=https://ollama.com/library/codeqwen">codeqwen</a> page is <code>ollama run codeqwen</code>.</p> <h2> Add the Continue VS Code Extension </h2> <p>Open up VS Code and click the extensions menu icon. </p> <p>Search for <code>continue</code>. </p> <p>You should find <code>Continue</code> by <a href="proxy.php?url=https://www.continue.dev/">continue.dev</a>.</p> <p>Install the extension and you should find it by icon or name in the activity bar afterwards. </p> <p>Click it to open and you should see a splash screen. </p> <p>The screen will confirm Ollama is installed and provide other recommendations that you can ignore. </p> <p>Click the "Local" button and the "next / continue" button. </p> <p>You should now have a chat screen that opened over your filetree (if your filetree is on the left side).</p> <h2> Configuring Continue </h2> <p>At the bottom of the chat window, select <code>Ollama - codeqwen:latest</code> from the menu. </p> <p>Click the settings icon to open up the <code>config.json</code> file. </p> <p>Look for the <code>tabAutocompleteModel</code> setting. </p> <p>Change both the <code>title</code> and <code>model</code> values to <code>codeqwen</code>.</p> <p>You will also see a <code>custom commands</code> setting like this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight json"><code><span class="nl">"customCommands"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test"</span><span class="p">,</span><span class="w"> </span><span class="nl">"prompt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{{{ input }}}</span><span class="se">\n\n</span><span class="s2">Write a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated. Give the tests just as chat output, don't edit any file."</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Write unit tests for highlighted code"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="err">,</span><span class="w"> </span></code></pre> </div> <p>You can add your own custom commands here. In this example, if you highlight a function in your code and type <code>test</code> in the chat window, it will execute the prompt you see. </p> <p>Here's an example of a custom command I added:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight json"><code><span class="nl">"customCommands"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test"</span><span class="p">,</span><span class="w"> </span><span class="nl">"prompt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{{{ input }}}</span><span class="se">\n\n</span><span class="s2">Write a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated. Give the tests just as chat output, don't edit any file."</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Write unit tests for highlighted code"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"step"</span><span class="p">,</span><span class="w"> </span><span class="nl">"prompt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{{{ input }}}</span><span class="se">\n\n</span><span class="s2">Explain the selected code step by step."</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Code explanation"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="err">,</span><span class="w"> </span></code></pre> </div> <h2> Getting Started: </h2> <p>Click the help icon at the bottom right of the chat window. </p> <p>It provides a link to a tutorial, web resources and keyboard shortcuts. </p> <p>A couple of quick things to try: </p> <ul> <li><p>Select some existing code in your file, then press <code>Ctrl+L</code> to start a chat on the selected code. </p></li> <li><p>Select some existing code in your file, then type <code>test</code> in the chat window, and press enter to generate unit tests for that code.</p></li> <li><p>Create a new empty code file, press <code>Ctrl+i</code> and type instructions for code generation. Then watch your AI Code Assistant generate the code. </p></li> <li><p>Start typing code in a file and look for the autocompletion suggestions. Press tab to use them.</p></li> </ul> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> ai githubcopilot codingassistant codeassistant Next.js Server Actions with next-safe-action Dave Gray Tue, 18 Jun 2024 00:00:00 +0000 https://dev.to/gitdagray/nextjs-server-actions-with-next-safe-action-ggc https://dev.to/gitdagray/nextjs-server-actions-with-next-safe-action-ggc <p><strong>TLDR:</strong> Add type safe and validated server actions to your Next.js App Router project with next-safe-action.</p> <h2> Next.js Server Actions </h2> <p><a href="proxy.php?url=https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations">Server Actions</a> are asynchronous functions executed on the server in Next.js. </p> <p>They are defined with the <code>"use server"</code> directive and can be used <br> in both server and client components for handling form submissions and data mutations.</p> <p>Over the last year, I've seen them applied in a variety of ways, and I've used them in projects myself, too.</p> <p>Now, I have recently discovered the <a href="proxy.php?url=https://next-safe-action.dev/">next-safe-action</a> library, and I like the structure, ease-of-use, and extra features it provides. </p> <h2> An Example Server Action without next-safe-action </h2> <p>I think the best way to show why I like <a href="proxy.php?url=https://next-safe-action.dev/">next-safe-action</a> is to show how I implemented a Next.js server action without the library first. </p> <p>Afterwards, I will show the refactor with <a href="proxy.php?url=https://next-safe-action.dev/">next-safe-action</a>.</p> <p>Here's an example server action from a repository and tutorial I recently published on creating a <a href="proxy.php?url=https://youtu.be/WyL_Jc6_-sY">Next.js Modal Form with react-hook-form, ShadCN/ui, Server Actions and Zod validation</a>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// src/app/actions/actions.ts</span> <span class="dl">"</span><span class="s2">use server</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">UserSchema</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="kd">type</span> <span class="nb">ReturnType</span> <span class="o">=</span> <span class="p">{</span> <span class="na">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">errors</span><span class="p">?:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nx">unknown</span><span class="o">&gt;</span> <span class="p">}</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">saveUser</span><span class="p">(</span><span class="nx">user</span><span class="p">:</span> <span class="nx">User</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nb">ReturnType</span><span class="o">&gt;</span> <span class="p">{</span> <span class="c1">// Check valid login here</span> <span class="kd">const</span> <span class="nx">parsed</span> <span class="o">=</span> <span class="nx">UserSchema</span><span class="p">.</span><span class="nf">safeParse</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">parsed</span><span class="p">.</span><span class="nx">success</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Submission Failed</span><span class="dl">"</span><span class="p">,</span> <span class="na">errors</span><span class="p">:</span> <span class="nx">parsed</span><span class="p">.</span><span class="nx">error</span><span class="p">.</span><span class="nf">flatten</span><span class="p">().</span><span class="nx">fieldErrors</span> <span class="p">}</span> <span class="p">}</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`http://localhost:3500/users/</span><span class="p">${</span><span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PATCH</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">firstname</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">firstname</span><span class="p">,</span> <span class="na">lastname</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">lastname</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="p">})</span> <span class="p">})</span> <span class="k">return</span> <span class="p">{</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">User Updated! 🎉</span><span class="dl">"</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>You can see above that I was using a local <code>json-server</code> instance in the tutorial to update user data. </p> <p>Before the update occurs, the data is validated with <a href="proxy.php?url=https://zod.dev/">Zod</a>. </p> <p>If the validation fails, Zod validation errors are sent back to the client component with the ZodError type <code>flatten</code> method applied. </p> <p>Now let's compare to the refactored version using <a href="proxy.php?url=https://next-safe-action.dev/">next-safe-action</a>.</p> <h2> An Example Server Action with next-safe-action </h2> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// src/app/actions/actions.ts</span> <span class="dl">"</span><span class="s2">use server</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">UserSchema</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">actionClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/lib/safe-action</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">flattenValidationErrors</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next-safe-action</span><span class="dl">"</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">saveUserAction</span> <span class="o">=</span> <span class="nx">actionClient</span> <span class="p">.</span><span class="nf">schema</span><span class="p">(</span><span class="nx">UserSchema</span><span class="p">,</span> <span class="p">{</span> <span class="na">handleValidationErrorsShape</span><span class="p">:</span> <span class="p">(</span><span class="nx">ve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nf">flattenValidationErrors</span><span class="p">(</span><span class="nx">ve</span><span class="p">).</span><span class="nx">fieldErrors</span><span class="p">,</span> <span class="p">})</span> <span class="p">.</span><span class="nf">action</span><span class="p">(</span><span class="k">async </span><span class="p">({</span> <span class="na">parsedInput</span><span class="p">:</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">firstname</span><span class="p">,</span> <span class="nx">lastname</span><span class="p">,</span> <span class="nx">email</span> <span class="p">}</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Check valid login here</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`http://localhost:3500/users/</span><span class="p">${</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PATCH</span><span class="dl">'</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">firstname</span><span class="p">:</span> <span class="nx">firstname</span><span class="p">,</span> <span class="na">lastname</span><span class="p">:</span> <span class="nx">lastname</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="nx">email</span><span class="p">,</span> <span class="p">})</span> <span class="p">})</span> <span class="k">return</span> <span class="p">{</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">User Updated! 🎉</span><span class="dl">"</span> <span class="p">}</span> <span class="p">})</span> </code></pre> </div> <p>The file has shrunk down from 33 lines to 26 lines of code.</p> <p>Starting at the top you can see I still import the Zod UserSchema I have defined. The inferred User type is no longer imported. </p> <p>New imports include <code>actionClient</code> and <code>flattenValidationErrors</code>. </p> <p>Instead of <code>export async function</code>, I'm using <code>export const</code> and starting the definition of <code>saveUserAction</code> with the <code>actionClient</code>. </p> <p>I chain the <code>schema</code> method to the <code>actionClient</code> while passing in the <code>UserSchema</code>. I also set the <code>handleValidationErrorsShape</code> option to use the imported <code>flattenValidationErrors</code> method. This method is similar to the ZodError type method <code>flatten</code> that I used in the original function.</p> <p>Next, I chain the <code>action</code> method and call the async function inside of it. It supplies a <code>parsedInput</code> prop. I destructure the prop to get the input data sent to the server action. </p> <p>The remainder of the function remains unchanged. </p> <p>Note that in this refactored version I did not define a <code>ReturnType</code>. </p> <p>The return type is the result defined by the <a href="proxy.php?url=https://next-safe-action.dev/docs/execution/hooks/useaction#useaction-return-object">useAction hook return object</a>. I apply the <code>useAction</code> hook in the client component. </p> <p>While some overhead is saved in the server action code you see above, even more is saved in the client component. </p> <p>Below, I again show before and after code versions. This time the before and afters are of the client component using <a href="proxy.php?url=https://react-hook-form.com/">react-hook-form</a>.</p> <h2> An Example Client Component without next-safe-action </h2> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// src/app/edit/[id]/UserForm.tsx</span> <span class="dl">"</span><span class="s2">use client</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useForm</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react-hook-form</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Form</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/ui/form</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/ui/button</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">InputWithLabel</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/InputWithLabel</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">zodResolver</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hookform/resolvers/zod</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">UserSchema</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">saveUser</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/app/actions/actions</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useRouter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/navigation</span><span class="dl">"</span> <span class="kd">type</span> <span class="nx">Props</span> <span class="o">=</span> <span class="p">{</span> <span class="na">user</span><span class="p">:</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">UserForm</span><span class="p">({</span> <span class="nx">user</span> <span class="p">}:</span> <span class="nx">Props</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">message</span><span class="p">,</span> <span class="nx">setMessage</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="dl">''</span><span class="p">)</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">errors</span><span class="p">,</span> <span class="nx">setErrors</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">({})</span> <span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="nf">useRouter</span><span class="p">()</span> <span class="kd">const</span> <span class="nx">form</span> <span class="o">=</span> <span class="nx">useForm</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;</span><span class="p">({</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">onBlur</span><span class="dl">'</span><span class="p">,</span> <span class="na">resolver</span><span class="p">:</span> <span class="nf">zodResolver</span><span class="p">(</span><span class="nx">UserSchema</span><span class="p">),</span> <span class="na">defaultValues</span><span class="p">:</span> <span class="p">{</span> <span class="p">...</span><span class="nx">user</span> <span class="p">},</span> <span class="p">})</span> <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// boolean to indicate if form has not been saved</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nf">setItem</span><span class="p">(</span><span class="dl">"</span><span class="s2">userFormModified</span><span class="dl">"</span><span class="p">,</span> <span class="nx">form</span><span class="p">.</span><span class="nx">formState</span><span class="p">.</span><span class="nx">isDirty</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span> <span class="p">},</span> <span class="p">[</span><span class="nx">form</span><span class="p">.</span><span class="nx">formState</span><span class="p">.</span><span class="nx">isDirty</span><span class="p">])</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">onSubmit</span><span class="p">()</span> <span class="p">{</span> <span class="nf">setMessage</span><span class="p">(</span><span class="dl">''</span><span class="p">)</span> <span class="nf">setErrors</span><span class="p">({})</span> <span class="cm">/* No need to validate here because react-hook-form already validates with the Zod schema */</span> <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">saveUser</span><span class="p">(</span><span class="nx">form</span><span class="p">.</span><span class="nf">getValues</span><span class="p">())</span> <span class="k">if </span><span class="p">(</span><span class="nx">result</span><span class="p">?.</span><span class="nx">errors</span><span class="p">)</span> <span class="p">{</span> <span class="nf">setMessage</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span> <span class="nf">setErrors</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">errors</span><span class="p">)</span> <span class="k">return</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nf">setMessage</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span> <span class="c1">// update client-side cache</span> <span class="nx">router</span><span class="p">.</span><span class="nf">refresh</span><span class="p">()</span> <span class="c1">// reset dirty fields</span> <span class="nx">form</span><span class="p">.</span><span class="nf">reset</span><span class="p">(</span><span class="nx">form</span><span class="p">.</span><span class="nf">getValues</span><span class="p">())</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">message</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-2xl</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">message</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">}</span> <span class="p">{</span><span class="nx">errors</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mb-10 text-red-500</span><span class="dl">"</span><span class="o">&gt;</span> <span class="p">{</span><span class="nb">Object</span><span class="p">.</span><span class="nf">keys</span><span class="p">(</span><span class="nx">errors</span><span class="p">).</span><span class="nf">map</span><span class="p">(</span><span class="nx">key</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">key</span><span class="p">}</span><span class="o">&gt;</span><span class="p">{</span><span class="s2">`</span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">: </span><span class="p">${</span><span class="nx">errors</span><span class="p">[</span><span class="nx">key</span> <span class="k">as</span> <span class="kr">keyof</span> <span class="k">typeof</span> <span class="nx">errors</span><span class="p">]}</span><span class="s2">`</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">}</span> <span class="o">&lt;</span><span class="nx">Form</span> <span class="p">{...</span><span class="nx">form</span><span class="p">}</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">form</span> <span class="nx">onSubmit</span><span class="o">=</span><span class="p">{(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">e</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">()</span> <span class="nx">form</span><span class="p">.</span><span class="nf">handleSubmit</span><span class="p">(</span><span class="nx">onSubmit</span><span class="p">)();</span> <span class="p">}}</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex flex-col gap-4</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">InputWithLabel</span> <span class="nx">fieldTitle</span><span class="o">=</span><span class="dl">"</span><span class="s2">First Name</span><span class="dl">"</span> <span class="nx">nameInSchema</span><span class="o">=</span><span class="dl">"</span><span class="s2">firstname</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">InputWithLabel</span> <span class="nx">fieldTitle</span><span class="o">=</span><span class="dl">"</span><span class="s2">Last Name</span><span class="dl">"</span> <span class="nx">nameInSchema</span><span class="o">=</span><span class="dl">"</span><span class="s2">lastname</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">InputWithLabel</span> <span class="nx">fieldTitle</span><span class="o">=</span><span class="dl">"</span><span class="s2">Email</span><span class="dl">"</span> <span class="nx">nameInSchema</span><span class="o">=</span><span class="dl">"</span><span class="s2">email</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex gap-4</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">Button</span><span class="o">&gt;</span><span class="nx">Submit</span><span class="o">&lt;</span><span class="sr">/Button</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">Button</span> <span class="kd">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">button</span><span class="dl">"</span> <span class="nx">variant</span><span class="o">=</span><span class="dl">"</span><span class="s2">destructive</span><span class="dl">"</span> <span class="nx">onClick</span><span class="o">=</span><span class="p">{()</span> <span class="o">=&gt;</span> <span class="nx">form</span><span class="p">.</span><span class="nf">reset</span><span class="p">()}</span> <span class="o">&gt;</span><span class="nx">Reset</span><span class="o">&lt;</span><span class="sr">/Button</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/form</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/Form</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">}</span> </code></pre> </div> <p>In the above example, I had to set state for both the message and errors that the original server action could return. </p> <p>I also needed to consider that state in the onSubmit function. </p> <p>In the refactored version below, you can see how this is simplified.</p> <h2> An Example Client Component with next-safe-action </h2> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// src/app/edit/[id]/UserForm.tsx</span> <span class="dl">"</span><span class="s2">use client</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useForm</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react-hook-form</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Form</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/ui/form</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/ui/button</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">InputWithLabel</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/InputWithLabel</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">zodResolver</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hookform/resolvers/zod</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">UserSchema</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schemas/User</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">saveUserAction</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/app/actions/actions</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useRouter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/navigation</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useAction</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next-safe-action/hooks</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">DisplayServerActionResponse</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/components/DisplayServerActionResponse</span><span class="dl">"</span> <span class="kd">type</span> <span class="nx">Props</span> <span class="o">=</span> <span class="p">{</span> <span class="na">user</span><span class="p">:</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">UserForm</span><span class="p">({</span> <span class="nx">user</span> <span class="p">}:</span> <span class="nx">Props</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="nf">useRouter</span><span class="p">()</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">execute</span><span class="p">,</span> <span class="nx">result</span><span class="p">,</span> <span class="nx">isExecuting</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">useAction</span><span class="p">(</span><span class="nx">saveUserAction</span><span class="p">)</span> <span class="kd">const</span> <span class="nx">form</span> <span class="o">=</span> <span class="nx">useForm</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;</span><span class="p">({</span> <span class="na">resolver</span><span class="p">:</span> <span class="nf">zodResolver</span><span class="p">(</span><span class="nx">UserSchema</span><span class="p">),</span> <span class="na">defaultValues</span><span class="p">:</span> <span class="p">{</span> <span class="p">...</span><span class="nx">user</span> <span class="p">},</span> <span class="p">})</span> <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// boolean to indicate if form has not been saved</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nf">setItem</span><span class="p">(</span><span class="dl">"</span><span class="s2">userFormModified</span><span class="dl">"</span><span class="p">,</span> <span class="nx">form</span><span class="p">.</span><span class="nx">formState</span><span class="p">.</span><span class="nx">isDirty</span><span class="p">.</span><span class="nf">toString</span><span class="p">())</span> <span class="p">},</span> <span class="p">[</span><span class="nx">form</span><span class="p">.</span><span class="nx">formState</span><span class="p">.</span><span class="nx">isDirty</span><span class="p">])</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">onSubmit</span><span class="p">()</span> <span class="p">{</span> <span class="cm">/* No need to validate here because react-hook-form already validates with the Zod schema */</span> <span class="nf">execute</span><span class="p">(</span><span class="nx">form</span><span class="p">.</span><span class="nf">getValues</span><span class="p">())</span> <span class="c1">// update client-side cache</span> <span class="nx">router</span><span class="p">.</span><span class="nf">refresh</span><span class="p">()</span> <span class="c1">// reset dirty fields</span> <span class="nx">form</span><span class="p">.</span><span class="nf">reset</span><span class="p">(</span><span class="nx">form</span><span class="p">.</span><span class="nf">getValues</span><span class="p">())</span> <span class="p">}</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">DisplayServerActionResponse</span> <span class="nx">result</span><span class="o">=</span><span class="p">{</span><span class="nx">result</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">Form</span> <span class="p">{...</span><span class="nx">form</span><span class="p">}</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">form</span> <span class="nx">onSubmit</span><span class="o">=</span><span class="p">{(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">e</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">()</span> <span class="nx">form</span><span class="p">.</span><span class="nf">handleSubmit</span><span class="p">(</span><span class="nx">onSubmit</span><span class="p">)();</span> <span class="p">}}</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex flex-col gap-4</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">InputWithLabel</span> <span class="nx">fieldTitle</span><span class="o">=</span><span class="dl">"</span><span class="s2">First Name</span><span class="dl">"</span> <span class="nx">nameInSchema</span><span class="o">=</span><span class="dl">"</span><span class="s2">firstname</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">InputWithLabel</span> <span class="nx">fieldTitle</span><span class="o">=</span><span class="dl">"</span><span class="s2">Last Name</span><span class="dl">"</span> <span class="nx">nameInSchema</span><span class="o">=</span><span class="dl">"</span><span class="s2">lastname</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">InputWithLabel</span> <span class="nx">fieldTitle</span><span class="o">=</span><span class="dl">"</span><span class="s2">Email</span><span class="dl">"</span> <span class="nx">nameInSchema</span><span class="o">=</span><span class="dl">"</span><span class="s2">email</span><span class="dl">"</span> <span class="o">/&gt;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex gap-4</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">Button</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">isExecuting</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">Working...</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">Submit</span><span class="dl">"</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/Button</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">Button</span> <span class="kd">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">button</span><span class="dl">"</span> <span class="nx">variant</span><span class="o">=</span><span class="dl">"</span><span class="s2">destructive</span><span class="dl">"</span> <span class="nx">onClick</span><span class="o">=</span><span class="p">{()</span> <span class="o">=&gt;</span> <span class="nx">form</span><span class="p">.</span><span class="nf">reset</span><span class="p">()}</span> <span class="o">&gt;</span><span class="nx">Reset</span><span class="o">&lt;</span><span class="sr">/Button</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/form</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/Form</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">}</span> </code></pre> </div> <p>In this refactored version, I imported the <a href="proxy.php?url=https://next-safe-action.dev/docs/execution/hooks/useaction">useAction</a> hook supplied by next-safe-action and a custom component I created called <code>DisplayServerActionResponse</code>.</p> <p>I eliminated all usage of <code>useState</code>.</p> <p><code>DisplayServerActionResponse</code> receives the <code>result</code> that is provided by the useAction hook. It holds the data sent back from the server action. </p> <p><code>useAction</code> also provides an <code>execute</code> function and an <code>isExecuting</code> boolean. (Check the <a href="proxy.php?url=https://next-safe-action.dev/docs/introduction">docs</a> for what else it can provide, too.)</p> <p>All of this greatly reduces the logic I needed to put in the <code>onSubmit</code> function. </p> <p>Receiving the <code>result</code> from the server action makes it easy to abstract the displayed response to the custom <code>DisplayServerActionResponse</code> component, too. </p> <p>Here's a quick look at that component as well..</p> <h2> Displaying the Server Action Result </h2> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Props</span> <span class="o">=</span> <span class="p">{</span> <span class="na">result</span><span class="p">:</span> <span class="p">{</span> <span class="nx">data</span><span class="p">?:</span> <span class="p">{</span> <span class="nx">message</span><span class="p">?:</span> <span class="kr">string</span><span class="p">,</span> <span class="p">},</span> <span class="nx">serverError</span><span class="p">?:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">fetchError</span><span class="p">?:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">validationErrors</span><span class="p">?:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="p">[]</span> <span class="o">|</span> <span class="kc">undefined</span><span class="o">&gt;</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">,</span> <span class="p">}</span> <span class="p">}</span> <span class="k">export</span> <span class="kd">function</span> <span class="nf">DisplayServerActionResponse</span><span class="p">({</span> <span class="nx">result</span> <span class="p">}:</span> <span class="nx">Props</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">serverError</span><span class="p">,</span> <span class="nx">fetchError</span><span class="p">,</span> <span class="nx">validationErrors</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">result</span> <span class="k">return </span><span class="p">(</span> <span class="o">&lt;&gt;</span> <span class="p">{</span><span class="cm">/* Success Message */</span><span class="p">}</span> <span class="p">{</span><span class="nx">data</span><span class="p">?.</span><span class="nx">message</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">h2</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-2xl my-2</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/h2</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">}</span> <span class="p">{</span><span class="nx">serverError</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">my-2 text-red-500</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">serverError</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">}</span> <span class="p">{</span><span class="nx">fetchError</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">my-2 text-red-500</span><span class="dl">"</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">fetchError</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">}</span> <span class="p">{</span><span class="nx">validationErrors</span> <span class="p">?</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">my-2 text-red-500</span><span class="dl">"</span><span class="o">&gt;</span> <span class="p">{</span><span class="nb">Object</span><span class="p">.</span><span class="nf">keys</span><span class="p">(</span><span class="nx">validationErrors</span><span class="p">).</span><span class="nf">map</span><span class="p">(</span><span class="nx">key</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">key</span><span class="p">}</span><span class="o">&gt;</span><span class="p">{</span><span class="s2">`</span><span class="p">${</span><span class="nx">key</span><span class="p">}</span><span class="s2">: </span><span class="p">${</span><span class="nx">validationErrors</span> <span class="o">&amp;&amp;</span> <span class="nx">validationErrors</span><span class="p">[</span><span class="nx">key</span> <span class="k">as</span> <span class="kr">keyof</span> <span class="k">typeof</span> <span class="nx">validationErrors</span><span class="p">]}</span><span class="s2">`</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">:</span> <span class="kc">null</span><span class="p">}</span> <span class="o">&lt;</span><span class="sr">/</span><span class="err">&gt; </span> <span class="p">)</span> <span class="p">}</span> </code></pre> </div> <p>Above, you can see that <a href="proxy.php?url=https://next-safe-action.dev/">next-safe-action</a> provides not only validation errors from the Zod schema I constructed, but it also provides server errors and fetch errors. </p> <p>In addition, the result object contains the success message I provided from the server action. </p> <h2> Learn More </h2> <p>This is just one example and a simple one at that! Dive into the docs and solve your own specific use case to see what else <a href="proxy.php?url=https://next-safe-action.dev/">next-safe-action</a> is capable of. </p> <p>I plan to refactor my old server actions and use next-safe-action going forward. </p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> nextjs typesafety validation serveractions How to Create Excel Spreadsheets with Styling Options Using JavaScript Dave Gray Mon, 17 Jun 2024 00:00:00 +0000 https://dev.to/gitdagray/how-to-create-excel-spreadsheets-with-styling-options-using-javascript-54o2 https://dev.to/gitdagray/how-to-create-excel-spreadsheets-with-styling-options-using-javascript-54o2 <p><strong>TLDR:</strong> An Open Source fork of SheetJS lets you create and style Excel spreadsheets with JavaScript.</p> <h2> Creating XLSX Files </h2> <p>An XLSX file is a Microsoft Excel spreadsheet. </p> <p>I previously documented <a href="proxy.php?url=https://www.davegray.codes/posts/how-to-download-xlsx-files-from-a-nextjs-route-handler">How to Download xlsx Files from a Next.js Route Handler</a>. </p> <p>In that blog post, I used the <a href="proxy.php?url=https://docs.sheetjs.com/docs/getting-started/installation/frameworks">xlsx</a> package which is also known as SheetJS. </p> <p>However, I discovered a limitation of the community edition of the xlsx package: It did not allow the row and cell styling that my stakeholders desired in their final product. </p> <p>The PRO edition does allow styling, but I looked for an open source solution and found it.</p> <h2> xlsx-js-style </h2> <p><a href="proxy.php?url=https://www.npmjs.com/package/xlsx-js-style">xlsx-js-style</a> is a fork of SheetJS combined with code from a couple of other open source projects that were adding styles to SheetJS. </p> <p>The only drawback I see is the last version was published 2 years ago, but it does not have the vulnerability I warned about if you install the <a href="proxy.php?url=https://www.davegray.codes/posts/how-to-download-xlsx-files-from-a-nextjs-route-handler#xlsx-dependency">xlsx dependency</a> directly from npm.</p> <p><a href="proxy.php?url=https://www.npmjs.com/package/xlsx-js-style">xlsx-js-style</a> allows you to create Excel spreadsheets with JavaScript and style the cells with borders, colors, alignment, and font styles. </p> <h2> Add xlsx-js-style to Your Project </h2> <p>Install xlsx:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npm i xlsx-js-style </code></pre> </div> <p>Next, import xlsx-js-style into your project:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="nx">XLSX</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">xlsx-js-style</span><span class="dl">"</span> </code></pre> </div> <p>You can use this package in any JavaScript or TypeScript project.</p> <h2> Creating and Styling the XLSX Worksheet </h2> <p>Here's an example of how to use the xlsx-js-style package:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// define your headers </span> <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="p">[</span> <span class="dl">"</span><span class="s2">FirstName</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">LastName</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Email</span><span class="dl">"</span><span class="p">,</span> <span class="p">]</span> <span class="c1">// set column widths</span> <span class="kd">const</span> <span class="nx">colWidths</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="na">wch</span><span class="p">:</span> <span class="mi">30</span> <span class="p">},</span> <span class="p">{</span> <span class="na">wch</span><span class="p">:</span> <span class="mi">30</span> <span class="p">},</span> <span class="p">{</span> <span class="na">wch</span><span class="p">:</span> <span class="mi">50</span> <span class="p">},</span> <span class="p">]</span> <span class="c1">// get the data </span> <span class="kd">const</span> <span class="nx">userData</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getUserData</span><span class="p">()</span> <span class="c1">// early return if no data</span> <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">userData</span> <span class="o">||</span> <span class="o">!</span><span class="nx">userData</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">null</span> <span class="p">}</span> <span class="c1">// set header row height </span> <span class="c1">// consider if you have vertical headers</span> <span class="kd">const</span> <span class="nx">headerRowHeight</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="na">hpt</span><span class="p">:</span> <span class="mi">80</span> <span class="p">},</span> <span class="p">]</span> <span class="c1">// Dynamically set row height based on size of data</span> <span class="kd">const</span> <span class="nx">dataRowHeight</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">({</span> <span class="na">length</span><span class="p">:</span> <span class="nx">userData</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">length</span> <span class="p">},</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">hpt</span><span class="p">:</span> <span class="mi">30</span> <span class="p">}))</span> <span class="c1">// Combine header row height and data row height</span> <span class="kd">const</span> <span class="nx">rowHeight</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">headerRowHeight</span><span class="p">,</span> <span class="p">...</span><span class="nx">dataRowHeight</span><span class="p">]</span> <span class="c1">// Create a new worksheet: </span> <span class="kd">const</span> <span class="nx">worksheet</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">json_to_sheet</span><span class="p">([])</span> <span class="c1">// Assign widths to columns</span> <span class="nx">worksheet</span><span class="p">[</span><span class="dl">'</span><span class="s1">!cols</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="nx">colWidths</span> <span class="c1">// Assign height to rows</span> <span class="nx">worksheet</span><span class="p">[</span><span class="dl">'</span><span class="s1">!rows</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="nx">rowHeight</span> <span class="c1">// Enable auto-filter for columns</span> <span class="nx">worksheet</span><span class="p">[</span><span class="dl">'</span><span class="s1">!autofilter</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="na">ref</span><span class="p">:</span> <span class="dl">"</span><span class="s2">A1:C1</span><span class="dl">"</span> <span class="p">}</span> <span class="c1">// Add the headers to the worksheet: </span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">sheet_add_aoa</span><span class="p">(</span><span class="nx">worksheet</span><span class="p">,</span> <span class="p">[</span><span class="nx">headers</span><span class="p">])</span> <span class="c1">// add data to sheet </span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">sheet_add_json</span><span class="p">(</span><span class="nx">worksheet</span><span class="p">,</span> <span class="nx">userData</span><span class="p">,</span> <span class="p">{</span> <span class="na">skipHeader</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">origin</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">})</span> <span class="c1">// get size of sheet </span> <span class="kd">const</span> <span class="nx">range</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">decode_range</span><span class="p">(</span><span class="nx">worksheet</span><span class="p">[</span><span class="dl">"</span><span class="s2">!ref</span><span class="dl">"</span><span class="p">]</span> <span class="o">??</span> <span class="dl">""</span><span class="p">)</span> <span class="kd">const</span> <span class="nx">rowCount</span> <span class="o">=</span> <span class="nx">range</span><span class="p">.</span><span class="nx">e</span><span class="p">.</span><span class="nx">r</span> <span class="kd">const</span> <span class="nx">columnCount</span> <span class="o">=</span> <span class="nx">range</span><span class="p">.</span><span class="nx">e</span><span class="p">.</span><span class="nx">c</span> <span class="c1">// Add formatting by looping through data in sheet </span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">row</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">row</span> <span class="o">&lt;=</span> <span class="nx">rowCount</span><span class="p">;</span> <span class="nx">row</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">col</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">col</span> <span class="o">&lt;=</span> <span class="nx">columnCount</span><span class="p">;</span> <span class="nx">col</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">cellRef</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">encode_cell</span><span class="p">({</span> <span class="na">r</span><span class="p">:</span> <span class="nx">row</span><span class="p">,</span> <span class="na">c</span><span class="p">:</span> <span class="nx">col</span> <span class="p">})</span> <span class="c1">// Add this format to every cell</span> <span class="nx">worksheet</span><span class="p">[</span><span class="nx">cellRef</span><span class="p">].</span><span class="nx">s</span> <span class="o">=</span> <span class="p">{</span> <span class="na">alignment</span><span class="p">:</span> <span class="p">{</span> <span class="na">horizontal</span><span class="p">:</span> <span class="dl">"</span><span class="s2">left</span><span class="dl">"</span><span class="p">,</span> <span class="na">wrapText</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">},</span> <span class="p">}</span> <span class="c1">// vertical header - 1st column only</span> <span class="k">if </span><span class="p">(</span><span class="nx">row</span> <span class="o">===</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">col</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="nx">worksheet</span><span class="p">[</span><span class="nx">cellRef</span><span class="p">].</span><span class="nx">s</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">//spreads in previous cell settings</span> <span class="p">...</span><span class="nx">worksheet</span><span class="p">[</span><span class="nx">cellRef</span><span class="p">].</span><span class="nx">s</span><span class="p">,</span> <span class="na">alignment</span><span class="p">:</span> <span class="p">{</span> <span class="na">horizontal</span><span class="p">:</span> <span class="dl">"</span><span class="s2">center</span><span class="dl">"</span><span class="p">,</span> <span class="na">vertical</span><span class="p">:</span> <span class="dl">"</span><span class="s2">center</span><span class="dl">"</span><span class="p">,</span> <span class="na">wrapText</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">textRotation</span><span class="p">:</span> <span class="mi">180</span><span class="p">,</span> <span class="p">},</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// Format headers bold</span> <span class="k">if </span><span class="p">(</span><span class="nx">row</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="nx">worksheet</span><span class="p">[</span><span class="nx">cellRef</span><span class="p">].</span><span class="nx">s</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">//spreads in previous cell settings</span> <span class="p">...</span><span class="nx">worksheet</span><span class="p">[</span><span class="nx">cellRef</span><span class="p">].</span><span class="nx">s</span><span class="p">,</span> <span class="na">font</span><span class="p">:</span> <span class="p">{</span> <span class="na">bold</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">worksheet</span> <span class="c1">// After this, add worksheet to workbook, download, etc. </span> <span class="c1">// See docs and my previous article</span> </code></pre> </div> <h2> Other Styles Available </h2> <p>I only applied a few of the styles available in the above example. </p> <p>Checkout all of the available style settings on the <a href="proxy.php?url=https://www.npmjs.com/package/xlsx-js-style">npm page for xlsx-js-style</a>.</p> <h2> Learning More: </h2> <ul> <li><p>Read my previous article on <a href="proxy.php?url=https://www.davegray.codes/posts/how-to-download-xlsx-files-from-a-nextjs-route-handler">How to Download xlsx Files</a> if you want to create these files in a Node.js backend and request them from your frontend. My example is with Next.js, but you could just set it up in Node.js without Next.js. </p></li> <li><p>If you read the previous article or watch the video below, remember to replace the <code>xlsx</code> dependency with the <code>xlsx-js-style</code> dependency I discussed in this article. </p></li> </ul> <p>Enjoy creating Excel spreadsheets with JavaScript!</p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> javascript excel xlsx FIX: Git Bash is Slow and has Strange Random Characters in VS Code Dave Gray Fri, 14 Jun 2024 00:00:00 +0000 https://dev.to/gitdagray/fix-git-bash-is-slow-and-has-strange-random-characters-in-vs-code-41fl https://dev.to/gitdagray/fix-git-bash-is-slow-and-has-strange-random-characters-in-vs-code-41fl <p><strong>TLDR:</strong> If you're using git bash in VS Code and notice it slows down and/or starts printing random characters, here's how to fix it.</p> <h2> The Problem </h2> <p>After a recent VS Code update, I noticed git bash really slowed down. </p> <p>Sometimes it even output random weird characters like the ones you see in my terminal window below.</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--mVY3Tqpb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/git-bash-vscode-issue-1200x675.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--mVY3Tqpb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/git-bash-vscode-issue-1200x675.png" alt="git bash with strange characters" width="800" height="450"></a></p> <h2> How to Fix It (for now) </h2> <p>Press <code>Ctrl+,</code> or click the cog icon in the bottom left of VS Code to open Settings. </p> <p>Use the search bar and search for <code>terminal integrated shell</code>. </p> <p>The setting for <code>Terminal &gt; Integrated &gt; Shell Integration &gt; Enabled</code> should show up. </p> <p>Uncheck the box for this setting. </p> <p>If you don't find that setting, you can open the JSON settings file and add this line: <br> <code>"terminal.integrated.shellIntegration.enabled": false,</code></p> <h2> Check Back </h2> <p>This seems to be a bug after a recent VS Code update. I don't think this setting provides anything I will miss, but if you do, you may want to check back and re-enable this in the future. </p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> gitbash vscode How to Download xlsx Files from a Next.js Route Handler Dave Gray Sun, 18 Feb 2024 00:00:00 +0000 https://dev.to/gitdagray/how-to-download-xlsx-files-from-a-nextjs-route-handler-1m2l https://dev.to/gitdagray/how-to-download-xlsx-files-from-a-nextjs-route-handler-1m2l <p><strong>TLDR:</strong> You can set up a Next.js Route Handler that creates and downloads Microsoft Excel (xlsx) files. </p> <h2> XLSX Files </h2> <p>No matter where you work as a developer, there's a good chance someone will ask you to send them an MS Excel spreadsheet sooner or later. Those files end with the extension xlsx or xls. </p> <p>At my job, I manage a large data project and regularly receive requests for table exports as spreadsheets. </p> <p>I decided to set up an API endpoint via Next.js route handler which will allow my boss and co-workers to create and download their own table exports on demand. </p> <h2> xlsx dependency </h2> <p>The <a href="proxy.php?url=https://docs.sheetjs.com/docs/getting-started/installation/frameworks">xlsx</a> package is also known as SheetJS. </p> <p>Install xlsx:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npm i <span class="nt">--save</span> https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz </code></pre> </div> <p><strong>Note:</strong> Do NOT get the xlsx package from the npm registry with <code>npm install xlsx</code>. Sheetjs has stopped using the public registry at version 18.5. You will see the version in the public registry is 2+ years old and has a high severity vulnerability now. You can confirm this vulnerability on the <a href="proxy.php?url=https://socket.dev/npm/package/xlsx">Socket.dev xlsx page</a>.</p> <h2> Add xlsx to Your Project </h2> <p>Next, import xlsx into your project:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">XLSX</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">xlsx</span><span class="dl">'</span> </code></pre> </div> <p>My Next.js route handler starts by receiving a parameter with the requested table name. I'm also including some pseudo-code comments to allow you to follow the logic process until I get to the xlsx details.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="cm">/* example path: /api/tables/[table] */</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">GET</span><span class="p">(</span> <span class="nx">request</span><span class="p">:</span> <span class="nx">NextRequest</span><span class="p">,</span> <span class="p">{</span> <span class="nx">params</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">table</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">}</span> <span class="p">)</span> <span class="p">{</span> <span class="c1">// check for authorized user first! </span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">table</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">params</span> <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">table</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Table name required</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// check table name with list of table names here</span> <span class="c1">// if table doesn't exist, throw an error </span> <span class="c1">// Query: SELECT * FROM table and get a JSON response </span> <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="k">if </span><span class="p">(</span><span class="nx">e</span> <span class="k">instanceof</span> <span class="nb">Error</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">Response</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">400</span><span class="p">,</span> <span class="p">})</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <h2> Creating the XLSX File </h2> <p>Here's the good stuff using the xlsx package:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// ...previous code </span> <span class="c1">// Query: SELECT * FROM your table and get a JSON response</span> <span class="c1">// Create a new XLSX workbook:</span> <span class="kd">const</span> <span class="nx">workbook</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">book_new</span><span class="p">()</span> <span class="c1">// Create a new worksheet: </span> <span class="kd">const</span> <span class="nx">worksheet</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">json_to_sheet</span><span class="p">(</span><span class="nx">jsonTableData</span><span class="p">)</span> <span class="c1">// Append the worksheet to the workbook: </span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">book_append_sheet</span><span class="p">(</span><span class="nx">workbook</span><span class="p">,</span> <span class="nx">worksheet</span><span class="p">,</span> <span class="dl">"</span><span class="s2">MySheet</span><span class="dl">"</span><span class="p">)</span> <span class="c1">// Create data buffer </span> <span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="nx">workbook</span><span class="p">,</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">buffer</span><span class="dl">"</span><span class="p">,</span> <span class="na">bookType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">xlsx</span><span class="dl">"</span> <span class="p">})</span> <span class="c1">// Create and send a new Response</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">Response</span><span class="p">(</span><span class="nx">buffer</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">Content-Disposition</span><span class="dl">'</span><span class="p">:</span> <span class="s2">`attachment; filename="</span><span class="p">${</span><span class="nx">table</span><span class="p">}</span><span class="s2">.xlsx"`</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/vnd.ms-excel</span><span class="dl">'</span><span class="p">,</span> <span class="p">}</span> <span class="p">})</span> <span class="c1">// } catch (e) { and rest of code...</span> </code></pre> </div> <p>You can find a similar example for a Node.js &amp; Express server in the <a href="proxy.php?url=https://docs.sheetjs.com/docs/solutions/output#example-server-responses">SheetJS docs</a>. </p> <p>The key to success here is creating the buffer with the <code>XLSX.write</code> method, and then sending it in the Response with the proper headers. </p> <h2> Other File Types </h2> <p>Do you want to download CSV (comma-separated) or TSV (tab-separated) files? </p> <p>Or maybe just display an HTML version? </p> <p>No problem! </p> <p>You can find all of the appropriate XLSX methods in the <a href="proxy.php?url=https://docs.sheetjs.com/">SheetJS docs</a>, but here is how I provide CSV downloads, too. </p> <p>I'm looking for a <code>format</code> parameter. If it equals <code>csv</code>, then I'm sending that file type instead.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="cm">/* example path: /api/tables/[table]?format=csv */</span> <span class="c1">// begin route handler code above </span> <span class="c1">// put this somewhere before the XLSX creation and response </span> <span class="kd">const</span> <span class="nx">searchParams</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">nextUrl</span><span class="p">.</span><span class="nx">searchParams</span> <span class="kd">const</span> <span class="nx">format</span> <span class="o">=</span> <span class="nx">searchParams</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">format</span><span class="dl">'</span><span class="p">)</span> <span class="k">if </span><span class="p">(</span><span class="nx">format</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">csv</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">csv</span> <span class="o">=</span> <span class="nx">XLSX</span><span class="p">.</span><span class="nx">utils</span><span class="p">.</span><span class="nf">sheet_to_csv</span><span class="p">(</span><span class="nx">worksheet</span><span class="p">,</span> <span class="p">{</span> <span class="na">forceQuotes</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">})</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">Response</span><span class="p">(</span><span class="nx">csv</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">Content-Disposition</span><span class="dl">'</span><span class="p">:</span> <span class="s2">`attachment; filename="</span><span class="p">${</span><span class="nx">tableName</span><span class="p">}</span><span class="s2">.csv"`</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/csv</span><span class="dl">'</span><span class="p">,</span> <span class="p">}</span> <span class="p">})</span> <span class="p">}</span> </code></pre> </div> <h2> Final Notes: </h2> <ul> <li><p>Always check for an authorized user. </p></li> <li><p>Don't allow SQL injections. I'm verifying the <code>table</code> parameter with a list of accurate table names. Only those specific values are allowed. </p></li> </ul> <p>Enjoy creating downloads!</p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> nextjs react excel xlsx How to get an Accurate Column Item Count in React Table Dave Gray Fri, 26 Jan 2024 00:00:00 +0000 https://dev.to/gitdagray/how-to-get-an-accurate-column-item-count-in-react-table-1hpl https://dev.to/gitdagray/how-to-get-an-accurate-column-item-count-in-react-table-1hpl <p><strong>TLDR:</strong> If you have null field data, you'll need to take an extra step to get an accurate column count in React Table.</p> <h2> React Table </h2> <p>I've been working with <a href="proxy.php?url=https://tanstack.com/table/v8/docs/introduction">React Table v8</a>, and it is very good! </p> <p>It has allowed me to quickly create a very useful table for a large amount of data. </p> <p>The only problem with this data: It has a lot of null values saved over approximately 15 years. </p> <p>I implemented column filters like you see in <a href="proxy.php?url=https://tanstack.com/table/v8/docs/framework/react/examples/filters">this React Table example</a>, but <em>I noticed the item count shown in the placeholder text was off</em>.</p> <p>The code in the docs example giving the column item count for the filter input placeholder:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="o">&lt;</span><span class="nx">DebouncedInput</span> <span class="c1">// other attributes here</span> <span class="nx">placeholder</span><span class="o">=</span><span class="p">{</span><span class="s2">`Search... (</span><span class="p">${</span><span class="nx">column</span><span class="p">.</span><span class="nf">getFacetedUniqueValues</span><span class="p">().</span><span class="nx">size</span><span class="p">}</span><span class="s2">)`</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span></code></pre> </div> <p><a href="proxy.php?url=https://tanstack.com/table/v8/docs/api/features/filters#getfaceteduniquevalues"><code>getFacetedUniqueValues</code></a> returns a <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">Map</a>. </p> <p>The number of items in a Map is retrieved from its <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size">size</a> property.</p> <h2> The Column Item Count Fix </h2> <p>The above works great if your data has no null values OR you want to include those null values in your count. </p> <p>In my implementation, I did not want to count those values. </p> <p>To avoid including them in the item count, I switched from the above placeholder to what you see here:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="o">&lt;</span><span class="nx">DebouncedInput</span> <span class="c1">// other attributes here</span> <span class="nx">placeholder</span><span class="o">=</span><span class="p">{</span><span class="s2">`Search... (</span><span class="p">${[...</span><span class="nx">column</span><span class="p">.</span><span class="nf">getFacetedUniqueValues</span><span class="p">()].</span><span class="nf">filter</span><span class="p">(</span><span class="nx">arr</span> <span class="o">=&gt;</span> <span class="nx">arr</span><span class="p">[</span><span class="mi">0</span><span class="p">]).</span><span class="nx">length</span><span class="p">}</span><span class="s2">)`</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span></code></pre> </div> <p>Spreading the Map returned from <code>column.getFacetedUniqueValues()</code> into a new array provides me with an array of arrays. </p> <p>I filter that array to eliminate any null, empty or false values and finally, get the length value of the array. </p> <p>You would want to handle this differently if you were filtering boolean or numerical data. </p> <p>In this example, I'm filtering a text field and only looking for a truthy return in the filter method. </p> <p>Now I have an accurate column item count that does not include any null or empty values. </p> <h2> Filtering by Null Values </h2> <p>In the end, my stakeholders wanted to be able to filter by the null values in their data. </p> <p>To do this, you need to replace the null data in the React Table with something else. </p> <p>You can do this in the <a href="proxy.php?url=https://tanstack.com/table/v8/docs/guide/column-defs#accessor-functions">accessor function</a> as you create your columns.</p> <p>Here is the part to add in your accessor function:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">if </span><span class="p">(</span><span class="nx">row</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span> <span class="dl">'</span><span class="s1">✖</span><span class="dl">'</span> </code></pre> </div> <p>This allows the table filter to use the ✖ value. </p> <p>Note: the ✖ will show in the table instead of an empty cell, too. </p> <p>This is what my stakeholders wanted. Your mileage may vary. </p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> react reacttable How to Write a SQL Subquery with Drizzle ORM Dave Gray Sun, 14 Jan 2024 00:00:00 +0000 https://dev.to/gitdagray/how-to-write-a-sql-subquery-with-drizzle-orm-44ck https://dev.to/gitdagray/how-to-write-a-sql-subquery-with-drizzle-orm-44ck <p>I need to write a SQL select query that joins a couple of tables. </p> <p>No problem. </p> <p>However, I also need to join that query with the results of <em>another query</em>. </p> <p>This is possible in SQL with a <code>subquery</code>. </p> <p>I am using <a href="proxy.php?url=https://orm.drizzle.team/">Drizzle ORM</a> to write type-safe queries instead of just raw SQL statements. </p> <p>I create the main query BEFORE adding in the subquery:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">db</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/index</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">plant</span><span class="p">,</span> <span class="nx">genus</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schema</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">eq</span><span class="p">,</span> <span class="nx">or</span><span class="p">,</span> <span class="nx">like</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">drizzle-orm</span><span class="dl">"</span> <span class="kd">const</span> <span class="nx">plantData</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nf">select</span><span class="p">({</span> <span class="na">ID</span><span class="p">:</span> <span class="nx">plant</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">,</span> <span class="na">Genus</span><span class="p">:</span> <span class="nx">genus</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span> <span class="p">}).</span><span class="k">from</span><span class="p">(</span><span class="nx">plant</span><span class="p">)</span> <span class="p">.</span><span class="nf">leftJoin</span><span class="p">(</span><span class="nx">genus</span><span class="p">,</span> <span class="nf">eq</span><span class="p">(</span><span class="nx">plant</span><span class="p">.</span><span class="nx">genusId</span><span class="p">,</span> <span class="nx">genus</span><span class="p">.</span><span class="nx">id</span><span class="p">))</span> <span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="nf">or</span><span class="p">(</span> <span class="nf">like</span><span class="p">(</span><span class="nx">plant</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">,</span> <span class="s2">`%</span><span class="p">${</span><span class="nx">searchTerm</span><span class="p">}</span><span class="s2">%`</span><span class="p">),</span> <span class="nf">like</span><span class="p">(</span><span class="nx">genus</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span> <span class="s2">`%</span><span class="p">${</span><span class="nx">searchTerm</span><span class="p">}</span><span class="s2">%`</span><span class="p">),</span> <span class="p">))</span> <span class="p">.</span><span class="nf">orderBy</span><span class="p">(</span><span class="nx">genus</span><span class="p">.</span><span class="nx">description</span><span class="p">)</span> </code></pre> </div> <p>Next, I need to add in a max ship week value from a Shipping Records table. This table has a many-to-many relationship with the Plant table. </p> <p>To begin, I create the ship week subquery <em>above the main query</em> in the file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">db</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/index</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">plant</span><span class="p">,</span> <span class="nx">genus</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/schema</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">eq</span><span class="p">,</span> <span class="nx">or</span><span class="p">,</span> <span class="nx">like</span><span class="p">,</span> <span class="nx">max</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">drizzle-orm</span><span class="dl">"</span> <span class="kd">const</span> <span class="nx">shipWeekQuery</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">select</span><span class="p">({</span> <span class="na">ShipWeek</span><span class="p">:</span> <span class="nf">max</span><span class="p">(</span><span class="nx">shippingRecord</span><span class="p">.</span><span class="nx">expectedShipWeek</span><span class="p">).</span><span class="k">as</span><span class="p">(</span><span class="dl">'</span><span class="s1">shipWeek</span><span class="dl">'</span><span class="p">),</span> <span class="na">ID</span><span class="p">:</span> <span class="nx">shippingRecord</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">,</span> <span class="p">}).</span><span class="k">from</span><span class="p">(</span><span class="nx">shippingRecord</span><span class="p">)</span> <span class="p">.</span><span class="nf">groupBy</span><span class="p">(</span><span class="nx">shippingRecord</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">)</span> <span class="p">.</span><span class="k">as</span><span class="p">(</span><span class="dl">'</span><span class="s1">shipWeekRecords</span><span class="dl">'</span><span class="p">)</span> </code></pre> </div> <p>I need to use <code>as()</code> for aliases in the above subquery twice. Once on the max ship week value, and once on the overall subquery. Without these, the subquery will not work. </p> <p>When adding the subquery <code>ShipWeek</code> value to the main query, I need to refer to the <code>shipWeekQuery</code>.</p> <p>Finally, I can add the subquery to the main query with a left join:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">plantData</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nf">select</span><span class="p">({</span> <span class="na">ID</span><span class="p">:</span> <span class="nx">plant</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">,</span> <span class="na">Genus</span><span class="p">:</span> <span class="nx">genus</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span> <span class="na">ShipWeek</span><span class="p">:</span> <span class="nx">shipWeekQuery</span><span class="p">.</span><span class="nx">ShipWeek</span><span class="p">,</span> <span class="p">}).</span><span class="k">from</span><span class="p">(</span><span class="nx">plant</span><span class="p">)</span> <span class="p">.</span><span class="nf">leftJoin</span><span class="p">(</span><span class="nx">genus</span><span class="p">,</span> <span class="nf">eq</span><span class="p">(</span><span class="nx">plant</span><span class="p">.</span><span class="nx">genusId</span><span class="p">,</span> <span class="nx">genus</span><span class="p">.</span><span class="nx">id</span><span class="p">))</span> <span class="p">.</span><span class="nf">leftJoin</span><span class="p">(</span><span class="nx">shipWeekQuery</span><span class="p">,</span> <span class="nf">eq</span><span class="p">(</span><span class="nx">plant</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">,</span> <span class="nx">shipWeekQuery</span><span class="p">.</span><span class="nx">ID</span><span class="p">))</span> <span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="nf">or</span><span class="p">(</span> <span class="nf">like</span><span class="p">(</span><span class="nx">plant</span><span class="p">.</span><span class="nx">plantVarietyInfoId</span><span class="p">,</span> <span class="s2">`%</span><span class="p">${</span><span class="nx">searchTerm</span><span class="p">}</span><span class="s2">%`</span><span class="p">),</span> <span class="nf">like</span><span class="p">(</span><span class="nx">genus</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span> <span class="s2">`%</span><span class="p">${</span><span class="nx">searchTerm</span><span class="p">}</span><span class="s2">%`</span><span class="p">),</span> <span class="p">))</span> <span class="p">.</span><span class="nf">orderBy</span><span class="p">(</span><span class="nx">genus</span><span class="p">.</span><span class="nx">description</span><span class="p">)</span> </code></pre> </div> <p>You can read more about creating a <a href="proxy.php?url=https://orm.drizzle.team/docs/select#select-from-subquery">select from subquery</a> in the Drizzle ORM docs.</p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> sql drizzle orm mysql How to Auto-Format Unwanted Python Line Indentations in VS Code Dave Gray Fri, 05 Jan 2024 00:00:00 +0000 https://dev.to/gitdagray/how-to-auto-format-unwanted-python-line-indentations-in-vs-code-53h4 https://dev.to/gitdagray/how-to-auto-format-unwanted-python-line-indentations-in-vs-code-53h4 <p><em>VS Code can remove unwanted line indentations in a Python file.. or can it?</em></p> <p><strong>TLDR:</strong> VS Code no longer supports the desired formatting behavior, but if you want, you can still install a version that supports it.</p> <p><strong>Update:</strong> You can install autopep8 in your virtual environment and run <code>autopep8 -i -a -a your-file-name.py</code> to get the desired formatting. Unfortunately, it won't apply in VS Code settings. You must run this in a terminal window. <em>Thanks to Michael J. Sheets for the email!</em> </p> <h2> The Beginning </h2> <p>I published a 9 Hour <a href="proxy.php?url=https://youtu.be/H2EJuAcrZYU">Python Course for Beginners</a> in 2023. </p> <p>In the second chapter of the course, I show how VS Code can auto-format your Python code when you save your file. </p> <p>I did it again to create the screen recording below:</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--59ysHa2N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/python-auto-format-indents.gif" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--59ysHa2N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/python-auto-format-indents.gif" alt="VS Code Python auto-formatting unwanted indentations" width="600" height="375"></a></p> <p>You can see that I tab twice and then choose "Format Document With" from the context menu. I then choose "Python" and the file auto-formats. </p> <p>Then I tab twice again and save the file with <code>Ctrl+S</code>. The file once again formats because I have <code>Format On Save</code> selected in my settings.</p> <h2> The Problem </h2> <p>Shortly after publishing my course, <strong>VS Code changed how it handles auto-formatting.</strong> </p> <p>This has led to some confusion and frustration.</p> <p>I get it. </p> <p><em>I am also frustrated when things change right after I publish a video.</em> </p> <p>In tech things change all the time - and that includes web development and programming tools. </p> <p>My best advice for beginners is to get used to change. Change will keep happening throughout your career in tech. </p> <p>That said, you can tackle a problem like this and find a solution. </p> <p>I did, and I'm going to share it below. </p> <p>First, let's address some fixes that won't work and a discussion that is not exactly accurate.</p> <h2> Fixes That Won't Work </h2> <p>After receiving many video comments about the Python formatting example I provided, I opened a Python file in VS Code and tried it again. </p> <p><strong>It didn't work.</strong> </p> <p>It clearly <em>used to work</em> as I demonstrated in the video, but something had indeed changed. </p> <p>At this point, I knew it was likely due to a VS Code update. </p> <p>I searched the web for an answer. I found all sorts of random advice given with the best of intentions. </p> <p>Some shared their VS Code <code>settings.json</code> preferences for Python because theirs worked. </p> <p>Some blamed using a virtual environment. </p> <p>Others claimed <code>autopep8</code> was deprecated, and everyone should use <code>black</code> instead. (As of January 2024, <a href="proxy.php?url=https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8">autopep8</a> is available for use and is not deprecated.)</p> <p>I believe anyone sharing the above solutions were just a VS Code update away from theirs not working as well. </p> <h2> A Not Exactly Accurate Discussion </h2> <p>I read the autopep8 logs of my current VS Code installation. There I noticed a warning stating <code>Skipping non python code</code>. </p> <p>I Googled this phrase along with <code>VS Code</code> and found a <a href="proxy.php?url=https://github.com/microsoft/vscode-python/discussions/21201">GitHub discussion</a> with a VS Code maintainer. </p> <p>I know he was trying to help, but he suggested what I demonstrated in the video above was NOT possible with VS Code as I had it setup: </p> <blockquote> <p>That's probably not being done by a formatter we support since that isn't valid Python code (you can't have an indent like that as it's not valid Python code). They may have some other extension installed, pressed something else to fix the indentation (e.g. ctrl+shift+tab to dedent the line), etc.</p> <p>Regardless, there is zero expectation from us that any formatter we support would be able to handle fixing that code.</p> </blockquote> <p>Again, I know he meant well, and I respect the VS Code (and all) maintainers. Nothing personal here, friend. Your work is vital. </p> <p>Unfortunately, <em>all of the assumptions above were incorrect</em>. </p> <ul> <li>I did not use another formatter. </li> <li>I did not have another extension installed. </li> <li>I did not press a key combination to dedent the line </li> </ul> <p>I'm sure the maintainer would have directly addressed the issue if he was provided with steps to recreate the conditions for himself. </p> <p><em>That's exactly what I'm going to do.</em></p> <h2> How to Remove Unwanted Line Indentations with VS Code Auto-Formatting </h2> <p>I had updated VS Code on my main workstation. The auto-formatting I demonstrated in the video was no longer working on it.</p> <p><em>Fortunately, my old Windows laptop had not been updated</em>. </p> <p>I checked my installed versions of VS Code and the Python extension for VS Code: </p> <p><code>VS Code v1.70.2</code></p> <p><code>Python Extension v2022.16.1</code></p> <p>I created a new Python file in VS Code and quickly recreated the auto-formatting scenario. </p> <p><strong>It worked!</strong></p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--59ysHa2N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/python-auto-format-indents.gif" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--59ysHa2N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/python-auto-format-indents.gif" alt="VS Code Python auto-formatting unwanted indentations" width="600" height="375"></a></p> <p>This told me my hunch was correct. The desired formatting behavior had been removed in a VS Code update.</p> <p>I wanted to confirm I could recreate this desired formatting behavior on another computer, too. </p> <p>I got out my new Macbook. It has the latest version of Python installed. (<em>3.12.1 as of this writing</em>) </p> <p>I downloaded <a href="proxy.php?url=https://code.visualstudio.com/updates/v1_70"><code>VS Code v1.70.2</code></a> and installed it.</p> <p>That's not enough though. I needed the VS Code Python extension to identify improper formatting. </p> <p>I couldn't find a <code>VSIX</code> file for version <code>2022.16.1</code> of the extension, so I went with the closest version I could find to download: <a href="proxy.php?url=https://www.vsixhub.com/go.php?post_id=101360&amp;app_id=f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5&amp;s=4WVwftKw6Tl%2FA&amp;link=https%3A%2F%2Ff2.vsixhub.com%2Ffile.php%3Fversion%3D2022.15.12731017%26ext_name%3Dpython"><code>v2022.15.12731017</code></a></p> <p>To install a VS Code extension from a VSIX file, open the Command Palette (<code>Ctrl+Shift+P</code>) and use the <code>Install from VSIX...</code> command. </p> <p>With these older versions now installed on my new Macbook, I once again recreated the auto-formatting scenario. </p> <p><strong>It worked, too!</strong></p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--59ysHa2N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/python-auto-format-indents.gif" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--59ysHa2N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://raw.githubusercontent.com/gitdagray/my-blogposts/main/images/python-auto-format-indents.gif" alt="VS Code Python auto-formatting unwanted indentations" width="600" height="375"></a></p> <p>Note the following:</p> <ul> <li>I do not have any other formatting extensions. </li> <li>I do not have anything about Python in my <code>settings.json</code> file.</li> <li>If I <code>Ctrl+Click</code> for the context menu and choose <code>Format Document With</code>, <code>Python</code> is the only formatting option listed.</li> </ul> <h2> Is It Worth It? </h2> <p><em>Probably not</em>. </p> <p>I wouldn't go back to an old version of VS Code just for this feature. This would also cause you to miss any new updates. </p> <p>Recent editions of VS Code give you red squiggly lines to identify this formatting mistake. You should be able to easily see them and correct them on your own. </p> <p>Note: I said <em>formatting mistake</em>. </p> <p>If you read the full <a href="proxy.php?url=https://github.com/microsoft/vscode-python/discussions/21201">GitHub discussion</a> I mentioned earlier, the maintainer says the line in question isn't being formatted because it isn't valid Python code.</p> <p>Instead of considering this to be a formatting issue, it seems VS Code is now handling this as invalid code - <em>an error</em>. Their opinion is this is something you should identify and fix yourself instead of relying on a formatter.</p> <p>That said, we all now know <strong>VS Code was once capable of auto-correcting unwanted line-indentations</strong>. </p> <p>It sure would be cool if a current version of VS Code did this again.</p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> python vscode What is the Yandex Verification meta tag? Dave Gray Wed, 27 Dec 2023 00:00:00 +0000 https://dev.to/gitdagray/what-is-the-yandex-verification-meta-tag-3573 https://dev.to/gitdagray/what-is-the-yandex-verification-meta-tag-3573 <p><em>If you look at the metadata in websites, you may see a meta tag with the name yandex-verification. Here's what it means.</em></p> <h2> What is Yandex? </h2> <p>According to Wikipedia, <a href="proxy.php?url=https://en.wikipedia.org/wiki/Yandex">Yandex</a> is a Russia based company that owns <a href="proxy.php?url=https://yandex.com/">Yandex Search</a>. The meta tag <code>yandex-verification</code> refers to Yandex Search.</p> <h2> What does yandex-verification do? </h2> <p><code>yandex-verification</code> is much like <a href="proxy.php?url=https://support.google.com/webmasters/answer/9008080">google-site-verification</a>. <code>yandex-verification</code> confirms you are the webmaster of your website for <a href="proxy.php?url=https://yandex.com/">Yandex Search</a>.</p> <h2> How can I add yandex-verfication to my website? </h2> <p>Yandex provides <a href="proxy.php?url=https://webmaster.yandex.com/welcome/">webmaster tools</a>. These tools help you add your site and receive your verification code. You then place the code in the yandex-verification meta tag for confirmation. </p> <h2> How to Add yandex-verification with Next.js </h2> <p>To add the yandex-verification meta tag in Next.js, add the following to the <code>metadata</code> object in your root <code>layout.tsx</code> file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/layout.tsx</span> <span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">Metadata</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next</span><span class="dl">'</span> <span class="c1">// rest of layout.tsx file here...</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">metadata</span><span class="p">:</span> <span class="nx">Metadata</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// rest of metadata object here... </span> <span class="na">verification</span><span class="p">:</span> <span class="p">{</span> <span class="na">google</span><span class="p">:</span> <span class="dl">"</span><span class="s2">YOUR-VERIFICATION-CODE-PROVIDED-BY-GOOGLE</span><span class="dl">"</span><span class="p">,</span> <span class="na">yandex</span><span class="p">:</span> <span class="dl">"</span><span class="s2">YOUR-VERIFICATION-CODE-PROVIDED-BY-YANDEX</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="p">}</span> </code></pre> </div> <p>Don't worry if you haven't added a <code>google-site-verification</code> code to your website. You can learn how by reading my <a href="proxy.php?url=https://www.davegray.codes/posts/nextjs-how-to-submit-your-sitemap"><em>How to Submit Your Sitemap</em></a> article. </p> <h2> The Result </h2> <p>You should now have a <code>yandex-verification</code> meta tag inside the <code>&lt;head&gt;</code> section of your website. </p> <p>Here's the meta tag from my website:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"yandex-verification"</span> <span class="na">content=</span><span class="s">"YOUR-VERIFICATION-CODE-PROVIDED-BY-YANDEX"</span><span class="nt">&gt;</span> </code></pre> </div> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Patreon:</strong> <a href="proxy.php?url=//patreon.com/davegray">Join my Support Team!</a></p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> seo webdev nextjs blog Robots.txt is NOT Robots meta Dave Gray Sat, 16 Dec 2023 00:00:00 +0000 https://dev.to/gitdagray/robotstxt-is-not-robots-meta-58ba https://dev.to/gitdagray/robotstxt-is-not-robots-meta-58ba <p><em>Providing a robots.txt file is not the same as providing robots meta tags for your website.</em></p> <h2> Robots? On My Website? </h2> <p>To be fair, they probably should have named robots <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Glossary/Crawler">crawlers</a>. As in "web crawler". The name <em>crawler</em> refers to what is also called a bot or robot and is a program. Search engines send these robots out to crawl the web and build their search indexes.</p> <h2> Robots.txt vs Robots Meta Tags: What's the Difference? </h2> <p>Both provide instructions to the bots visiting your website. The level of instruction is the difference.</p> <p>A robots.txt file applies to your entire website. </p> <p>Robots meta tags apply specifically to the pages they are in.</p> <p>Let's look at both in a little more detail...</p> <h2> Robots.txt </h2> <p>A <code>robots.txt</code> file is placed in the root directory of your website. It follows the <a href="proxy.php?url=https://www.rfc-editor.org/rfc/rfc9309.html">Robots Exclusion Protocol</a>. A robots.txt file tells robots what they are allowed or not allowed to index. The file is usually very simple. It contains directives for the User-agent (aka "robot"). It also states what files are allowed or disallowed from indexing. A few other optional directives exist like Sitemap, Host, and Crawl-delay.</p> <p>The <a href="proxy.php?url=https://www.davegray.codes/robots.txt"><code>robots.txt</code> for my blog</a> looks like this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>User-Agent: * Allow: / Sitemap: https://www.davegray.codes/sitemap.xml </code></pre> </div> <p>The wildcard <code>*</code> character value for User-Agent stands for all robots. Putting a slash <code>/</code> for the Allow value tells robots they are allowed to visit all files on your website.</p> <p>You can learn more <a href="proxy.php?url=https://www.robotstxt.org/robotstxt.html">about robots.txt</a> at <a href="proxy.php?url=https://www.robotstxt.org/">robotstxt.org</a>. For Next.js, you can learn <a href="proxy.php?url=https://www.davegray.codes/posts/nextjs-how-to-build-sitemap-robots-txt">how to build sitemap and robots.txt files</a> from one of my previous articles. </p> <h2> Robots Meta Tags </h2> <p>Robots meta tags go inside of the <code>&lt;head&gt;</code> element in your web pages. The directions they provide only apply to the page they are within. You can also provide more detailed instructions for bots in your robots meta tags. </p> <p>MDN provides a detailed table for <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name#other_metadata_names">robots metadata</a>. It lists the possible values you can use in your robots meta tags. My content pages use the <code>index, follow</code> values. In contrast, my tag results pages use the <code>noindex, follow</code> values. </p> <p>It is also worth noting that you can provide a robots meta tag specific to Google called <code>googlebot</code>. This provides directives specifically for the bot Google crawls websites with. </p> <p>Here's how I set up my robots metadata in Next.js:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/layout.tsx</span> <span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">Metadata</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next</span><span class="dl">'</span> <span class="c1">// rest of my layout.tsx file here...</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">metadata</span><span class="p">:</span> <span class="nx">Metadata</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// rest of my metadata object here... </span> <span class="na">robots</span><span class="p">:</span> <span class="p">{</span> <span class="na">index</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">follow</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">googleBot</span><span class="p">:</span> <span class="p">{</span> <span class="na">index</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">follow</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="dl">'</span><span class="s1">max-video-preview</span><span class="dl">'</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">max-image-preview</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">large</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">max-snippet</span><span class="dl">'</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">}</span> <span class="p">},</span> <span class="p">}</span> </code></pre> </div> <p>Notice the nested <code>googleBot</code> object that Next.js supports. The googlebot accepts some extra values. You can read about these values in their <a href="proxy.php?url=https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag">valid indexing and serving rules</a> list for robots meta tags. </p> <p>I'm providing <code>max-snippet: -1</code>. This means there is no limit on the number of characters Google can show in their snippet. <code>max-image-preview: large</code> indicates Google may provide a larger image preview of my page. There is also the <code>max-video-preview: -1</code> setting. It says there is no maximum limit to the number of seconds to use as a video snippet for videos on this page. </p> <p>Here's the output inside the <code>&lt;head&gt;</code> element of my pages:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"robots"</span> <span class="na">content=</span><span class="s">"index, follow"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"googlebot"</span> <span class="na">content=</span><span class="s">"index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1"</span><span class="nt">&gt;</span> </code></pre> </div> <p>Next.js <a href="proxy.php?url=https://www.davegray.codes/posts/nextjs-ordering-merging-metadata">orders and merges metadata</a>. Thus, you only need to add this data to your root <code>layout.tsx</code> file. That is unless you want to provide different directives elsewhere. For example, I do provide different directives for my tag results pages, and here is the code:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/tags/[tag]/page.tsx</span> <span class="c1">// rest of my page.tsx file here...</span> <span class="k">export</span> <span class="kd">function</span> <span class="nf">generateMetadata</span><span class="p">({</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="nx">tag</span> <span class="p">}</span> <span class="p">}:</span> <span class="nx">Props</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="c1">// rest of my metadata object here...</span> <span class="na">robots</span><span class="p">:</span> <span class="p">{</span> <span class="na">index</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">follow</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">googleBot</span><span class="p">:</span> <span class="p">{</span> <span class="na">index</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">follow</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="p">}</span> <span class="p">},</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>You could say I'm being redundant. I am providing the same directives to the standard robots meta tag and to the googlebot tag. That said, I want to keep my metadata consistent and since I use this pattern on my other pages, I'm using it here, too. </p> <p>Here is the resulting output inside the <code>&lt;head&gt;</code> element of any given tag results page:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"robots"</span> <span class="na">content=</span><span class="s">"noindex, follow"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"googlebot"</span> <span class="na">content=</span><span class="s">"noindex, follow"</span><span class="nt">&gt;</span> </code></pre> </div> <h2> Final Considerations </h2> <p>Like the <a href="proxy.php?url=https://youtu.be/k9ojK9Q_ARE?si=AOl396L5YrKgVRSW&amp;t=41">Pirates Code</a>, robots directives are more like guidelines than actual rules. <a href="proxy.php?url=https://www.robotstxt.org/faq/blockjustbad.html">Bad robots</a> can ignore these guidelines. Thus, you should not depend on robots directives. There is no guarantee they will keep your pages from public access or from search indexes.</p> <p>Check the docs for more examples of <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/functions/generate-metadata#robots">robots metadata in Next.js</a>. </p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> seo webdev nextjs blog Automate Open Graph Image Creation in Next.js Dave Gray Sun, 10 Dec 2023 00:00:00 +0000 https://dev.to/gitdagray/automate-open-graph-image-creation-in-nextjs-ee7 https://dev.to/gitdagray/automate-open-graph-image-creation-in-nextjs-ee7 <p>In this article, I'll share how you can dynamically create open graph images with Next.js. </p> <h2> What are Open Graph Images? </h2> <p>An open graph image is only one piece of a much larger set of <a href="proxy.php?url=https://ogp.me/#metadata">open graph metadata</a> that helps social media platforms build a display card for your content when it is shared. I previously published a full article on <a href="proxy.php?url=https://www.davegray.codes/posts/nextjs-open-graph-social-media-cards"><em>"How to Create Open Graph Social Media Cards"</em></a> where you can learn much more about the open graph metadata your site needs. When you provide open graph image data in your metadata, the image becomes part of the generated social media card.</p> <h2> Why Generate Dynamic Open Graph Images? </h2> <p>By setting up your Next.js website to dynamically generate open graph images, you are automating part of your workflow. You may not want to do this for all pages on your website, but I believe you will find it very useful.</p> <h2> My Current Workflow </h2> <p>I'm currently using a mixed approach of manual and dynamic open graph images for my blog. </p> <p>Here's what I do for each blog post: </p> <ul> <li>Manually select an image on the web</li> <li>Resize it to 1200px wide</li> <li>Crop it to 1200x630 </li> <li>Save it with the same file name as my blog article</li> <li>Load it up to the GitHub repository where I store my blog article <a href="proxy.php?url=https://mdxjs.com/">MDX</a> files </li> </ul> <p>Now that I have typed the steps out, I can really see that there are several additional steps I have to take every time I create a blog post. </p> <p>In contrast, the open graph images for my <a href="proxy.php?url=https://www.davegray.codes/">homepage</a> and for my <a href="proxy.php?url=https://www.davegray.codes/tags/nextjs">tag results</a> pages are dynamically generated by Next.js. The images are created automatically with <em>no extra steps!</em> </p> <h2> What Do Dynamic Open Graph Images Look Like? </h2> <p>They can be as simple or complex as you want to make them. </p> <p>Here's what my open graph image for the tag <code>nextjs</code> looks like:</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--nNFj07Zj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/tags/nextjs/opengraph-image/nextjs%3Fe6574072cc5e5cea" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--nNFj07Zj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/tags/nextjs/opengraph-image/nextjs%3Fe6574072cc5e5cea" alt="Open Graph image for nextjs tag results on my blog" width="800" height="420"></a></p> <p>And here's what the open graph image for my homepage looks like: <br> <a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--t5L0Qaat--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/opengraph-image%3F0ebeab659c19d3d8" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--t5L0Qaat--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/opengraph-image%3F0ebeab659c19d3d8" alt="Open Graph image for my blog homepage" width="800" height="420"></a></p> <p>The images I'm dynamically creating with Next.js for my tag results pages are very simple. </p> <p>The image I'm dynamically creating for my homepage has a more complex layout and fetches some vanity data from both GitHub and YouTube. </p> <p>Let's look at how both of these are made with Next.js:</p> <h2> 1. Specialized Route Handlers </h2> <p>Next.js supports <code>opengraph-image</code> and <code>twitter-image</code> files that can be placed alongside <code>page</code> files in any given route segment. Both of these files are <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#route-segment-config"><em>specialized route handlers</em></a> that will dynamically generate open graph images and twitter images respectively. </p> <p>Note that twitter images are essentially the same as open graph images but specifically for Twitter (now X). Learn more about the metadata for open graph and twitter in my previous article <a href="proxy.php?url=https://www.davegray.codes/posts/nextjs-open-graph-social-media-cards"><em>"How to Create Open Graph Social Media Cards"</em></a>. </p> <p>The Next.js docs provide some examples of how to <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#generate-images-using-code-js-ts-tsx">generate images using code</a>. I hope you benefit from seeing the code in my <code>opengraph-image.tsx</code> file as well. </p> <p>My <code>opengraph-image.tsx</code> file has three functions in it: </p> <ul> <li>generateStaticParams </li> <li>generateImageMetadata </li> <li>Image </li> </ul> <p>Here is the full code for my <code>opengraph-image.tsx</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/tags/[tag]/opengraph-image.tsx </span> <span class="k">import</span> <span class="p">{</span> <span class="nx">ImageResponse</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/og</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Inter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/font/google</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">getPostsMeta</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@/lib/posts</span><span class="dl">"</span> <span class="kd">const</span> <span class="nx">websiteURL</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">https://www.davegray.codes/</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">http://localhost:3000/</span><span class="dl">'</span> <span class="kd">const</span> <span class="nx">inter</span> <span class="o">=</span> <span class="nc">Inter</span><span class="p">({</span> <span class="na">weight</span><span class="p">:</span> <span class="dl">"</span><span class="s2">400</span><span class="dl">"</span><span class="p">,</span> <span class="na">subsets</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">latin</span><span class="dl">'</span><span class="p">]</span> <span class="p">})</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">generateStaticParams</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">posts</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getPostsMeta</span><span class="p">()</span> <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">posts</span><span class="p">)</span> <span class="k">return</span> <span class="p">[]</span> <span class="kd">const</span> <span class="nx">tags</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Set</span><span class="p">(</span><span class="nx">posts</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="nx">post</span> <span class="o">=&gt;</span> <span class="nx">post</span><span class="p">.</span><span class="nx">tags</span><span class="p">).</span><span class="nf">flat</span><span class="p">())</span> <span class="k">return</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">tags</span><span class="p">).</span><span class="nf">map</span><span class="p">((</span><span class="nx">tag</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">tag</span> <span class="p">}))</span> <span class="p">}</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">generateImageMetadata</span><span class="p">({</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="nx">tag</span> <span class="p">}</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">tag</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">})</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[{</span> <span class="na">id</span><span class="p">:</span> <span class="nx">tag</span><span class="p">,</span> <span class="na">size</span><span class="p">:</span> <span class="p">{</span> <span class="na">width</span><span class="p">:</span> <span class="mi">1200</span><span class="p">,</span> <span class="na">height</span><span class="p">:</span> <span class="mi">630</span> <span class="p">},</span> <span class="na">alt</span><span class="p">:</span> <span class="s2">`Posts about </span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="na">contentType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span> <span class="p">}]</span> <span class="p">}</span> <span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">Image</span><span class="p">({</span> <span class="nx">id</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span> <span class="p">})</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">ImageResponse</span><span class="p">(</span> <span class="p">(</span> <span class="c1">// ImageResponse JSX element</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="si">{</span><span class="nx">inter</span><span class="p">.</span><span class="nx">className</span><span class="si">}</span> <span class="na">style</span><span class="p">=</span><span class="si">{</span><span class="p">{</span> <span class="na">fontSize</span><span class="p">:</span> <span class="mi">184</span><span class="p">,</span> <span class="na">background</span><span class="p">:</span> <span class="dl">'</span><span class="s1">black</span><span class="dl">'</span><span class="p">,</span> <span class="na">color</span><span class="p">:</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">,</span> <span class="na">width</span><span class="p">:</span> <span class="dl">'</span><span class="s1">100%</span><span class="dl">'</span><span class="p">,</span> <span class="na">height</span><span class="p">:</span> <span class="dl">'</span><span class="s1">100%</span><span class="dl">'</span><span class="p">,</span> <span class="na">display</span><span class="p">:</span> <span class="dl">'</span><span class="s1">flex</span><span class="dl">'</span><span class="p">,</span> <span class="na">justifyContent</span><span class="p">:</span> <span class="dl">'</span><span class="s1">center</span><span class="dl">'</span><span class="p">,</span> <span class="na">alignItems</span><span class="p">:</span> <span class="dl">'</span><span class="s1">center</span><span class="dl">'</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="dl">'</span><span class="s1">black</span><span class="dl">'</span><span class="p">,</span> <span class="na">backgroundImage</span><span class="p">:</span> <span class="s2">`url(</span><span class="p">${</span><span class="nx">websiteURL</span><span class="p">}</span><span class="s2">images/og-card-bg-1.png)`</span><span class="p">,</span> <span class="p">}</span><span class="si">}</span><span class="p">&gt;</span> <span class="si">{</span><span class="nx">id</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">),</span> <span class="c1">// ImageResponse options</span> <span class="p">{</span> <span class="na">width</span><span class="p">:</span> <span class="mi">1200</span><span class="p">,</span> <span class="na">height</span><span class="p">:</span> <span class="mi">630</span><span class="p">,</span> <span class="p">}</span> <span class="p">)</span> <span class="p">}</span> </code></pre> </div> <p>The <code>generateStaticParams</code> function is identical to the same function in my <code>page.tsx</code> file that generates dynamic pages for the tag results. This function returns an array of tag objects that is passed to the <code>generateImageMetadata</code> function. </p> <p>The <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata"><code>generateImageMetadata</code> function</a> can be used to return multiple images for one route segment. It does this by sending the objects in its returned array to the companion <code>Image</code> function. Note that <code>generateImageMetadata</code> returns an array of objects and each object <strong>must</strong> include an <strong>id</strong> value. </p> <p>The <code>Image</code> function uses the <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/functions/image-response">ImageResponse</a> constructor. This function receives the required <strong>id</strong> parameter from each object provided by the array from <code>generateImageMetadata</code> and creates an image for it. In the above example, you can see I assign my <strong>tag</strong> value to <strong>id</strong> in <code>generateImageMetadata</code>, and then I use the <strong>id</strong> value in the <code>Image</code> function. </p> <h2> 2. Static vs Dynamic Output </h2> <p>By default, the dynamically generated images from the specialized route handlers are <a href="proxy.php?url=https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default">statically optimized</a>. For something like tag results pages or even blog article pages, that should be good. </p> <p>However, that will not be good if you need to fetch constantly updated data like the vanity metrics in the image for my homepage. </p> <p>No worries! </p> <p>You can change the static behavior with individual fetch options or route segment options. </p> <p>Below is the code in my <code>opengraph-image.tsx</code> file for my homepage:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/opengraph-image.tsx </span> <span class="k">import</span> <span class="p">{</span> <span class="nx">ImageResponse</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next/og</span><span class="dl">'</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Inter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next/font/google</span><span class="dl">'</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">FaYoutube</span><span class="p">,</span> <span class="nx">FaGithub</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-icons/fa6</span><span class="dl">'</span> <span class="kd">const</span> <span class="nx">websiteURL</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">https://www.davegray.codes/</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">http://localhost:3000/</span><span class="dl">'</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">runtime</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">edge</span><span class="dl">'</span> <span class="kd">const</span> <span class="nx">inter</span> <span class="o">=</span> <span class="nc">Inter</span><span class="p">({</span> <span class="na">weight</span><span class="p">:</span> <span class="dl">"</span><span class="s2">400</span><span class="dl">"</span><span class="p">,</span> <span class="na">subsets</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">latin</span><span class="dl">'</span><span class="p">]</span> <span class="p">})</span> <span class="c1">// Image metadata</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">alt</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Preview image for Dave Gray</span><span class="dl">'</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">size</span> <span class="o">=</span> <span class="p">{</span> <span class="na">width</span><span class="p">:</span> <span class="mi">1200</span><span class="p">,</span> <span class="na">height</span><span class="p">:</span> <span class="mi">630</span><span class="p">,</span> <span class="p">}</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">contentType</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span> <span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">Image</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">githubData</span> <span class="o">=</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`https://api.github.com/users/gitdagray`</span><span class="p">,</span> <span class="p">{</span> <span class="na">next</span><span class="p">:</span> <span class="p">{</span> <span class="na">revalidate</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="p">}</span> <span class="p">}).</span><span class="nf">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span> <span class="p">)</span> <span class="kd">const</span> <span class="nx">youtubeData</span> <span class="o">=</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`https://youtube.googleapis.com/youtube/v3/channels?part=statistics&amp;id=UCY38RvRIxYODO4penyxUwTg&amp;key=</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">YOUTUBE_API_KEY</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">{</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="na">Accept</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span> <span class="p">},</span> <span class="na">next</span><span class="p">:</span> <span class="p">{</span> <span class="na">revalidate</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="p">}</span> <span class="p">}).</span><span class="nf">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span> <span class="p">)</span> <span class="kd">const</span> <span class="p">[{</span> <span class="na">followers</span><span class="p">:</span> <span class="nx">githubFollowers</span><span class="p">,</span> <span class="na">bio</span><span class="p">:</span> <span class="nx">githubBio</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="nx">githubName</span><span class="p">,</span> <span class="p">},</span> <span class="p">{</span> <span class="na">items</span><span class="p">:</span> <span class="nx">youtubeItems</span> <span class="p">}</span> <span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nf">all</span><span class="p">([</span><span class="nx">githubData</span><span class="p">,</span> <span class="nx">youtubeData</span><span class="p">])</span> <span class="kd">const</span> <span class="nx">ytSubCount</span> <span class="o">=</span> <span class="nx">youtubeItems</span><span class="p">?.</span><span class="nx">length</span> <span class="p">?</span> <span class="nx">youtubeItems</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">statistics</span><span class="p">.</span><span class="nx">subscriberCount</span> <span class="p">:</span> <span class="kc">null</span> <span class="k">return</span> <span class="k">new</span> <span class="nc">ImageResponse</span><span class="p">(</span> <span class="p">(</span> <span class="c1">// ImageResponse JSX element goes here</span> <span class="p">),</span> <span class="c1">// ImageResponse options</span> <span class="p">{</span> <span class="c1">// For convenience, you can re-use the exported opengraph-image size config to also set the ImageResponse's width and height.</span> <span class="p">...</span><span class="nx">size</span><span class="p">,</span> <span class="p">}</span> <span class="p">)</span> <span class="p">}</span> </code></pre> </div> <p>You can see the <code>opengraph-image.tsx</code> file above is similar but still very different from the previous example I gave. </p> <p>This file is used to create one image. I am not using the <code>generate</code> functions that I used in the dynamic route segment for tags. Therefore, I define three exports for image metadata: alt, size, and contentType. </p> <p>I am also using the <strong>edge runtime</strong> in this file. If you look at the <a href="proxy.php?url=https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#runtime-differences">runtime differences</a>, you will see that <strong>edge</strong> does not support static rendering. </p> <p>When you use the <strong>edge runtime</strong>, <em>the <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate">revalidate</a> route segment config option is not available</em>. Because of this, I set revalidate to zero in each of the fetch requests instead. </p> <p>The fetch requests use the <a href="proxy.php?url=https://nextjs.org/docs/app/building-your-application/data-fetching/patterns#parallel-data-fetching">Parallel Data Fetching</a> pattern with <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all</a>. </p> <p>I did not share the HTML utilizing inlined CSS that makes up the JSX Element of the ImageResponse. It is truly an uglier version of the first example. <em>Why create such ugliness with inline CSS?</em> This question leads me to a discussion about the developer experience using the ImageResponse API. I will talk about that near the end of this article.</p> <h2> 3. Output </h2> <p>The resulting output of this file is the open graph image I shared above that includes up-to-date vanity metrics. An updated image will be provided when my homepage is shared. </p> <p>The only caveat is some social media sites prefer to cache open graph images. Adding any random parameter to the URL like <code>?dave</code> or <code>?reset=1</code> should get the site to grab the latest image for your post. </p> <p>When you create dynamic open graph images, Next.js also adds the related meta tags inside the <code>&lt;head&gt;</code> element of your website. Inspect your page with devtools, and you should find the following meta tags, but they may not be next to each other or in this order:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image"</span> <span class="na">content=</span><span class="s">"https://www.davegray.codes/tags/nextjs/opengraph-image/nextjs?e6574072cc5e5cea"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image:width"</span> <span class="na">content=</span><span class="s">"1200"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image:height"</span> <span class="na">content=</span><span class="s">"630"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image:alt"</span> <span class="na">content=</span><span class="s">"Posts about nextjs"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">property=</span><span class="s">"og:image:type"</span> <span class="na">content=</span><span class="s">"image/png"</span><span class="nt">&gt;</span> </code></pre> </div> <h2> 4. Twitter Images (X.com) </h2> <p>Everything discussed above also applies to the <code>twitter-image.tsx</code> files you can create as well. The <em>only difference</em> between my opengraph and twitter files is the background image I applied to each and the chosen color of an icon. Share my homepage on <a href="proxy.php?url=https://x.com/">X.com</a> and on <a href="proxy.php?url=https://www.linkedin.com/">LinkedIn</a> to see the difference. You can do the same with my tag results pages like the one for <a href="proxy.php?url=https://www.davegray.codes/tags/nextjs">articles tagged with nextjs</a>.</p> <p>Here's what my twitter image for the tag <code>nextjs</code> looks like:</p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--MRrS_53L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/tags/nextjs/twitter-image/nextjs%3F6f38845728b6d005" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--MRrS_53L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/tags/nextjs/twitter-image/nextjs%3F6f38845728b6d005" alt="Twitter image for the tag nextjs on my blog" width="800" height="420"></a></p> <p>And here's what the twitter image for my homepage looks like: </p> <p><a href="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--TsQR7ao1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/twitter-image%3F6269119bae970ac4" class="article-body-image-wrapper"><img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--TsQR7ao1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://www.davegray.codes/twitter-image%3F6269119bae970ac4" alt="Twitter image for my blog homepage" width="800" height="420"></a></p> <p>The metadata Next.js creates for your <code>twitter-image</code> is different than the metadata for your <code>opengraph-image</code>. Here are the meta tags you should find inside your <code>&lt;head&gt;</code> element:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight html"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:image"</span> <span class="na">content=</span><span class="s">"https://www.davegray.codes/tags/nextjs/twitter-image/nextjs?6f38845728b6d005"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:image:width"</span> <span class="na">content=</span><span class="s">"1200"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:image:height"</span> <span class="na">content=</span><span class="s">"630"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:image:alt"</span> <span class="na">content=</span><span class="s">"Posts about nextjs"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"twitter:image:type"</span> <span class="na">content=</span><span class="s">"image/png"</span><span class="nt">&gt;</span> </code></pre> </div> <h2> 5. View Your Images </h2> <p>In dev mode, you can open up devtools, grab the URL for your open graph image from the meta tag, and paste it into another browser tab to display your image. </p> <p>After you deploy your site, you can check your live URLs in several ways: </p> <ul> <li><a href="proxy.php?url=https://developers.facebook.com/tools/debug/">Facebook Sharing Debugger</a></li> <li><a href="proxy.php?url=https://www.linkedin.com/post-inspector/">LinkedIn Post Inspector</a></li> <li>Create a draft post on any social media and preview the card without actually posting </li> </ul> <p>The last suggestion is the <em>only way</em> to currently test images on <a href="proxy.php?url=https://x.com/">X.com</a>. The site previously allowed you to test with a <a href="proxy.php?url=https://cards-dev.twitter.com/validator">Twitter Card Validator</a>. However, it no longer previews images. </p> <p><strong>Remember:</strong> Add a random parameter to the end of your URL to get the social media sites to refresh their cached image: <code>?random</code>, <code>?abc</code>, etc.</p> <h2> 6. The Developer Experience </h2> <p>The specialized route handlers <code>opengraph-image</code> and <code>twitter-image</code> were introduced in Next.js version 13.3. If you already know how to create dynamic pages in Next.js, these files follow that pattern. This makes the learning curve intuitive.</p> <p>One noticeable impact to the DX while working with these files - <em>especially while tweaking the CSS</em> - is that the image does not automatically update after you save your changes. You are probably used to your Next.js app doing that for you, but for these image route handlers, you must make a new request by hitting refresh in your browser in order to see your changes. This is simply how the web works though and should not reflect on Next.js in a negative way. You might want to use a browser extension like <a href="proxy.php?url=https://chromewebstore.google.com/detail/auto-refresh-page/aipbahhkojbhioodfbfmnobjnkagpnfg">Auto Refresh Page</a> that refreshes the page for you at a regular interval while you work on these.</p> <p>The <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/functions/image-response">ImageResponse</a> constructor is limited in what it supports. I mentioned ugly code earlier. The ugliness is really due to <a href="proxy.php?url=https://nextjs.org/docs/app/api-reference/functions/image-response#supported-css-properties">limited support for CSS properties</a> and the need to inline all CSS with <code>style</code>. I would prefer to use Tailwind as I have on the rest of my site instead of applying inline styles. Not all CSS styles are supported either. I would like to see more support for CSS and possibly Tailwind inside of the ImageResponse constructor. </p> <h2> 7. An Additional Option </h2> <p>You can learn a lot by looking at <a href="proxy.php?url=https://github.com/leerob/leerob.io/tree/main">leerob's blog repo</a> which is open to the public. As of this writing (December 2023), Lee's blog has not yet adopted the specialized <code>opengraph-image</code> and <code>twitter-image</code> files. Instead, he is using a <a href="proxy.php?url=https://github.com/leerob/leerob.io/blob/main/app/og/route.tsx">standard <code>route</code> handler file</a> to dynamically create his open graph images, and that is always an additional option.</p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> nextjs webdev react seo Light & Dark Mode in Next.js App Router + Tailwind with No Flicker Dave Gray Mon, 04 Dec 2023 00:00:00 +0000 https://dev.to/gitdagray/light-dark-mode-in-nextjs-app-router-tailwind-with-no-flicker-19hd https://dev.to/gitdagray/light-dark-mode-in-nextjs-app-router-tailwind-with-no-flicker-19hd <p>Applying light and <a href="proxy.php?url=https://tailwindcss.com/docs/dark-mode">dark mode</a> themes with TailwindCSS is easy. However, if you want to allow users to toggle between light and dark mode themes while also identifying the system preference setting at load time and avoiding a page flicker, things get a little more complicated.</p> <h2> Why This Is Challenging </h2> <p>Next.js <a href="proxy.php?url=https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration#static-site-generation-getstaticprops">generates static pages</a> on the server before sending them to the browser (aka "the client"). This helps keep things fast and websites seem snappy, but the server cannot read what user preferences will be. The server has no idea in advance that John prefers light-mode in his browser and Jane prefers dark-mode in her browser. </p> <p>The best you can do is apply the <a href="proxy.php?url=https://tailwindcss.com/docs/dark-mode">TailwindCSS dark mode</a> variant in advance that will read the <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme">prefers-color-scheme</a> user system setting and apply the dark mode theme if that is preferred.</p> <p>When you want to allow a user to manually toggle the light-dark mode theme setting, you need to read the system preference first. If they do toggle the setting, you want to save the new setting in <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"><code>localStorage</code></a>. However, <code>localStorage</code> is only available in the browser. Your server components won't be able to read this setting, and this situation can cause a theme <em>flash</em> when your site loads due to an incorrect theme being applied for a split second. </p> <p>Fortunately, the <a href="proxy.php?url=https://github.com/pacocoursey/next-themes">next-themes</a> package can help avoid this issue while allowing users the ability to toggle between light and dark mode in your Next.js website.</p> <p>Here's how I applied <code>next-themes</code> with TailwindCSS to my blog:</p> <h2> 1. Edit your TailwindCSS config file </h2> <p>You need to make one addition to your <code>tailwind.config.ts</code> file if you want to <a href="proxy.php?url=https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually">toggle dark mode manually</a>. You must add a <code>darkMode</code> setting with a <code>class</code> value.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight typescript"><code><span class="c1">// tailwind.config.ts </span> <span class="cm">/** @type {import('tailwindcss').Config} */</span> <span class="kr">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="na">darkMode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">class</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre> </div> <h2> 2. next-themes </h2> <p>Now you are ready to add <code>next-themes</code> to your project by typing the following in your terminal:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>npm i next-themes </code></pre> </div> <p>The <a href="proxy.php?url=https://github.com/pacocoursey/next-themes">next-themes</a> GitHub repository provides directions for use with the Next.js App Router. The directions do not include TypeScript, but I'm adding some TypeScript below. </p> <p>First, create a <code>providers.tsx</code> file inside your <code>app</code> folder like this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/providers.tsx</span> <span class="dl">'</span><span class="s1">use client</span><span class="dl">'</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">ThemeProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-themes</span><span class="dl">'</span> <span class="k">export</span> <span class="kd">function</span> <span class="nf">Providers</span><span class="p">({</span> <span class="nx">children</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span> <span class="p">})</span> <span class="p">{</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nc">ThemeProvider</span> <span class="na">attribute</span><span class="p">=</span><span class="s">"class"</span> <span class="na">defaultTheme</span><span class="p">=</span><span class="s">'system'</span> <span class="na">enableSystem</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">children</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">ThemeProvider</span><span class="p">&gt;</span> <span class="p">}</span> </code></pre> </div> <p>Next, add the <code>&lt;Providers&gt;</code> component to your root <code>layout.tsx</code> by placing it <em>inside</em> the <code>&lt;body&gt;</code> tag:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/layout.tsx</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Providers</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./providers</span><span class="dl">'</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">RootLayout</span><span class="p">({</span> <span class="nx">children</span><span class="p">,</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span> <span class="p">})</span> <span class="p">{</span> <span class="k">return </span><span class="p">(</span> <span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="p">=</span><span class="s">"en"</span> <span class="na">suppressHydrationWarning</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nc">Providers</span><span class="p">&gt;</span> <span class="si">{</span><span class="nx">children</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nc">Providers</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span> <span class="p">)</span> <span class="p">}</span> </code></pre> </div> <h2> 3. suppressHydrationWarning </h2> <p>Notice the <a href="proxy.php?url=https://legacy.reactjs.org/docs/dom-elements.html#suppresshydrationwarning">suppressHydrationWarning</a> setting inside the <code>&lt;html&gt;</code> tag. As noted in the GitHub repo for <a href="proxy.php?url=https://github.com/pacocoursey/next-themes">next-themes</a>, if you do not apply this setting, you will get warnings because the <code>&lt;html&gt;</code> element <em>is being updated</em> by next-themes. </p> <p>The React docs discuss <a href="proxy.php?url=https://legacy.reactjs.org/docs/dom-elements.html#suppresshydrationwarning">suppressHydrationWarning</a>. The use of it by <code>next-themes</code> is exactly as intended. You are telling Next.js you want to override what the server may have sent because it <em>does not match</em> the user setting saved in <code>localStorage</code>. </p> <h2> 4. useTheme </h2> <p>Your implementation of <code>next-themes</code> is not complete until you create a component allowing users to manually change the themes. I called my component <code>ThemeSwitch.tsx</code>. </p> <p>Here it is:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight tsx"><code><span class="c1">// app/components/ThemeSwitch.tsx</span> <span class="dl">'</span><span class="s1">use client</span><span class="dl">'</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">FiSun</span><span class="p">,</span> <span class="nx">FiMoon</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react-icons/fi</span><span class="dl">"</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useTheme</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-themes</span><span class="dl">'</span> <span class="k">import</span> <span class="nx">Image</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">next/image</span><span class="dl">"</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">ThemeSwitch</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">mounted</span><span class="p">,</span> <span class="nx">setMounted</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">setTheme</span><span class="p">,</span> <span class="nx">resolvedTheme</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">useTheme</span><span class="p">()</span> <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nf">setMounted</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span> <span class="p">[])</span> <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">mounted</span><span class="p">)</span> <span class="k">return </span><span class="p">(</span> <span class="p">&lt;</span><span class="nc">Image</span> <span class="na">src</span><span class="p">=</span><span class="err">`</span><span class="na">data</span><span class="err">:</span><span class="na">image</span><span class="err">/</span><span class="na">svg</span><span class="err">+</span><span class="na">xml</span><span class="err">;</span><span class="na">base64</span><span class="err">,</span><span class="na">PHN2ZyBzdHJva2U9IiNGRkZGRkYiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMCIgdmlld0JveD0iMCAwIDI0IDI0IiBoZWlnaHQ9IjIwMHB4IiB3aWR0aD0iMjAwcHgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI</span><span class="err">+</span><span class="na">PHJlY3Qgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiB4PSIyIiB5PSIyIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjIiIHJ4PSIyIj48L3JlY3Q</span><span class="err">+</span><span class="na">PC9zdmc</span><span class="err">+</span><span class="na">Cg</span><span class="p">==</span><span class="err">`</span> <span class="na">width</span><span class="p">=</span><span class="si">{</span><span class="mi">36</span><span class="si">}</span> <span class="na">height</span><span class="p">=</span><span class="si">{</span><span class="mi">36</span><span class="si">}</span> <span class="na">sizes</span><span class="p">=</span><span class="s">"36x36"</span> <span class="na">alt</span><span class="p">=</span><span class="s">"Loading Light/Dark Toggle"</span> <span class="na">priority</span><span class="p">=</span><span class="si">{</span><span class="kc">false</span><span class="si">}</span> <span class="na">title</span><span class="p">=</span><span class="s">"Loading Light/Dark Toggle"</span> <span class="p">/&gt;</span> <span class="p">)</span> <span class="k">if </span><span class="p">(</span><span class="nx">resolvedTheme</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nc">FiSun</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">setTheme</span><span class="p">(</span><span class="dl">'</span><span class="s1">light</span><span class="dl">'</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">}</span> <span class="k">if </span><span class="p">(</span><span class="nx">resolvedTheme</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">light</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nc">FiMoon</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">setTheme</span><span class="p">(</span><span class="dl">'</span><span class="s1">dark</span><span class="dl">'</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>There's a lot going on above, but I'll guide you through. </p> <p>The goal is to allow users to click an icon to change the theme. If they click the sun icon provided with dark mode, they will switch to light mode, and if they click the moon icon provided with light mode, they will switch to dark mode. </p> <p>I start by importing the icons I want to use. Then I bring in both <code>useState</code> and <code>useEffect</code> followed by <code>useTheme</code> from <code>next-themes</code> and the <code>&lt;Image&gt;</code> component from Next.js. </p> <p>You must keep track of the mounted state because you cannot use the <code>setTheme</code> function from the <code>useTheme</code> hook unless you know your code is running in the browser. Without this check, you will get a hydration mismatch warning when initially rendering the component on the server. Components rendering on the server will not use hooks and cannot access client-side <code>localStorage</code>. </p> <p>You need to set the mount state inside of the <code>useEffect</code> hook that only runs on component mount in the client when it has an empty dependency array. This insures it will only run in the browser.</p> <p>If the component is <em>not mounted</em>, it renders a placeholder image to avoid <a href="proxy.php?url=https://web.dev/articles/cls">cumulative layout shift</a>. </p> <p>If the component <em>is mounted</em>, it checks the <code>resolvedTheme</code> value from the <code>useTheme</code> hook and provides the correct icon. </p> <p>Note: I did not have to add code for saving the theme choice in <code>localStorage</code>. <code>next-themes</code> handles that for you.</p> <h2> 5. End Result </h2> <p>You should now have a Next.js website that not only applies the user system preference, but also remembers any user changes to the light and dark mode theme preference specifically for your site. Therefore, a user may have a system preference of light mode, but can still choose a default dark mode for your website. </p> <p>This should <em>not</em> result in a page <a href="proxy.php?url=https://github.com/pacocoursey/next-themes#the-flash">flash</a>, but the <code>next-themes</code> docs do say your website <em>may still flash in dev mode</em>. Even if so, in production mode, there should be no flash. </p> <p>I have implemented <code>next-themes</code> with TailwindCSS on <a href="proxy.php?url=https://www.davegray.codes/">my blog</a> as of this writing. Please check it out if you want to see a deployed example. </p> <h2> Let's Connect! </h2> <p>Hi, I'm Dave. I work as a full-time developer, instructor and creator. </p> <p>If you enjoyed this article, you might enjoy my other content, too.</p> <p><strong>My Stuff:</strong> <a href="proxy.php?url=https://courses.davegray.codes/">Courses, Cheat Sheets, Roadmaps</a></p> <p><strong>My Blog:</strong> <a href="proxy.php?url=https://www.davegray.codes/">davegray.codes</a></p> <p><strong>YouTube:</strong> <a href="proxy.php?url=https://www.youtube.com/davegrayteachescode">@davegrayteachescode</a> </p> <p><strong>X:</strong> <a href="proxy.php?url=https://x.com/yesdavidgray">@yesdavidgray</a> </p> <p><strong>GitHub:</strong> <a href="proxy.php?url=https://github.com/gitdagray">gitdagray</a></p> <p><strong>LinkedIn:</strong> <a href="proxy.php?url=https://www.linkedin.com/in/davidagray/">/in/davidagray</a> </p> <p><strong>Buy Me A Coffee:</strong> <a href="proxy.php?url=https://www.buymeacoffee.com/davegray">You will have my sincere gratitude</a> </p> <p>Thank you for joining me on this journey. </p> <p>Dave</p> nextjs tailwindcss webdev react