<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://gsong.dev/</id>
    <title>George's Articles</title>
    <updated>2025-10-26T02:24:36.430Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>George Song</name>
        <email>george@gsong.dev</email>
        <uri>https://gsong.dev/</uri>
    </author>
    <link rel="alternate" href="https://gsong.dev/"/>
    <link rel="self" href="https://gsong.dev/atom.xml"/>
    <subtitle>Various articles by George Song</subtitle>
    <icon>https://gsong.dev/favicon.png</icon>
    <rights>Copyright 2025 George Song</rights>
    <entry>
        <title type="html"><![CDATA[My AI-Assisted Development Workflow (With Claude Code)]]></title>
        <id>https://gsong.dev/articles/ai-dev-workflow</id>
        <link href="https://gsong.dev/articles/ai-dev-workflow"/>
        <updated>2025-10-24T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[<p>How I've structured my development workflow around Claude Code, including the directories, modes, and tooling that make AI collaboration effective for me.</p>]]></summary>
        <content type="html"><![CDATA[<p>If you're reading this, you've probably already tried AI coding tools. Maybe you've had some wins, maybe some frustrations. This isn't a guide to "the right way" to work with AI—it's a snapshot of what I've landed on after lots of experimentation with Claude Code.</p><p>The most important thing I can tell you? <mark>Your workflow should emerge from your needs, not from copying mine</mark>. Take what resonates, ignore the rest, and adapt everything to your context.</p><h2>The Foundation: Two Key Directories</h2><p>Before diving into workflows and tooling, let's talk about the directories that anchor my AI collaboration.</p><h3>ai-swap/: The Collaboration Space</h3><p>This is a gitignored directory where Claude Code and I brainstorm, plan, and explore ideas without cluttering the repository. Think of it as a scratch space—it lives alongside my codebase but stays local.</p><p>I might start drafting an implementation plan here, iterate on architecture decisions, or just work through complex problems without worrying about commit history. It's messy, it's temporary, and that's exactly the point.</p><h3>docs-ai/: Project Memory</h3><p>While <code>ai-swap/</code> is ephemeral, <code>docs-ai/</code> is permanent. This directory contains documentation specifically written for Claude Code to consume:</p><ul><li>Architecture decisions and rationale (<code>architecture.md</code>)</li><li>Project-specific coding conventions (<code>development-patterns.md</code>)</li><li>Brand style guides (<code>style-guide.md</code>)</li><li>Data flow and state management (<code>data-flow.md</code>, <code>state-management.md</code>)</li><li>Common patterns and anti-patterns</li></ul><p>The key insight: <mark><code>docs-ai/</code> gets updated as part of the development cycle, not as an afterthought</mark>. When I merge a PR that introduces a new pattern or makes an architectural decision worth documenting, updating <code>docs-ai/</code> is part of the process.</p><p>This version-controlled documentation evolves with the project, giving Claude Code the context it needs to make better suggestions and maintain consistency.</p><h2>A Complete Development Cycle</h2><p>Let me walk you through how I typically approach a feature from start to finish.</p><p><strong>1. Think</strong></p><p>Before touching Claude Code, I spend time thinking through what I actually want. Mostly the desired outcome, though sometimes I'll have implementation ideas worth suggesting.</p><p><strong>2. Explore</strong></p><p>I start Claude Code in <strong>chat mode</strong> and talk through the feature. What are we trying to achieve? What constraints exist? What are the possible approaches?</p><p>This is conversation, not instruction. I'm exploring the solution space, not prescribing solutions.</p><p><strong>3. Design</strong></p><p>Once we've explored options, I switch to <strong>plan mode</strong> and ask for an implementation plan. If there's a UI component, I'll request ASCII diagrams to visualize the layout.</p><p>For non-trivial plans, we break everything down into steps so we can implement them one at a time and change direction if needed. This incremental approach keeps options open.</p><p>The plan might stay in <code>ai-swap/</code> for my reference, or it might become GitHub or Linear issues if I'm working with a team.</p><p><strong>4. Review and edit</strong></p><p>I read the plan carefully and edit as needed. This often involves a few rounds of iteration with Claude Code alongside manual edits—I'm the one who knows the project's hidden constraints and long-term direction.</p><p><strong>5. Branch or worktree</strong></p><p>Usually a branch is enough. But when I'm exploring multiple options simultaneously or juggling multiple features, I'll create git worktrees. This gives me clean, isolated environments and makes it easy to switch contexts without stashing changes.</p><p><strong>6. Iterate</strong></p><p>Now we implement in <strong>auto mode</strong>. When the requirements are clear and I know the inputs and outputs, I'll use red-green-refactor (TDD). But often I'm exploring, and that's okay—we'll implement, adjust, refactor, and commit frequently.</p><p><strong>7. Commit frequently</strong></p><p>Lots of small commits create rewind points. If we take a wrong turn, we can easily back up without losing much work. (Claude Code has a <code>/rewind</code> command for this too, but I prefer using git.)</p><p><strong>8. Create PR</strong></p><p>Once the implementation feels right, I create a pull request.</p><p><strong>9. Critical review</strong></p><p>This is an area where I'm constantly experimenting. Right now I'm testing a team of specialized agents—each focused on different facets of the review (security, accessibility, performance, test coverage)—rather than a single agent reviewing everything. But here's what doesn't change: <mark>I review the code myself</mark>. The AI-assisted review surfaces issues and perspectives I might miss, but ultimately I'm responsible for what gets merged, regardless of how the code was generated.</p><p><strong>10. Address feedback</strong></p><p>I work through the review feedback, making changes where they make sense and pushing back where they don't.</p><p><strong>11. Update docs-ai/</strong></p><p>If this PR introduced new patterns or made architectural decisions worth documenting, I update <code>docs-ai/</code> before merging.</p><p><strong>12. Merge to main</strong></p><p>Ship it!</p><h2>The Philosophy: Delegation vs Collaboration</h2><p>Did you notice something about that development cycle? I'm intentionally vague about <em>how</em> to implement things.</p><p>There are two modes of working with Claude Code: delegation and collaboration. I prefer collaboration, but delegation has its place.</p><p><strong>Delegation mode:</strong></p><blockquote><p>Create a pie chart that breaks down the line items of capex and opex, and add radio buttons to toggle between the two charts. When I hover over each pie piece, I want to see the actual dollar amount as a tooltip. Also, there should be callouts showing the percentage of each pie piece.</p></blockquote><p><strong>Collaboration mode:</strong></p><blockquote><p>I want to create visualizations for capex and opex that I can toggle between</p></blockquote><p>The first approach is delegation—I've already decided on the solution and I'm asking Claude Code to implement my specific design. This is perfectly valid when I know exactly what I want and how it should work.</p><p>But I usually prefer collaboration. The second approach states the goal and invites Claude Code to explore the solution space with me. Maybe a pie chart isn't the best visualization. Maybe radio buttons aren't the most intuitive toggle. By focusing on the outcome rather than the implementation, we can discover options I might not have considered.</p><p><mark>Choose delegation when you know the solution. Choose collaboration when you want to explore possibilities.</mark></p><h2>Development Approaches: When to Use What</h2><p>I mentioned red-green-refactor earlier, but that's not always the right approach. Let me explain when I use which strategy.</p><h3>Red-Green-Refactor (TDD)</h3><p>This works well when:</p><ul><li>I know the inputs and outputs upfront (like a conversion calculator)</li><li>Requirements are clear and specific</li><li>The problem aligns well with traditional TDD practices</li></ul><p>With Claude Code, TDD becomes incredibly efficient. Write a failing test, let Claude Code implement it, verify it passes, refactor if needed. Repeat.</p><h3>Exploratory Implementation</h3><p>But sometimes I don't want TDD. I use exploratory implementation when:</p><ul><li>I know the goal but not the exact solution</li><li>I need to see options before committing to an architecture</li><li>Discovery is part of the value (not just the end result)</li><li>I'm okay with dead ends and refactoring along the way</li></ul><p>This isn't sloppiness—it's intentionality about the development approach. Sometimes the best way forward is to try something, see how it feels, and adjust.</p><h2>The Tooling Philosophy</h2><p>Now let's talk about the tools I've built around Claude Code. These fall into three categories, each serving a distinct purpose.</p><h3>Slash Commands: Complex Workflows</h3><p>I use slash commands for workflows that need LLM decision-making—tasks that would be hard to script because they require judgment calls at each step.</p><p><strong>My most-used commands:</strong></p><ul><li><strong>Git commit management</strong>: Intelligent staging and commit message generation following conventional commit formats</li><li><strong>PR review</strong>: Critical analysis focused on actionable items</li><li><strong>Dependency upgrades</strong>: Research changelogs, update packages, run tests, and create structured PRs</li><li><strong>Worktree management</strong>: Smart branch naming and directory setup</li></ul><p>Notice what these have in common: they're all workflows that require making decisions based on context. A bash script could approximate these, but it would need complex branching logic to handle all the cases. With a slash command, Claude Code makes those judgment calls.</p><p>You <em>could</em> also use slash commands as a prompt library (shortcuts to frequently-used prompts), though I don't primarily use them that way.</p><h3>MCP Servers: Extended Capabilities</h3><p>MCP servers give Claude Code access to tools it can't reach via bash commands alone.</p><p><strong>Examples of what I use:</strong></p><ul><li>Linear (project management integration)</li><li>Context7 (library documentation lookup)</li><li>Chrome DevTools (browser automation for testing)</li><li>Netlify (deployment management)</li></ul><p>My strategy: <mark>only enable MCP servers when I need them</mark>. I might keep them configured in my session but disabled. This prevents context bloat (wasting Claude Code's token budget on unused tool descriptions) while maintaining quick access when I do need them.</p><p>(Shameless plug: I built <a href=https://github.com/gsong/ccmcp><code>ccmcp</code></a> to make managing MCP server configurations easier. Check it out if you find yourself juggling multiple MCP setups.)</p><h3>Agents: Context Management</h3><p>Here's where my thinking shifted: I initially thought of agents as task delegation tools. But the real value represents <mark>a fundamental shift in how I think about AI collaboration—agents aren't just about saving time, they're about saving context</mark>.</p><p><strong>Example: docs-lookup agent</strong></p><p>Instead of the main Claude Code instance reading through all my <code>docs-ai/</code> files (consuming valuable context tokens), I have an agent that specializes in that task. The orchestrator asks the agent a question, the agent searches the docs and returns an answer, and the orchestrator continues with its work.</p><p>The agent has its own context window, which means the orchestrator's context stays focused on the implementation task at hand.</p><h3>Skills: Still Exploring</h3><p>Skills are a brand new primitive in Claude Code, and I'm still figuring out how they fit into my workflow. I'm experimenting with them, but haven't yet found their natural place in my development process the way I have with slash commands, MCP servers, and agents.</p><h2>Context Management: Being Intentional</h2><p>Claude Code's token budget is generous, but it's not infinite. My strategy is <mark>minimal but ready</mark>:</p><ul><li>I don't enable all my MCP servers at once—only when I know I'll need them</li><li>I keep my <code>docs-ai/</code> focused and concise</li><li>I use agents to keep the orchestrator's context clean</li><li>Each time I enable a tool, I'm making a conscious decision about what capabilities I need</li></ul><p>Why does this matter? Because a cleaner context means better responses. When Claude Code isn't wading through dozens of tool descriptions it won't use, it can focus on the task at hand.</p><h2>Git Workflow Integration</h2><p>Since we're talking about practical workflows, let me touch on how this all integrates with git:</p><ul><li><strong>Worktrees</strong> give me isolated environments for each feature</li><li><strong>Frequent commits</strong> create safety nets</li><li><strong>Slash commands</strong> help with commit hygiene (fixup/autosquash workflows)</li><li><strong>PR review</strong> ensures quality control before merging</li><li><strong>docs-ai updates</strong> happen as part of the merge process, not after</li></ul><p>This isn't revolutionary—it's just being intentional about how AI assistance fits into a solid development workflow.</p><h2>Make It Your Own</h2><p>Here's the thing: <mark>this workflow evolved through lots of trial and error</mark>. I tried things, kept what worked, and discarded what didn't. Your context is different than mine. Your projects have different constraints. Your thinking style might prefer different approaches.</p><p>Start with one or two ideas from this article that resonate with you. Try them out. Adapt them. Break them if they don't work. Add your own innovations.</p><p>The tools are flexible. Claude Code supports slash commands, MCP servers, agents, different modes—but there's no single "right" way to use them. The best workflow is the one you'll actually use consistently.</p><p><strong>Some questions to guide your experimentation:</strong></p><ul><li>What parts of your development process feel repetitive?</li><li>Where do you lose context when switching between tasks?</li><li>What decisions require judgment but follow patterns?</li><li>How do you currently document architectural decisions?</li></ul><p>Your answers will lead you to different solutions than mine. And that's exactly how it should be.</p><p>The goal isn't to copy my workflow. The goal is to find the workflow that makes you more effective, more creative, and maybe even more excited about the work you're doing.</p><p>Now go experiment.</p>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2025-10-24T16:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building Better RSS/Atom Feeds in Astro with the `unified` Ecosystem]]></title>
        <id>https://gsong.dev/articles/astro-feed-unified</id>
        <link href="https://gsong.dev/articles/astro-feed-unified"/>
        <updated>2025-04-01T21:50:53.000Z</updated>
        <summary type="html"><![CDATA[<p>How I solved the challenge of generating RSS and Atom feeds for an Astro site with complex MDX content by leveraging the <code>unified</code> ecosystem and custom plugins.</p>]]></summary>
        <content type="html"><![CDATA[<p>Migrating this site from Gatsby v2 to Astro v5 was a big success! 🙌 But one area required significant custom work: generating RSS and Atom feeds. While feeds are crucial for content syndication, handling MDX content within them presented unexpected challenges that required a custom solution.</p><h2>Requirements for Feed Generation</h2><p>My goal was to generate both RSS and Atom feeds that accurately represented the site's articles, which are written in MDX. This meant addressing several key requirements:</p><ol><li><strong>Dual Format Support:</strong> Provide both RSS 2.0 and Atom 1.0 feeds.</li><li><strong>MDX Content Handling:</strong> Process MDX source, stripping out JavaScript (e.g., <code>import</code> statements) and dynamic React components that wouldn't work in a feed.</li><li><strong>Consistent HTML:</strong> Ensure the HTML output within feed entries (<code>&lt;content:encoded></code> or <code>&lt;content></code>) was clean, valid, and accurately reflected the article structure.</li><li><strong>Valid Markup:</strong> Maintain valid HTML, avoiding issues like unclosed tags often caused by aggressive minification.</li><li><strong>Absolute URLs:</strong> Convert all relative links within the content to absolute URLs based on the site's domain.</li><li><strong>Proper Encoding:</strong> Ensure correct character encoding throughout the process.</li></ol><h2>The Standard Approach: Astro's Built-in RSS Package</h2><p>Astro provides an official <a href=https://docs.astro.build/en/recipes/rss/><code>@astrojs/rss</code> package</a> for generating feeds. Initially, this seemed like the obvious solution to use.</p><pre><code class=language-ts>// Example using @astrojs/rss (Conceptual)
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import sanitizeHtml from "sanitize-html";
import MarkdownIt from "markdown-it";
const parser = new MarkdownIt();

export async function GET(context) {
  const posts = await getCollection("blog");
  return rss({
    // ... feed options
    items: posts.map((post) => ({
      // ... item options
      content: sanitizeHtml(parser.render(post.body)), // Problematic for MDX
    })),
  });
}
</code></pre><p>However, this approach quickly proved insufficient for several reasons:</p><ol><li><strong>MDX Incompatibility:</strong> <code>@astrojs/rss</code> relies on tools like <code>markdown-it</code> and <code>sanitize-html</code>, which are designed for standard Markdown (<code>.md</code>), not MDX (<code>.mdx</code>). These tools don't understand JSX or imports/components embedded within the content. My attempts to render MDX to HTML using Astro's internal tools specifically for the feed proved complex and unreliable.</li><li><strong>Limited Transformation:</strong> The package lacked built-in mechanisms to easily transform relative URLs to absolute ones within the rendered HTML content.</li><li><strong>Insufficient Control:</strong> There wasn't a straightforward way to selectively remove specific elements generated by MDX processing (like automatically generated Tables of Contents) or handle custom syntax (like <code>&lt;mark></code> tags used for highlighting) correctly for feed output.</li><li><strong>No Atom Support:</strong> The package focuses on RSS, lacking direct support for generating Atom feeds.</li></ol><h2>A More Flexible Solution: The <code>unified</code> Ecosystem</h2><p>To gain the necessary control over the MDX-to-HTML transformation pipeline specifically for feeds, I turned to the <a href=https://unifiedjs.com/><code>unified</code></a> ecosystem. This powerful framework processes content through Abstract Syntax Trees (ASTs), giving precise control at each transformation stage:</p><ul><li><strong><a href=https://github.com/remarkjs/remark><code>remark</code></a>:</strong> For parsing and transforming Markdown/MDX into a Markdown AST (MDAST).</li><li><strong><a href=https://github.com/rehypejs/rehype><code>rehype</code></a>:</strong> For parsing and transforming HTML into an HTML AST (HAST).</li></ul><p>For structuring and generating the final XML output, I chose the <a href=https://github.com/jpmonette/feed><code>feed</code></a> library. It offers a clean and effective API for creating both RSS 2.0 and Atom 1.0 feeds once the content is properly prepared.</p><h2>Building a Custom MDX-to-HTML Pipeline</h2><p>The core task was creating a function, <code>mdxToHtml</code>, to convert raw MDX article content and the description into clean, valid, feed-friendly HTML. Let's look at how this pipeline works:</p><pre><code class=language-ts>import type { Root as HastRoot, RootContent } from "hast";
import type { Root as MdastRoot } from "mdast";
import type { Plugin } from "unified";

import { Buffer } from "node:buffer";

import minifyHtml from "@minify-html/node";
import rehypeStringify from "rehype-stringify";
import remarkMarkers from "remark-flexible-markers";
import remarkMdx from "remark-mdx";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";

type UrlLike = URL | string;

export async function mdxToHtml(
  mdxContent: string,
  site: UrlLike,
): Promise&lt;string> {
  const result = await unified()
    .use(remarkParse) // 1. Parse Markdown/MDX text -> MDAST
    .use(remarkMdx) // 2. Handle MDX specific syntax (JSX, imports/exports)
    .use(remarkMarkers, { markerClassName: () => [] }) // 3. Handle &lt;mark> correctly
    .use(remarkRemoveToc) // 4. Custom: Remove "Table of Contents" headings
    .use(remarkRemoveImports) // 5. Custom: Remove JS import/export statements
    .use(remarkRehype) // 6. Bridge: Convert MDAST -> HAST
    .use(rehypeAbsoluteUrls, site) // 7. Custom: Convert relative URLs -> absolute
    .use(rehypeStringify) // 8. Convert HAST -> HTML string
    .process(mdxContent);

  // 9. Minify the HTML safely
  return minifyHtml
    .minify(Buffer.from(result.toString()), { keep_closing_tags: true })
    .toString();
}

// Custom remark plugin to remove JS imports/exports
const remarkRemoveImports: Plugin&lt;[], MdastRoot> = () => {
  return (tree) => {
    tree.children = tree.children.filter((node) => node.type !== "mdxjsEsm");
    return tree;
  };
};

// Custom remark plugin to remove "Table of Contents" headings
const remarkRemoveToc: Plugin&lt;[], MdastRoot> = () => {
  // ... (implementation filters out h2 nodes matching "Table of Contents")
  return (tree: MdastRoot) => {
    tree.children = tree.children.filter((node) => {
      if (node.type === "heading" && node.depth === 2) {
        const text = node.children
          .filter((child) => child.type === "text")
          .map((child) => child.value)
          .join("")
          .trim();
        const tocRegex = /(table[ -]of[ -])?contents?|toc/i;
        return !tocRegex.test(text);
      }
      return true;
    });
    return tree;
  };
};

// Custom rehype plugin to make URLs absolute
const rehypeAbsoluteUrls: Plugin&lt;[UrlLike], HastRoot> = (baseUrl) => {
  // ... (implementation traverses HAST, updates href/src)
  return (tree) => {
    const visit = (node: RootContent | HastRoot) => {
      if (node.type === "element") {
        if (node.tagName === "a" && node.properties?.href) {
          node.properties.href = createUrl(
            node.properties.href as string,
            baseUrl,
          );
        }
        // Note: A complete implementation would also handle `img[src]`, etc.
      }
      if ("children" in node) {
        node.children.forEach(visit);
      }
    };
    visit(tree);
    return tree;
  };
};

// Helper to create absolute URLs
export function createUrl(path: string, baseUrl: UrlLike): string | null {
  try {
    const fullUrl = new URL(path, baseUrl);
    return fullUrl.href;
  } catch (error) {
    console.error("Invalid path or base URL:", error);
    return null;
  }
}
</code></pre><p><strong>Key Plugins and Customizations:</strong></p><ul><li><code>remark-parse</code> & <code>remark-mdx</code>: Essential for correctly parsing the MDX source into an Abstract Syntax Tree (MDAST).</li><li><a href=https://github.com/ipikuka/remark-flexible-markers><code>remark-flexible-markers</code></a>: Allows using <code>==highlighted text==</code> syntax, which is correctly processed, unlike <code>&lt;mark></code> elements that <code>remark-mdx</code> can mistake for JSX components.</li><li><code>remarkRemoveToc</code> (Custom Plugin): Removes the auto-generated "Table of Contents" often included in articles, which is unnecessary and potentially confusing in a feed item.</li><li><code>remarkRemoveImports</code> (Custom Plugin): Strips out MDX <code>import</code> and <code>export</code> statements (<code>mdxjsEsm</code> nodes in the AST), which are invalid and unnecessary in feed content.</li><li><code>remark-rehype</code>: Converts the processed Markdown AST (MDAST) into an HTML AST (HAST).</li><li><code>rehypeAbsoluteUrls</code> (Custom Plugin): Traverses the HAST and uses the site's base URL to convert relative <code>href</code> attributes in <code>&lt;a></code> tags to absolute URLs—crucial for links to work correctly when viewed in feed readers.</li><li><code>rehype-stringify</code>: Serializes the final HAST back into an HTML string.</li></ul><h2>Overcoming Processing Challenges</h2><p>Even with the powerful <code>unified</code> ecosystem, I encountered several challenges that we need to solve:</p><ol><li><strong>Typography and Encoding Issues:</strong> An attempt to use <code>remark-smartypants</code> for typographic enhancements (curly quotes, em-dashes) resulted in mangled characters in the final feed output. Unable to properly debug the encoding conflicts, I omitted <code>remark-smartypants</code> from the feed generation pipeline to ensure feed validity.</li><li><strong>Minification Complications:</strong> Initial attempts used <code>rehype-preset-minify</code> to reduce HTML size. However, its default settings proved too aggressive, sometimes removing optional closing tags (like <code>&lt;/p></code>) which, while valid in browsers, could break stricter XML parsers used by some feed readers. Switching to <a href=https://github.com/wilsonzlin/minify-html><code>@minify-html/node</code></a> with the <code>keep_closing_tags: true</code> option provided safe and effective minification.</li></ol><pre><code class=language-ts>export async function mdxToHtml(
  mdxContent: string,
  site: UrlLike,
): Promise&lt;string> {
  const result = await unified()
    // ... remark/rehype pipeline
    .use(rehypeStringify)
    .process(mdxContent);

  return minifyHtml
    .minify(Buffer.from(result.toString()), { keep_closing_tags: true })
    .toString();
}
</code></pre><h2>Assembling the Complete Solution</h2><p>With the <code>mdxToHtml</code> utility handling the complex content transformation, the final feed generation logic became much cleaner and more maintainable.</p><p>The <code>generateFeed</code> function in <code>feeds/index.ts</code> orchestrates the process:</p><pre><code class=language-ts>import type { APIContext } from "astro";
import type { Author, FeedOptions } from "feed";

import { getCollection } from "astro:content";
import { Feed } from "feed"; // Use the 'feed' library

import { createUrl, mdxToHtml } from "./utils"; // Import our custom utils

// ... Author interface

export async function generateFeed(context: APIContext): Promise&lt;Feed> {
  const site = context.site!.toString();
  const author: SiteAuthor = {
    /* ... author details */
  };
  const feed = createFeedInstance(site, author); // Initialize Feed object
  await addArticlesToFeed(feed, site, author); // Add processed articles
  return feed;
}

function createFeedInstance(site: string, author: SiteAuthor): Feed {
  const feedOptions: FeedOptions = {
    /* ... feed metadata */
  };
  return new Feed(feedOptions);
}

async function addArticlesToFeed(
  feed: Feed,
  site: string,
  author: SiteAuthor,
): Promise&lt;void> {
  const articles = (await getCollection("articles")).sort(
    (a, b) => b.data.published.valueOf() - a.data.published.valueOf(),
  );

  for (const article of articles) {
    const link = createUrl(`/articles/${article.slug}`, site) as string;

    feed.addItem({
      title: article.data.title,
      id: link,
      link,
      published: article.data.published,
      date: article.data.updated || article.data.published, // Use updated if available
      author: [author],
      // Process description and body using our custom mdxToHtml
      description: await mdxToHtml(article.data.summary, site),
      content: await mdxToHtml(article.body || "", site),
    });
  }
}
</code></pre><p>Finally, simple Astro API endpoints (<code>pages/atom.xml.ts</code> and <code>pages/rss.xml.ts</code>) call <code>generateFeed</code> and use the appropriate methods from the <code>feed</code> library to return the XML:</p><pre><code class=language-ts>import type { APIContext } from "astro";
import { generateFeed } from "@/data/feeds";

export async function GET(context: APIContext) {
  const feed = await generateFeed(context);
  return new Response(feed.atom1(), {
    headers: { "Content-Type": "application/atom+xml" },
  });
}
</code></pre><pre><code class=language-ts>import type { APIContext } from "astro";
import { generateFeed } from "@/data/feeds";

export async function GET(context: APIContext) {
  const feed = await generateFeed(context);
  return new Response(feed.rss2(), {
    headers: { "Content-Type": "application/xml" },
  });
}
</code></pre><p>While Astro's standard RSS package works well for simpler sites using standard Markdown, the complexities of MDX content required a more tailored approach. The <code>unified</code> toolchain, combined with custom <code>remark</code> and <code>rehype</code> plugins, provided the granular control needed to:</p><ul><li>Correctly parse and handle MDX syntax</li><li>Strip unwanted JavaScript and components</li><li>Ensure proper HTML structure (including special elements like <code>&lt;mark></code> tags)</li><li>Remove extraneous content like Tables of Contents</li><li>Generate absolute URLs consistently</li><li>Apply safe HTML minification that preserves XML compatibility</li></ul><p>The primary job of our feed generation pipeline is to transform complex MDX content into clean, valid HTML that works reliably in feed readers while preserving the essence of our articles.</p><p>Pairing this custom pipeline with the robust <code>feed</code> library resulted in valid, clean, and content-rich Atom and RSS feeds that work reliably across feed readers. This solution successfully addressed one of the key challenges in my migration from Gatsby to Astro, ensuring content syndication remained a first-class feature of the site.</p>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2025-04-01T12:44:21.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Using the HTML Data List Element for Simple Combo Boxes]]></title>
        <id>https://gsong.dev/articles/datalist-autosuggest</id>
        <link href="https://gsong.dev/articles/datalist-autosuggest"/>
        <updated>2025-03-27T14:24:16.000Z</updated>
        <summary type="html"><![CDATA[<p>Learn how the HTML <code>&lt;datalist></code> element can create simple yet powerful combo boxes with dynamic suggestions.</p>]]></summary>
        <content type="html"><![CDATA[<h2>Use Case</h2><p>For a search feature, we're storing the 100 most recently used ("MRU") search terms. In the search box, we want to auto suggest based on the MRU terms, but not restrict the input to just those terms.</p><div></div><h2>Solution: HTML <code>&lt;datalist></code></h2><p>Until recently, I didn't know about the <a href=https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist><code>&lt;datalist></code></a> element until I read <a href=https://www.peterbe.com/plog/datalist-looks-great-on-mobile-devices>Peter Bengtsson's article</a>. <a href=https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist>According to MDN</a>:</p><blockquote><p>The HTML <code>&lt;datalist></code> element contains a set of <code>&lt;option></code> elements that represent the permissible or recommended options available to choose from within other controls.</p></blockquote><p>Sounds exactly like what we need.</p><h3>Version 1</h3><p>Let's start with a basic implementation:</p><pre><code class=language-js>const [term, setTerm] = React.useState("");
const [mruTerms, setMruTerms] = React.useState([]);

React.useEffect(() => {
  fetch("https://random-word-api.herokuapp.com/word?number=100")
    .then((response) => response.json())
    .then((data) => setMruTerms(data));
}, []);

return (
  &lt;main>
    &lt;form
      autoComplete="off"
      onSubmit={(e) => {
        e.preventDefault();
        const term = e.currentTarget.term.value;
        if (term.trim() !== "") {
          setTerm(term.trim());
          setMruTerms([...new Set([term.trim(), ...mruTerms])].slice(0, 100));
        }
      }}
    >
      &lt;label htmlFor="term">Search term&lt;/label>
      &lt;input list="mru-terms" id="term" name="term" />

      &lt;datalist id="mru-terms" key={term}>
        {mruTerms.map((term) => (
          &lt;option key={term} value={term} />
        ))}
      &lt;/datalist>

      &lt;button>Search&lt;/button>
    &lt;/form>

    &lt;p>Submitted term: {term}&lt;/p>
  &lt;/main>
);
</code></pre><ol><li>We keep track of two pieces of context data: what the user submits (<code>term</code>), and the list of MRU terms (<code>mruTerms</code>).</li><li>We initialize <code>mruTerms</code> when the component is first rendered. In a real app, this can be fetched from a service, upstream app state, or <code>localStorage</code>.</li><li><code>&lt;option></code> elements of the <code>&lt;datalist></code> are generated from <code>mruTerms</code>. Notice the entire <code>&lt;datalist></code> component is replaced every time <code>term</code> changes since we bind it to the <code>key</code> prop. <ul><li>This approach effectively links the <code>mruTerms</code> update cycle to changes in <code>term</code>, as a new array is generated whenever <code>term</code> changes.</li><li>As a bonus, this also works around a longstanding <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1474137">Firefox bug</a> with dynamic datalists.</li></ul></li><li>Each time the user performs a search, both <code>term</code> and <code>mruTerms</code> are updated (and persisted in a real app).</li></ol><div></div><h3>Version 2</h3><p>With very little code, our autosuggest feature already works pretty well. Let's refactor our code so all context data is handled within a single object:</p><pre><code class=language-js>const useInitialize = () => {
  const [{ term, mruTerms }, dispatch] = React.useReducer(reducer, initialData);
  React.useEffect(() => {
    fetch("https://random-word-api.herokuapp.com/word?number=100")
      .then((response) => response.json())
      .then((payload) => dispatch({ type: "init", payload }));
  }, []);

  const actions = {
    updateTerm: (payload) => dispatch({ type: "updateTerm", payload }),
  };
  return { term, mruTerms, actions };
};

const initialData = { term: "", mruTerms: [] };

const reducer = (data, { type, payload }) => {
  const { term, mruTerms } = data;
  switch (type) {
    case "init":
      return { term, mruTerms: payload };
    case "updateTerm":
      if (payload.trim() === "") return data;
      return {
        term: payload.trim(),
        mruTerms: [...new Set([payload.trim(), ...mruTerms])].slice(0, 100),
      };
    default:
      return data;
  }
};
</code></pre><ol><li>Instead of multiple <code>useState</code>s, we consolidate into a single <code>useReducer</code>.</li><li>We move most of the business logic into a custom hook, simplifying the actual component.</li></ol><div></div><h3>Version 3</h3><p>For our purposes, the search terms "ice cream", "Ice Cream", and " ICE CREAM " are all considered equivalents. We want to store the last version used (minus the surrounding spaces) without duplicates in <code>mruTerms</code>.</p><p>Let's create a couple of helper functions first:</p><pre><code class=language-js>const addMruTerm = (mruTerms, term) =>
  [term, ...mruTerms]
    .reduce(
      (unique, item) =>
        unique.some((e) => trimLower(e) === trimLower(item))
          ? unique
          : [...unique, item.trim()],
      [],
    )
    .slice(0, 100);

const trimLower = (term) => term.trim().toLowerCase();
</code></pre><p>Then we change how we update <code>mruTerms</code> in the reducer:</p><pre><code class=language-js>mruTerms: addMruTerm(mruTerms, payload);
</code></pre><div></div><h3>Version 4</h3><p>Our autosuggest feature is starting to work nicely. You'll notice that Chrome and Firefox sort suggestions in the <code>mruTerms</code> array order. I think a better experience would be to sort by three major sections. For example, if the search term is "cr":</p><ol><li>First, suggestions that begin with the current search term, e.g. "<mark>cr</mark>anial", "<mark>cr</mark>eamery".</li><li>Next, suggestions that contain words that start with the current search, e.g. "heavy <mark>cr</mark>ate", "ice <mark>cr</mark>eam".</li><li>Finally, suggestions that contain the search term, anywhere, e.g. "dis<mark>cr</mark>eet", "s<mark>cr</mark>ibe".</li></ol><p>Within each section, we can further sort alphabetically.</p><p>In order to accomplish this, we need to update <code>&lt;datalist></code> as we type the search term. This means we need to keep track of a couple of additional pieces of context data: <code>draft</code> and <code>sortedMruTerms</code>:</p><pre><code class=language-js>const initialData = { draft: "", term: "", mruTerms: [], sortedMruTerms: [] };
</code></pre><p>We also add another helper function <code>sortMruTerms</code>, and adjust the <code>reducer</code> accordingly:</p><pre><code class=language-js>const reducer = (data, { type, payload }) => {
  const { draft, mruTerms } = data;
  switch (type) {
    case "init":
      return { ...data, mruTerms: payload, sortedMruTerms: payload };
    case "updateDraft":
      return {
        ...data,
        draft: payload,
        sortedMruTerms: sortMruTerms(mruTerms, payload),
      };
    case "updateTerm":
      if (draft.trim() === "") return data;
      return {
        ...data,
        term: draft.trim(),
        mruTerms: addMruTerm(mruTerms, draft),
      };
    default:
      return data;
  }
};

const sortMruTerms = (mruTerms, term) => {
  const partial = trimLower(term);
  if (partial === "") return mruTerms;

  let terms = [...mruTerms];
  let sorted = [];

  sortFilters(partial).forEach((filter) => {
    sorted = [...sorted, ...terms.filter(filter).sort()];
    terms = terms.filter((t) => !sorted.includes(t));
  });

  return sorted;
};

const sortFilters = (partial) => [
  (t) => trimLower(t).startsWith(partial),
  (t) => RegExp(`\\b${partial}`, "i").test(t),
  (t) => RegExp(partial, "i").test(t),
];
</code></pre><ol><li>Initialize <code>sortedMruTerms</code>.</li><li>Add action to handle <code>updateDraft</code>, which is called each time the search term changes.</li><li><code>updateTerm</code> no longer requires a payload, since we can calculate new context values based on <code>draft</code>.</li><li>Minor adjustments are also needed in the custom hook to accommodate these changes.</li></ol><p>Lastly, some minor adjustments in the JSX and we're done:</p><pre><code class=language-js>&lt;input
  value={draft}
  onChange={(e) => actions.updateDraft(e.currentTarget.value)}
  list="mru-terms"
  id="term"
  name="term"
/>

&lt;datalist id="mru-terms" key={draft}>
  {sortedMruTerms.map((term) => (
    &lt;option key={term} value={term} />
  ))}
&lt;/datalist>
</code></pre><ol><li>Convert the search term <code>&lt;input></code> into a controlled component, binding its value to <code>draft</code> and event handling to <code>updateDraft</code>.</li><li><code>&lt;datalist></code> is now replaced every time <code>draft</code> changes, and its values come from <code>sortedMruTerms</code>.</li></ol><div></div><p>As you can see, by abstracting the logic out from the component, we can easily tweak the behavior while minimizing changes to the component itself. We can also test the utility functions independently. Composition FTW 🙌.</p><p>Can you think of other improvements to this feature? Fork one of the CodeSandboxes and see what you come up with.</p><h2>Accessibility</h2><p><a href=https://westonthayer.com/>Weston Thayer</a> and I had a discussion about accessibility for the datalist element. He pointed out the following issues and resources for further investigation.</p><p><a href=https://a11ysupport.io/tech/html/datalist_element>There are potentially some accessibility issues</a> that may prevent you from using this technique, specifically screen readers do not convey datalist changes. This may be acceptable in the case of auto suggest, since the user is free to enter whatever they like—the auto suggest feature is a nice-to-have.</p><p>For a React-specific alternative, <a href=https://reach.tech/combobox/>check out Reach UI's Combobox</a>. Also <a href=https://www.24a11y.com/2019/select-your-poison/>read 24 Accessibility's "&lt;select> Your Poison"</a> article for an in-depth discussion of why this is a hard-to-solve problem.</p><h2>Other Use Cases</h2><p>It occurred to me that this technique can be used in situations where you want to normalize data as much as possible, while still allowing the user to freely enter anything they like.</p><p><a href=https://gsong.dev/articles/workplace-justice-ally>In a recent article</a>, I talked about the issue of HR asking for personal pronouns. HR would like the data to be as consistent as possible, but the right thing to do is to allow people to enter whatever they want. Here's a possible implementation that fulfills both requirements:</p><div></div><h2>Takeaways</h2><ul><li>For combo boxes, try the built-in <code>&lt;datalist></code> element first.</li><li>You can dynamically generate the datalist options based on events.</li><li>Abstract out as much logic as possible from your components for composability, testability, and agility.</li></ul>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-09-12T19:09:37.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Being an Ally Means Getting It Wrong Sometimes]]></title>
        <id>https://gsong.dev/articles/workplace-justice-ally</id>
        <link href="https://gsong.dev/articles/workplace-justice-ally"/>
        <updated>2025-03-27T14:25:51.000Z</updated>
        <summary type="html"><![CDATA[<p>Allyship isn’t about being perfect—it’s about learning, listening, and growing.</p>]]></summary>
        <content type="html"><![CDATA[<p>Recently, there was an exchange at work that bothered me. It happened in a small private Slack channel about social justice. The company's HR manager announced that she added a "Preferred Personal Pronoun" field to the HR system. It's a select-one field with three choices:</p><ul><li>He/Him</li><li>She/Her</li><li>They/Them</li></ul><p>Another person in the channel, who is part of the community impacted by this issue, suggested a couple of changes:</p><ol><li><p>Remove "preferred" from the label</p> <p><em>Affected person: Remove the word preferred from the "Preferred Personal Pronoun" label, since preference implies that it can be ignored.</em></p> <p><em>HR manager: "Hmm, I didn't pick up on it that way," but will look into removing the word from the label.</em></p> <p><strong>Possible intention</strong>: Trying to be more inclusive.</p> <p><strong>Unintended outcome</strong>: Your offhand remark invalidates my feelings and experiences. It makes me feel like my gender identity is something I have to justify, when most people are moving through life without having to even think about it.</p></li><li><p>Use a text field instead of select-one</p> <p><em>Affected person: Use a text field instead of select-one to allow people to put whatever they want as their pronoun.</em></p> <p><em>HR manager: Will look into adding an "other" selection with a text box.</em></p> <p><strong>Possible intention</strong>: Good point—there, problem solved.</p> <p><strong>Unintended outcome</strong>: <em>Other</em>? Can I feel even more, well, <em>othered</em>?</p></li></ol><h2>Intention vs. Outcome</h2><p>Of course, I don't know exactly how either person felt. The situation is far more nuanced than can be neatly summarized, but hopefully, the point is clear: words matter.</p><p>It may seem like an exaggeration when you're part of the privileged group, but interactions like this can make a person feel further marginalized, dehumanized, and even attacked, regardless of the best intentions.</p><p>Another trope reinforced by this interaction is that the affected person is, once again, in the position to do the educating, rather than the privileged person doing their own research and critical thinking. Regardless of intention, it makes the effort seem meaningless and the outcome careless.</p><h2>One of My Own Mistakes</h2><p>In a private follow-up conversation with the affected person, I asked for feedback on my own interactions with her. She pointed out that during the hiring process, the company doesn't formally ask candidates for their pronouns, and that I asked for her pronouns well after an offer was extended. She made a couple of suggestions:</p><ol><li>I should have asked the first time I met her.</li><li>I should have offered my own pronouns before asking.</li></ol><p>By not taking these simple steps, I reinforced harmful gender normative behaviors. I certainly never asked for pronouns from other people we've interviewed or hired. Why is that?</p><p>From her perspective, these are the mistakes I made:</p><ol><li>I assumed how a woman should look and sound.</li><li>I didn't create space for her actual identity, relying on assumptions until after she was hired.</li><li>By not offering my own pronouns, and by not asking people who appear to fit neatly into the gender box, I <em>othered</em> her.</li></ol><p>I had to ask myself, in my entire life, how many times have I been asked for my pronouns in a way that questioned my gender? How many times have I had to struggle with myself about what <em>my</em> pronouns are? Exactly zero. By assuming my experience is the <em>normal</em> experience, I actively caused harm to another person that I care about.</p><p>She suggested Natalie Reed's excellent <a href=https://freethoughtblogs.com/nataliereed/2012/04/17/the-null-hypothecis/>The Null HypotheCis</a> article as a more in-depth exploration of reframing the gender question. I encourage you to take the time and space to read, reflect, and gain a bit more empathy.</p><h2>Justice Work Is Hard Work</h2><p>You may think, why make a mountain out of a molehill? Wasn't it great when things were simple and clear-cut? The fact is that things are not, and never were, simple and clear-cut. They were only conveniently simple and clear-cut when you choose to go along, perhaps even a little uncomfortably, with the side that wields the power.</p><p>Most of us wield power of some kind, even if we belong to a marginalized group. None of us can do the right thing all the time. I mess up, and you'll mess up too. The key is to listen, recognize, acknowledge, educate, and activate.</p><p>Keep learning how to accept critical feedback graciously, even if it makes you feel bad. Keep learning how to spot injustices, big or small. Keep learning about injustices and expanding your own capacity to make meaningful changes to correct them.</p><h2>So, About That Pronoun Field…</h2><p>One month after the Slack exchange, the select-one field with three choices is still labeled "Preferred Personal Pronoun" in the HR system.</p><h2>Update (September 4, 2020)</h2><p>After another misstep in changing the pronoun field as a response to this article, and an impassioned follow-up by the affected person in the Slack channel, the HR system now has a text field labeled "Personal Pronoun."</p><p>One. Step. At. A. Time.</p>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-08-28T15:04:22.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[React.useReducer Reducer Patterns, Part 3]]></title>
        <id>https://gsong.dev/articles/reducer-patterns/part-3</id>
        <link href="https://gsong.dev/articles/reducer-patterns/part-3"/>
        <updated>2025-03-29T23:27:32.000Z</updated>
        <summary type="html"><![CDATA[<p>Discover the most common useReducer pattern in React, from basic state management to modeling complex state transitions.</p>]]></summary>
        <content type="html"><![CDATA[<ul><li><a href=https://gsong.dev/articles/reducer-patterns/part-1/>Part 1: Introduction to <code>useReducer</code> and basic reducer patterns</a></li><li><a href=https://gsong.dev/articles/reducer-patterns/part-2/>Part 2: Reducers with actions</a></li></ul><p>So far in parts 1 and 2 of this series, we've learned that:</p><ul><li>The primary job of a reducer is to produce a new state.</li><li>Dispatched actions can take any shape you want.</li></ul><p>In part 3, we'll look at the most common pattern for reducers and actions you'll find in documentation and online examples, and we'll model a state chart using a reducer.</p><h2>Typical <code>useReducer</code> Pattern</h2><p>Most <code>React.useReducer</code> examples you encounter dispatch actions resembling <code>{ type, payload }</code>. Their reducer functions typically consist of a <code>switch</code> statement that matches on <code>type</code> and uses <code>payload</code> to calculate the next state.</p><p>Remember, the reducer signature is <code>(currentState, action) => nextState</code>. A <code>switch</code>-statement-based reducer looks like this:</p><pre><code class=language-js>const reducer = (count, { type, payload }) => {
  switch (type) {
    case "add":
      return count + payload;
    case "reset":
      return initialValue;
    default:
      throw new Error();
  }
};
</code></pre><p>To trigger this reducer, there are two possible actions we can dispatch.</p><pre><code class=language-js>dispatch({ type: "add", payload: 5 });
dispatch({ type: "reset" });
</code></pre><p>We can encapsulate these two possibilities with <a href=https://redux.js.org/basics/actions#action-creators>bound action creators</a>:</p><pre><code class=language-js>const add = (payload) => dispatch({ type: "add", payload });
const reset = () => dispatch({ type: "reset" });
</code></pre><p>Action creators are optional; you can dispatch actions directly where needed instead. However, they offer a cleaner interface for the hook's user by hiding the reducer's implementation details.</p><p>The complete <code>useReducer</code> definition looks like this:</p><pre><code class=language-js>const initialValue = 0;
const reducer = (count, { type, payload }) => {
  switch (type) {
    case "add":
      return count + payload;
    case "reset":
      return initialValue;
    default:
      throw new Error();
  }
};

const [count, dispatch] = React.useReducer(reducer, initialValue);

const add = (payload) => dispatch({ type: "add", payload });
const reset = () => dispatch({ type: "reset" });
</code></pre><p>Our hook interface is now <code>count</code>, plus the <code>add()</code> and <code>reset()</code> functions.</p><div></div><h2>Tightly Coupled State Values</h2><p>So far, we've explored several reducer and action patterns, but we haven't discussed <em>why</em> we use <code>useReducer</code>. In fact, you can express the previous example more clearly using a <code>useState</code> hook:</p><pre><code class=language-js>const initialValue = 0;
const [count, setCount] = React.useState(initialValue);
const add = (value) => setCount(count + value);
const reset = () => setCount(initialValue);
</code></pre><div></div><p><code>useState</code> is optimized for managing a single state value. This implies that <code>useReducer</code> excels when we need to manage multiple related state values.</p><p>What if we want to add an undo function to retrieve previous counts? One way to do this is to track the count history. Instead of only tracking the latest count, our state object now looks like this:</p><pre><code class=language-js>const initialState = { count: 0, history: [] };
</code></pre><p>We update the reducer with the following changes:</p><ul><li>Return the current state as-is if we add zero, since this action doesn't change the state.</li><li>If we're adding any other value, also add the current <code>count</code> to the <code>history</code> stack.</li><li>Add a new <code>case</code> to handle the <code>undo</code> action.</li></ul><pre><code class=language-js>const reducer = (state, { type, payload }) => {
  const { count, history } = state;
  switch (type) {
    case "add":
      if (payload === 0) return state;
      return { count: count + payload, history: [...history, count] };
    case "reset":
      return initialState;
    case "undo":
      if (history.length === 0) return state;
      const lastCount = [...history].pop();
      return { count: lastCount, history: history.slice(0, -1) };
    default:
      throw new Error();
  }
};
</code></pre><p>Lastly, we add another bound action creator to handle <code>undo()</code>:</p><pre><code class=language-js>const undo = () => dispatch({ type: "undo" });
</code></pre><p>Putting it all together:</p><pre><code class=language-js>const initialState = { count: 0, history: [] };

const reducer = (state, { type, payload }) => {
  const { count, history } = state;
  switch (type) {
    case "add":
      if (payload === 0) return state;
      return { count: count + payload, history: [...history, count] };
    case "reset":
      return initialState;
    case "undo":
      if (history.length === 0) return state;
      const lastCount = [...history].pop();
      return { count: lastCount, history: history.slice(0, -1) };
    default:
      throw new Error();
  }
};

const [{ count, history }, dispatch] = React.useReducer(reducer, initialState);

const add = (payload) => dispatch({ type: "add", payload });
const reset = () => dispatch({ type: "reset" });
const undo = () => dispatch({ type: "undo" });
</code></pre><p>We see that <code>count</code> and <code>history</code> are tightly coupled. When we add a value, we update <code>count</code> and add the previous count to the <code>history</code> stack. When we undo, we replace <code>count</code> by popping the last item from the <code>history</code> stack.</p><div></div><h2>Nested Switch Statements</h2><p>Did you know you can nest a <code>switch</code> statement inside the <code>case</code> clause of another <code>switch</code> statement? Using this technique, you can implement sophisticated logic with the <code>useReducer</code> hook, especially when combined with <code>useEffect</code>.</p><p>Let's create a seemingly simple form with a single input and a couple of buttons:</p><ul><li>The input will only accept <code>[A-Za-z]</code> characters</li><li>Submit button to POST the input value to an API</li><li>Reset button to reset the input value to the last successful submission.</li></ul><p>Simple, right? That's often how forms start, but complexity arises when you consider:</p><ul><li>If input is invalid, display error message and don't allow submission.</li><li>Don't allow submission if input value hasn't changed from the previous submission.</li><li>During submission, disable all inputs.</li><li>Display submission status.</li><li>When a submission error occurs, the submit button should remain active to allow for retry.</li></ul><p>How can you satisfy all these requirements? Should you try to capture all the logic imperatively? Should you use one of the many React form libraries? I believe <a href=https://statecharts.github.io/>state charts</a> offer a great way to declaratively satisfy all these requirements.</p><div></div><p>Our state chart has four states—editing, submitting, resolved, rejected—with clear transitions defined among the states, e.g. we transition from <code>editing</code> to <code>submitting</code> state by performing the <code>submit</code> action.</p><h3>Initial Hook State</h3><p>First, let's define what our <code>useReducer</code> hook state looks like. Note that the hook's state is distinct from the state chart's state. Our hook state looks like this:</p><pre><code class=language-js>const initialState = {
  value: "editing",

  context: {
    previousValue: "",
    value: "",
    isValid: true,
    submitAllowed: false,
    isSuccessful: undefined,
  },
};
</code></pre><p>The <code>value</code> property holds our state chart's current state. We also have a separate <code>context</code> property that tracks the <a href=https://en.wikipedia.org/wiki/UML_state_machine#Extended_states>extended state</a>.</p><h3>Translate a State Chart to a Reducer Function</h3><pre><code class=language-js>const reducer = (state, { type, payload } = {}) => {
  const { value, context } = state;
  // Top-level switch based on the state chart's current state (state.value)
  switch (value) {
    case "editing":
      // Nested switch based on the action type (transition)
      switch (type) {
        case "change":
          const isValid = /^[A-Za-z]*$/.test(payload);
          const submitAllowed = isValid && context.previousValue !== payload;
          return {
            value,
            context: { ...context, value: payload, isValid, submitAllowed },
          };

        case "reset":
          return {
            value,
            context: {
              ...context,
              value: context.previousValue,
              isValid: true,
              submitAllowed: false,
            },
          };

        case "submit":
          if (context.submitAllowed) return { value: "submitting", context };
          return state;

        default:
          return state;
      }

    case "submitting":
      switch (type) {
        case "resolve":
          return { value: "resolved", context };
        case "reject":
          return { value: "rejected", context };
        default:
          return state;
      }

    case "resolved":
      return {
        value: "editing",
        context: {
          ...context,
          previousValue: context.value,
          isSuccessful: true,
          submitAllowed: false,
        },
      };

    case "rejected":
      return {
        value: "editing",
        context: {
          ...context,
          isSuccessful: false,
        },
      };

    default:
      return state;
  }
};
</code></pre><p>Notice the top-level <code>switch</code> statement evaluates the <code>state.value</code> (the state chart's current state). For the <code>editing</code> and <code>submitting</code> states, we use a nested <code>switch</code> based on <code>action.type</code> (representing the state chart transition). Why is this done? This guarantees that, for example, the <code>submit</code> transition is only valid when the state chart is in the <code>editing</code> state. Practically, this means that if the form is already in the <code>submitting</code> state, it cannot be submitted again, regardless of user actions (solving the multiple button click issue, even if the submit button isn't explicitly disabled).</p><p>If you compare the code to the diagram, you'll see that each <code>case</code> clause corresponds to a transition—seven transitions mean seven <code>case</code> clauses that return the next state.</p><p>Notice that the <code>default</code> clauses always return the original state unmodified. This aligns with how state machines typically work: attempting an invalid transition leaves the state unchanged.</p><p>Looking at each <code>case</code> clause, you'll notice we're performing a couple of tasks:</p><ol><li>Specify the next state by changing <code>state.value</code>.</li><li>Changing <code>state.context</code> as appropriate for a given transition.</li></ol><h3>Putting It All Together</h3><pre><code class=language-js>const [state, dispatch] = React.useReducer(reducer, initialState);
const transitions = {
  change: (payload) => dispatch({ type: "change", payload }),
  reset: () => dispatch({ type: "reset" }),
  submit: () => dispatch({ type: "submit" }),
};
</code></pre><p>To trigger transitions, we use the <code>dispatch()</code> function returned by the <code>useReducer</code> hook. Transitions can be triggered manually (e.g., by a user clicking a button) via event handlers, or automatically (e.g., when state changes) via <code>useEffect</code>.</p><p>Discussing complex state chart topics in depth is beyond the scope of this article. What we're demonstrating here is that you can model fairly complex systems using reducers, if somewhat awkwardly.</p><div></div><p>If you're curious about state charts, I highly recommend checking out <a href=https://stately.ai/docs/xstate>XState</a>.</p><h2>Takeaways</h2><ul><li>Use <code>React.useReducer</code> when you need to manage multiple tightly-coupled state values.</li><li>Reducers are capable of modeling complex systems.</li><li>Consider making your <code>useReducer</code> hook more user-friendly by providing action creators.</li></ul><p>As demonstrated in this series, <code>useReducer</code> is a hook pattern capable of solving a wide variety of problems, from simple to moderately complex. Now that the potentially intimidating aspect of the pattern (reducers) is hopefully less daunting, I encourage you to use <code>useReducer</code> more frequently and creatively.</p><ul><li><a href=https://gsong.dev/articles/reducer-patterns/part-1/>Part 1: Introduction to <code>useReducer</code> and basic reducer patterns</a></li><li><a href=https://gsong.dev/articles/reducer-patterns/part-2/>Part 2: Reducers with actions</a></li></ul>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-08-02T23:32:21.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[React.useReducer Reducer Patterns, Part 2]]></title>
        <id>https://gsong.dev/articles/reducer-patterns/part-2</id>
        <link href="https://gsong.dev/articles/reducer-patterns/part-2"/>
        <updated>2025-03-27T14:25:18.000Z</updated>
        <summary type="html"><![CDATA[<p>Discover advanced useReducer patterns that unlock new ways to manage state in React applications.</p>]]></summary>
        <content type="html"><![CDATA[<ul><li><a href=https://gsong.dev/articles/reducer-patterns/part-1/>Part 1: Introduction to <code>useReducer</code> and basic reducer patterns</a></li></ul><p><a href=https://gsong.dev/articles/reducer-patterns/part-1/>In part 1</a>, we looked at reducer patterns that didn't use any parameters or only used the current state to calculate the next state. We also learned that <mark>the primary job of a reducer is to produce a new state</mark>.</p><p>Let's continue exploring other reducer patterns.</p><h2>Reducer Using <code>action</code></h2><div></div><p>Remember that <code>(currentState, action) => newState</code> is the complete reducer signature. In the patterns we've explored so far, we haven't used the <code>action</code> parameter. We know the hook automatically supplies the current state to the reducer, but how does a reducer receive its <code>action</code> parameter? It receives the <code>action</code> parameter via the <code>dispatch</code> function returned by the hook: <code>[state, dispatch] = useReducer(reducer)</code>. The <code>dispatch</code> function takes a single optional parameter: <code>dispatch(action)</code>.</p><h3>Use Both <code>currentState</code> and <code>action</code> Parameters</h3><p>Let's look at an example where we use both parameters with the <code>reducer</code> function.</p><pre><code class=language-js>const reducer = (count, valueToAdd) => count + valueToAdd;
const [count, addToCount] = React.useReducer(reducer, 0);

return (
  &lt;main>
    &lt;div>Count: {count}&lt;/div>
    &lt;form
      onSubmit={(e) => {
        e.preventDefault();
        const valueToAdd = Number(e.currentTarget.numberToAdd.value);
        addToCount(valueToAdd);
      }}
    >
      &lt;label>
        Add to count:{" "}
        &lt;input
          name="numberToAdd"
          type="number"
          defaultValue={1}
          style={{ width: "4em" }}
        />
      &lt;/label>
      &lt;button>Add&lt;/button>
    &lt;/form>
  &lt;/main>
);
</code></pre><p>What's happening? When we submit the form, we dispatch (<code>addToCount</code>) an action (the value from <code>&lt;input name="numberToAdd"></code>). The reducer takes the current state (<code>count</code>) and the action (<code>valueToAdd</code>) to produce a new state (<code>count + valueToAdd</code>).</p><p>Many examples of actions you've seen use the shape <code>{ type, payload }</code>. While this is a common convention, <mark>an action can be anything you like</mark> (including <code>undefined</code>). In this example, an action is simply a number.</p><div></div><h3>Use <code>action</code> Param Only</h3><p>Just like how we can choose to only use the <code>currentState</code> param in a reducer, we can choose to only use the <code>action</code> parameter.</p><pre><code class=language-js>const reducer = (_, newCount) => newCount;
const [count, setCount] = React.useReducer(reducer, 0);

return (
  &lt;main>
    &lt;div>Count: {count}&lt;/div>
    &lt;form
      onSubmit={(e) => {
        e.preventDefault();
        const valueToAdd = Number(e.currentTarget.numberToAdd.value);
        setCount(count + valueToAdd);
      }}
    >
      ...
    &lt;/form>
  &lt;/main>
);
</code></pre><p>By switching up a few lines, our <code>reducer</code> now only relies on the action (<code>newCount</code>) to calculate its new state.</p><p>🤔 Wait a minute, that looks a lot like <code>React.useState</code>.</p><div></div><h2>Implement a Simple <code>useState</code></h2><p>Replace</p><pre><code class=language-js>const reducer = (_, newCount) => newCount;
const [count, setCount] = React.useReducer(reducer, 0);
</code></pre><p>with</p><pre><code class=language-js>const reducer = (_, newState) => newState;
const useState = (initialState) => React.useReducer(reducer, initialState);
const [count, setCount] = useState(0);
</code></pre><p>We have ourselves a simple <code>useState</code>! You can see that <code>React.useState</code> is syntactic sugar for <code>React.useReducer</code>, simplifying the common use case of updating a single state value. A complete re-implementation of <code>useState</code> is a few more lines of code. <a href=https://kentcdodds.com/blog/how-to-implement-usestate-with-usereducer>See Kent C. Dodd's "How to implement useState with useReducer"</a> if you're interested in an in-depth explanation.</p><div></div><h2>Implement a State Updater</h2><h3>Use Case</h3><p>We have a user profile form that allows users to change each value in the state object.</p><h3>Solution</h3><p>One elegant solution is to use the <a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>object spread syntax</a> to create the next state.</p><pre><code class=language-js>const reducer = (info, updates) => ({ ...info, ...updates });
const initialValue = {
  name: "Pat Doe",
  twitter: "@pdough",
  email: "pat@pdough.me",
  website: "https://pdough.me",
};
const [info, update] = React.useReducer(reducer, initialValue);

return (
  &lt;main>
    &lt;pre>{JSON.stringify(info, null, 2)}&lt;/pre>
    &lt;form>
      {Object.entries(info).map(([key, value]) => (
        &lt;label key={key} style={{ display: "block" }}>
          {key}:{" "}
          &lt;input
            value={value}
            onChange={(e) => update({ [key]: e.currentTarget.value })}
          />
        &lt;/label>
      ))}
    &lt;/form>
  &lt;/main>
);
</code></pre><div></div><h2>Dispatch Functions as Actions</h2><p>Wait, say what? 🤔</p><pre><code class=language-js>const reducer = (count, action) => action(count);
const [count, dispatch] = React.useReducer(reducer, 0);
const add = (value) => dispatch((count) => count + value);
const subtract = (value) => dispatch((count) => count - value);

return (
  &lt;main>
    &lt;div>Count: {count}&lt;/div>
    &lt;form>
      &lt;label>
        Change count by:{" "}
        &lt;input
          name="modifier"
          type="number"
          defaultValue={1}
          style={{ width: "4em" }}
        />
      &lt;/label>
      &lt;button
        type="button"
        onClick={(e) => add(Number(e.currentTarget.form.modifier.value))}
      >
        Add
      &lt;/button>
      &lt;button
        type="button"
        onClick={(e) => subtract(Number(e.currentTarget.form.modifier.value))}
      >
        Subtract
      &lt;/button>
    &lt;/form>
  &lt;/main>
);
</code></pre><p>Yup, as stated earlier, <mark>an action can be anything you like</mark>. In this example, we're dispatching callback functions! 🤯</p><div></div><h2>Takeaways</h2><ul><li>Dispatched actions can take any shape you want: <code>undefined</code>, a value, an object, or even a function.</li><li>The <code>action</code> parameter is the value shared between the <code>dispatch()</code> and <code>reducer()</code> functions, acting as their private contract.</li><li>React.useState is syntactic sugar for one specific use case of React.useReducer.</li></ul><h2>Intermission</h2><p>You can implement a reducer in any way you like, as long as it fulfills the contract of <code>([currentState], [action]) => newState</code>. You have complete freedom in deciding what an action looks like, and how you want to calculate the new state.</p><p>We've explored different ways of writing the reducer function, and we haven't even come across the familiar <code>switch</code> statement yet. Don't worry, we'll get to that in <a href=https://gsong.dev/articles/reducer-patterns/part-3/>part 3</a>.</p><ul><li><a href=https://gsong.dev/articles/reducer-patterns/part-1/>Part 1: Introduction to <code>useReducer</code> and basic reducer patterns</a></li><li><a href=https://gsong.dev/articles/reducer-patterns/part-3/>Part 3: Reducers with switch statements</a></li></ul>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-07-26T01:53:56.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[React.useReducer Reducer Patterns, Part 1]]></title>
        <id>https://gsong.dev/articles/reducer-patterns/part-1</id>
        <link href="https://gsong.dev/articles/reducer-patterns/part-1"/>
        <updated>2025-03-27T14:24:28.000Z</updated>
        <summary type="html"><![CDATA[<p>Learn practical <code>useReducer</code> patterns for writing clear and effective reducers, starting with no-param and current state-only approaches.</p>]]></summary>
        <content type="html"><![CDATA[<p>I see many people, especially those new to React hooks, shy away from <a href=https://reactjs.org/docs/hooks-reference.html#usereducer><code>useReducer</code></a> in favor of multiple <code>useState</code> calls. I suspect this stems partly from the simplicity of the <code>useState</code> API and partly from an assumed complexity due to its association with <a href=https://redux.js.org/>Redux</a>. In practice, <code>useReducer</code> has much more in common with <code>useState</code> than Redux.</p><h2>Updating State With <code>useState</code> and <code>useReducer</code></h2><p>The <code>useState</code> API has two parts: the state and the state setter. When you want to update the state, you replace it using the state setter.</p><div></div><p>The <code>useReducer</code> API has three parts: the state, an action dispatcher, and a reducer to produce the new state.</p><div></div><p>When you want to update the state:</p><ol><li>Dispatch an action.</li><li>The hook passes the current state and the action to the reducer function.</li><li>The reducer computes a new state.</li><li>The state is updated with the new state.</li></ol><p><code>useReducer</code> takes care of steps 2 and 4. You're responsible for steps 1 and 3.</p><h2>The Reducer Function</h2><p>These are all valid signatures for reducer functions:</p><pre><code class=language-js>(currentState, dispachedAction) => newState
(currentState) => newState
() => newState
</code></pre><p>As you can see, <mark>the primary job of a reducer is to produce a new state</mark>, optionally taking into account the current state and the dispatched action.</p><h2>Putting It All Together With useReducer</h2><p><a href=https://reactjs.org/docs/hooks-reference.html#usereducer>The signature of <code>useReducer</code></a> is:</p><pre><code class=language-js>const [state, dispatch] = useReducer(reducer, initialState);
</code></pre><p>You pass in a <code>reducer</code> function (and optionally an <code>initialState</code>) to set up a <code>useReducer</code> hook. What you get back is a <strong>read-only</strong> <code>state</code> and a <code>dispatch</code> function which you'll use to trigger the <code>reducer</code>.</p><p>This is one of the key differences when compared to <code>useState</code>—a <code>useReducer</code> hook computes the next state internally using a <code>reducer</code> function, while a <code>useState</code> hook relies entirely on external calculations for its next state.</p><p>For the rest of the article, we'll focus on different <code>reducer</code> (a.k.a. "new state" calculator) implementation patterns.</p><h2>Reducer Without Any Params</h2><p><code>() => newState</code></p><p>The primary job of a reducer is to return a new state. In its simplest form, a reducer doesn't even need to accept any parameters.</p><pre><code class=language-js>const reducer = () => new Date();
const [date, update] = React.useReducer(reducer, new Date());

return (
  &lt;main>
    &lt;div>
      Now: {date.toLocaleDateString()} {date.toLocaleTimeString()}
    &lt;/div>
    &lt;button onClick={update}>Update&lt;/button>
  &lt;/main>
);
</code></pre><p>The names <code>state</code>, <code>dispatch</code>, and <code>reducer</code> are conceptual. In practice, you can name them anything you like. In this example, they are <code>date</code> (state), <code>update</code> (dispatch), and <code>reducer</code>.</p><p>Notice that we invoke <code>update()</code> <em>without</em> passing any parameters. The dispatch function signals the hook to run the reducer function; it's up to you whether you need to send an action along with the signal. In this case, the hook actually runs <code>reducer(currentState, undefined)</code>.</p><h3>🤔 But Wait, the Reducer Doesn't Accept Any Params</h3><p>That's right. In JavaScript, just because you pass parameters to a function, doesn't mean it needs to accept them.</p><pre><code class=language-js>const sayHi = () => "hi";
const result = sayHi(1, 2, 3);
// result === "hi"
</code></pre><div></div><h2>Reducer Using Current State</h2><p><code>(currentState) => newState</code></p><p>This form of reducer is useful when you need the current state to calculate the next state.</p><pre><code class=language-js>const reducer = (count) => count + 1;
const [count, addOne] = React.useReducer(reducer, 0);

return (
  &lt;main>
    &lt;div>{count}&lt;/div>
    &lt;button onClick={addOne}>Add 1&lt;/button>
  &lt;/main>
);
</code></pre><p>Once again, the dispatch function, <code>addOne()</code>, doesn't need to pass an action to <code>reducer</code>—the <code>reducer</code> is capable of computing the next state entirely based on the current state.</p><div></div><h2>Takeaways</h2><ul><li>The primary job of a reducer is to produce a new state.</li><li>A reducer function can take zero, one, or two params: <code>([currentState], [dispatchedAction])</code>.</li><li><code>dispatch()</code> signals the <code>useReducer</code> hook to invoke the reducer.</li></ul><h2>Intermission</h2><p>😅 Whew, let's take a break for now. <a href=https://gsong.dev/articles/reducer-patterns/part-2/>In part 2</a>, we'll explore reducers that use actions, as well as reducers that use both actions and the current state.</p><ul><li><a href=https://gsong.dev/articles/reducer-patterns/part-2/>Part 2: Reducers with actions</a></li><li><a href=https://gsong.dev/articles/reducer-patterns/part-3/>Part 3: Reducers with switch statements</a></li></ul>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-07-19T19:06:41.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Formatting Dates in JavaScript]]></title>
        <id>https://gsong.dev/articles/to-locale-date-string</id>
        <link href="https://gsong.dev/articles/to-locale-date-string"/>
        <updated>2025-03-27T14:25:28.000Z</updated>
        <summary type="html"><![CDATA[<p>Format dates in JavaScript efficiently with <code>toLocaleDateString()</code>—no extra libraries required!</p>]]></summary>
        <content type="html"><![CDATA[<p>Do you automatically reach for <a href=https://momentjs.com/>Moment.js</a> or <a href=https://date-fns.org/>date-fns</a> when formatting dates? For basic needs, consider the built-in JavaScript tools instead.</p><h2>Use Case</h2><p>I'm displaying a date with each article on this site. The source of the date is an <a href=https://en.wikipedia.org/wiki/ISO_8601>ISO date string</a>, which looks like<div>isoDate</div>. This is a common format you'll receive from APIs. <p>While ISO format is excellent for data exchange, it's not user-friendly for display. Let's localize it instead.</p><div></div><h2>Solution</h2><ol><li><p><a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#Timestamp_string>Parse the ISO date string</a></p> <pre><code class=language-js>const isoDateString = "2020-07-10T14:16:40.463Z";
const date = new Date(isoDateString);
</code></pre></li><li><p><a href=https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/language>Get the browser locale</a></p> <pre><code class=language-js>const locale = typeof window === "undefined" ? "en" : navigator.language;
</code></pre> <p>If we're not in a browser environment (like during SSR), default to English.</p></li><li><p><a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString>Format the date</a></p> <pre><code class=language-js>const formattedDate = date.toLocaleDateString(locale, {
  year: "numeric",
  month: "long",
  day: "numeric",
});
</code></pre> <p><a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#Parameters>See the <code>Intl.DateTimeFormat()</code> constructor</a> for details on the parameters.</p></li></ol><p>It's a bit more code than using one of the libraries, but your users will thank you for the smaller bundle size.</p><p>OK, they probably won't—but you'll have the satisfaction of knowing you've done your part to improve their experience.</p><div></div>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-07-10T15:23:12.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Remove Properties From JavaScript Objects]]></title>
        <id>https://gsong.dev/articles/js-remove-unwanted-properties</id>
        <link href="https://gsong.dev/articles/js-remove-unwanted-properties"/>
        <updated>2025-03-27T14:25:42.000Z</updated>
        <summary type="html"><![CDATA[<p>Easily remove properties from JavaScript objects using destructuring and rest syntax for cleaner, immutable code.</p>]]></summary>
        <content type="html"><![CDATA[<h2>Use Case</h2><p>Suppose you have a JavaScript object received from a source (e.g., retrieved from an API):</p><pre><code class=language-js>const restaurant = {
  name: "Nong's Khao Man Gai",
  address: "609 SE Ankeny St",
  phone: "503-740-2907",
  secretKeyLocation: "somewhere",
};
</code></pre><p>You want to remove <code>secretKeyLocation</code> before passing the object on to the next part of the workflow.</p><h2>Solution: Remove Property Using Destructuring + Rest Syntax</h2><p>Use a combination of <a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring>object destructuring</a> and <a href=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals>object literal spread/rest syntax</a>.</p><pre><code class=language-js>const { secretKeyLocation, ...rest } = restaurant;
doSomethingWith(rest);
</code></pre><p>With this, you now have three variables:</p><ul><li><p><code>secretKeyLocation</code> with the value of <code>"somewhere"</code></p></li><li><p><code>rest</code>, a new object with the <code>secretKeyLocation</code> property removed:</p> <pre><code class=language-js>{
  name: "Nong's Khao Man Gai",
  address: "609 SE Ankeny St",
  phone: "503-740-2907",
}
</code></pre> <p>It's worth noting that <code>rest</code> can be any valid variable name; the name <code>rest</code> itself has no special meaning.</p></li><li><p><code>restaurant</code> remains untouched and retains its original value. Immutability FTW 🙌!</p></li></ul><div></div>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2020-07-09T14:53:23.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Eulogy for Lola]]></title>
        <id>https://gsong.dev/articles/lola-eulogy</id>
        <link href="https://gsong.dev/articles/lola-eulogy"/>
        <updated>2025-04-05T21:20:17.000Z</updated>
        <summary type="html"><![CDATA[<p>A reflection on the transformative journey with my dog Lola—from behavioral struggles to the impossible decision at the end of her life.</p>]]></summary>
        <content type="html"><![CDATA[<p>Lola. Dear, sweet Lola. The time since your death can still be quantified easily by the number of hours. You left this gaping hole inside me that grows bigger and smaller all on its own, out of my control. I still feel anger, the same anger I've felt since your diagnosis. For once I wanted you to catch a break, for once I wanted you to have it easy.</p><p>That was never in the cards for you, was it? I don't know how you started your first years of life, but your life with me started abandoned, starved, and shell-shocked. You had the worry and suspicion forced into you, most likely from abuse, that remained with you for the rest of your life.</p><p>At first you just seemed like a timid creature, needing a cat 50 pounds lighter than you to show that the stairs were nothing to be afraid of. You were never able to overcome your fear of metal grates and decided the upstairs bedrooms were just not worth the risk.</p><p>As you regained your strength, your frightfulness became something frightening—you were uncontrollably violent toward other dogs. At home, you were Henry Jekyll, and to the rest of the world (at least other dog owners), Edward Hyde. All of a sudden, your world became small again and that was the second time my heart broke for you.</p><p>Severe fear aggression, the behaviorists diagnosed. You scored as an un-adoptable dog in multiple temperament tests—a dog that should have been put down at the shelter.</p><p>You dragged me reluctantly into the world of canine behavior. I'll admit, Lola, it was all a bit too much for me. I didn't sign up for this, I didn't know how or want to manage your behavioral problems. I just wanted a dog who would follow me around, fetch my slippers, and occasionally get into minor trouble. So many times I felt rejected and angry and sad and resentful as the frustration and stress wore down my body and my emotions.</p><p>But every day, no matter how high or low it was, you always showed me with some gesture that <em>yes</em>, we made it through another day, and that it was okay. I depended on you to provide me that assurance, and you never once failed me.</p><p>Two and a half years, Lola. Two and a half years of consulting with numerous behaviorists, of learning all about your species, of pre-dawn off-leash training, of stressful dog park conditioning sessions. Two and a half years of ups and downs, ups and downs. I guess during that time I earned your trust, even though I still couldn't trust you to not make undesirable decisions on your own.</p><p>On May 10, 2006, 6:15 in the morning, just as the sun was rising, we were at the beach by ourselves. It was probably your reward for a good training session. All of a sudden I heard movements behind me and it was a large male Ridgeback. I instinctively reached for the leash so I could maintain control, but you turned to check in with me (thank you), asking me what you should do. In that split second, I decided to let you go. Maybe I saw something in your eyes, maybe I was being selfish and wanted to prove that all the effort paid off—who knows, who cares, because the next moments were among the most heart-stopping, happy, breathtaking, beautiful moments I've ever experienced. You started playing with Jaaco.</p><p><em>You started playing with another dog.</em></p><p>You chased Jaaco, then you ran and swam with Jaaco's sister, (the other) Lola. You were born again, (my) Lola. Do you have any idea what this meant? Do you? This was your true birthday.</p><p>I must have seemed like a delirious person to your other human when we got home. I still have no words today to describe how relieved, how grateful, how happy. From that moment on, May 10, 2006, 6:15am, you started living a much more fulfilling life. You started <em>really</em> living.</p><p>The next many years were filled with morning walks, evening walks, dogs that you loved to play with, dogs that you avoided, long hikes that tired you out, trips to the beach where you swam (you were such a terrible swimmer). We even took a road trip where you were uncertain, then curious, then ecstatic at the snow. You opened so many places inside me that allowed me to accept not just you, but other people and experiences as well.</p><p>You were always with me to observe the changes in the water, the horizon, the skies of the bay. You never complained but you wouldn't stick around that long either. I didn't mind you wandering off but sometimes you would lose sight of me and I would see you frantically running back and forth looking for me. I would smile and whistle and you always, always came rushing back.</p><p>You were never the self-assured or independent dog. You had confidence but only if I was around. It took a very long time for me to grasp that you had your own source of confidence, it's just not one that I understand. You were so confident that no matter what, I would be there for you, I would make the right decision for you when you didn't want to, I would help you navigate this society full of rules and judgment that you couldn't understand. You had no pretense, you had no agenda, you simply said, "hey you, do your best to take good care of me."</p><p>I did try my best to take good care of you, Lola. There was nothing I could do to protect you from your own mutated cells in the end, though. By the time the oral melanoma was symptomatic, the cells had already taken over in places where surgery was impossible. By the time there was a treatment plan (radiation and immunotherapy), the vicious disease had already spread into your lymph nodes and your lungs.</p><p>The last few nights of your life, you weren't able to sleep at all. Your nostrils were stuffed up, your mouth was filled with infectious blood, and your lymph nodes had swollen to the point where you couldn't breathe unless you held your head up. Every breath sounded as if you took a step summiting Mount Everest. You were so loud that I had to leave you by yourself upstairs so I could steal a few restless hours of sleep. I had to, Lola. I hated it but I had to. I had to make sure my mind was clear so I could make the best decisions for you.</p><p>As I sat by your side, by your bed, I would listen to your labored breathing, watch your eyes slowly close, then your head drop suddenly because you were tired, so tired. You looked peaceful, as if nothing was wrong and you were just sleeping normally. I became hopeful that you could rest, just rest. However, I could see that the muscles were working but there was no air getting into your lungs.</p><p><em>One one thousand.</em></p><p><em>Two one thousand.</em></p><p>You violently jerked your head straight up to gasp for air, to stay alive.</p><p>Five more breaths. Breathless sleep. Violent gasp. <em>Over</em> and <em>over</em> and <em>over</em> and <strong>OVER</strong>.</p><p>Each time my heart squeezed tighter, my own breath taking in less air.</p><p>You also stopped eating and were barely drinking water on your own. Your lean, low-fat body mass now worked against you as your body literally consumed your muscles to sustain itself. Your eyes became sunken from severe dehydration. Your nose and mouth crusted with dry, old blood.</p><p>I felt so helpless. I couldn't provide any relief or comfort. I dutifully wiped your nose and mouth, changed your bloody bedding. All I could do was to let you know that I was there, close by.</p><p>You were so strong, Lola, so strong. Even as you were enduring unimaginable pain, you stood up to find your humans in the kitchen when we ate lunch, you plopped your head into my lap because that's how I liked you to sit with me. Maybe you did all of these things out of habit, maybe you needed to be around us just as much as we needed to be around you. All I know is you didn't have to make the effort, but you did.</p><p>I couldn't ask any more of you. You've learned to look to me to make decisions for you when you didn't want to. And here was a decision you couldn't make, but I had to. There was no right answer, there were only impossible choices. We had to let you go while you still knew that you were loved and respected. We had to let you go while you were still able to enjoy the last car ride to the vet. We had to let you go while you still had the capacity to know that we decided to end your life.</p><p>There is no way for me to know if you understood, or were capable of understanding, all my thoughts and feelings for you. In the end, it doesn't really matter for I could never truly speak your language or understand all your intentions. Whether your anthropomorphic behaviors were intuitive or instinctual, it doesn't really matter. I related to you as a soulful companion. I tried as much as I could to maintain your dogginess. You were my family.</p><p>I miss you, Lola. With time, there will come more days when I don't cry for you (for me, really) anymore. Indeed, there will be days when I won't even consciously think of you. Like all creatures, I was born, and I will die. When I do, we will have shared another experience together. We will be equals.</p>]]></content>
        <author>
            <name>George Song</name>
            <email>george@gsong.dev</email>
            <uri>https://gsong.dev/</uri>
        </author>
        <published>2014-02-09T03:00:00.000Z</published>
    </entry>
</feed>