Generating TypeScript Code for a Dynamic Country Flag React Component

Generating TypeScript Code for a Dynamic Country Flag React Component

November 5, 2023

14 min read

Generating TypeScript Code for a Dynamic Country Flag React Component
Watch on YouTube

Generating TypeScript Functions for Enhanced Codebases

In this post, we'll dive into an intriguing topic: creating TypeScript functions that will enhance our codebase by generating TypeScript code. If you've ever worked with type generation from a GraphQL or database schema, you'll appreciate the convenience of these auto-generated types. Imagine having to manually write them each time the schema changes! While there might not always be a library tailored to our specific needs, fear not. Crafting our own code generator is straightforward. Rather than addressing a hypothetical issue, I'll guide you through a real use-case I encountered. The final demonstration is available on this demo page, and you can access the complete source code in my RadzionKit repository.

countries
countries

Crafting a User-friendly Country Selector in Increaser

Here's the challenge: I run a productivity app called Increaser. In it, users can set up a public profile that displays their country. We record the country in our database using the ISO 3166-1 alpha-2 standard—a two-letter country code. But on the front-end, we need a user-friendly way for individuals to select their country. This means having a map that links each country code to its full name. Moreover, we aim to enhance the user interface by showing a country flag next to the user's name. Thus, we need a dynamic React component that can display the correct flag based on the provided country code.

Leveraging Code Generation for Improved Component Reusability

We already possess a JSON file containing all the country codes and names, along with a directory filled with SVGs of the country flags. We could directly utilize the JSON, but that wouldn't be ideal. Furthermore, while we could host the SVGs on a CDN, this approach would compromise the reusability of our component. Why? Because any consumer of the component would then need to be concerned about the hosting location of the SVGs. This is where the power of code generation truly shines.

Transforming JSON Data into a TypeScript File

Let's tackle the most straightforward task first. We'll transform the JSON file into a TypeScript file. This file should:

  1. Export a record containing all the country codes and names.
  2. Define a CountryCode type, which will be a union of all the country codes.
  3. Offer a list of all these country codes.

Given our requirements, the generated TypeScript file will look like this:

// This file is generated by @radzionkit/utils/countries/codegen/generateCountries.ts
export const countryNameRecord = {
  AF: "Afghanistan",
  AL: "Albania",
  // ...
  ZM: "Zambia",
  ZW: "Zimbabwe",
} as const

export type CountryCode = keyof typeof countryNameRecord

export const countryCodes = Object.keys(countryNameRecord) as CountryCode[]

Integrating the Code Generation Script into Our Monorepo Workspace

Our workspace is structured as a monorepo. Within this setup, the countries data resides in the utils package, specifically under the countries directory. The generateCountries script from the codegen directory produces an index.ts file. To execute this script, we can either use the command npx tsx countries/codegen/generateCountries directly from the terminal or incorporate it into the package.json scripts section for ease of use. This allows you to simply run yarn generateCountries whenever you need to generate or update the country data.

  "scripts": {
    "generateCountries": "npx tsx countries/codegen/generateCountries"
  },

Breaking Down the generateCountries Script Functionality

In the generateCountries file, we read from the countries.json file. This file contains country codes and names and is situated in the same directory as the script. We first organize blocks of code and separate them with two newline characters. The initial block establishes the countryNameRecord object as a constant. The subsequent block determines the CountryCode type, and the final block assembles the countryCodes array. In the end, we invoke the createTsFile function, tasked with writing the file.

import path from "path"
import fs from "fs"
import { createTsFile } from "@radzionkit/codegen/utils/createTsFile"

const generateCountries = async () => {
  const countryNameRecord = JSON.parse(
    fs.readFileSync(path.resolve(__dirname, "./countries.json"), "utf8")
  )

  const content = [
    `export const countryNameRecord = ${JSON.stringify(
      countryNameRecord
    )} as const`,
    `export type CountryCode = keyof typeof countryNameRecord`,
    `export const countryCodes = Object.keys(countryNameRecord) as CountryCode[]`,
  ].join("\n\n")

  await createTsFile({
    directory: path.resolve(__dirname, "../"),
    fileName: "index",
    content,
    generatedBy: "@radzionkit/utils/countries/codegen/generateCountries.ts",
  })
}

generateCountries()

Understanding the createTsFile Utility Function in the Codegen Package

The createTsFile function is designed for reusability across multiple packages in our monorepo. As a result, we've isolated it within a dedicated package named codegen. This function accepts several parameters:

  • extension - specifies the file extension, defaulting to ts.
  • directory - indicates the directory where the file will be stored.
  • fileName - names the file.
  • content - outlines the content for the file.
  • generatedBy - records the script responsible for generating the file.

Upon execution, the function first checks and creates the necessary directory if it's missing. Subsequently, it retrieves the Prettier configuration from the project's root to ensure the content is formatted consistently with the existing codebase. A comment, indicating the source of the generated code, is added at the beginning of the file. Ultimately, the content is written into the designated directory.

import fs from "fs"
import { format, resolveConfig } from "prettier"
import path from "path"

interface CreateTsFileParams {
  extension?: "ts" | "tsx"
  directory: string
  fileName: string
  generatedBy: string
  content: string
}

export const createTsFile = async ({
  extension = "ts",
  directory,
  fileName,
  generatedBy,
  content,
}: CreateTsFileParams) => {
  fs.mkdirSync(directory, { recursive: true })

  const configPath = path.resolve(__dirname, "../../.prettierrc")

  const config = await resolveConfig(configPath)

  const formattedContent = await format(
    [`// This file is generated by ${generatedBy}`, content].join("\n"),
    {
      ...config,
      parser: "typescript",
    }
  )

  const tsFilePath = `${directory}/${fileName}.${extension}`

  fs.writeFileSync(tsFilePath, formattedContent)
}

Generating Dynamic Flag Components for Each Country

Let's move on to generating components, a task that may seem complex but becomes more manageable when broken down. First, we'll create individual components for each country's flag. Following that, we'll design a master component that renders the appropriate flag based on a given country code. Here's an example of the component for the Italian flag:

// This file is generated by @radzionkit/ui/country/codegen/generateFlags.ts
import { SvgIconProps } from "@radzionkit/ui/icons/SvgIconProps"

const ItFlag = (props: SvgIconProps) => (
  <svg
    {...props}
    width="1em"
    height="0.75em"
    xmlns="http://www.w3.org/2000/svg"
    id="flag-icons-it"
    viewBox="0 0 640 480"
    {...props}
  >
    <g fillRule="evenodd" strokeWidth="1pt">
      <path fill="#fff" d="M0 0h640v480H0z" />
      <path fill="#009246" d="M0 0h213.3v480H0z" />
      <path fill="#ce2b37" d="M426.7 0H640v480H426.7z" />
    </g>
  </svg>
)

export default ItFlag

In our system, when creating SVG icon components, we leverage the SvgIconProps type from the @radzionkit/ui/icons directory. This type is a modified version of React's SVGProps, with the "ref" property excluded.

import { SVGProps } from "react"

export type SvgIconProps = Omit<SVGProps<SVGSVGElement>, "ref">

Converting SVGs into Customized React Components with svgToReact

Before diving into the script, it's crucial to understand a foundational function: the one that converts an SVG string into a React component string. The heavy lifting within the svgToReact function is primarily executed by the transform function from the @svgr/core package. However, additional processing is required to finalize the component.

Our aim is to set the component size using the font-size attribute, which can be inherited from its parent. We also need to ensure that the SVG does not exceed its boundaries, meaning the larger dimension of the SVG should match the font-size. To realize this, we first extract the SVG's size from its viewBox attribute. Following that, the normalizeToMaxDimension function from RadzionKit helps us determine the dimensions in em units.

Although SVGR yields a component, our goal is to enhance it. We want this component to accept the SvgIconProps we previously discussed. Hence, we utilize a regular expression to isolate the SVG content and then embed it within our customized component.

import { Dimensions } from "@radzionkit/utils/entities/Dimensions"
import { normalizeToMaxDimension } from "@radzionkit/utils/normalizeToMaxDimension"
import { shouldBeDefined } from "@radzionkit/utils/shouldBeDefined"
import { transform } from "@svgr/core"

const getSvgDimensions = (svg: string): Dimensions => {
  const viewBoxMatch = svg.match(/viewBox="([^"]+)"/)

  if (!viewBoxMatch) {
    throw new Error("SVG does not have a viewBox attribute.")
  }

  const [, viewBoxValues] = viewBoxMatch
  const [, , width, height] = viewBoxValues.split(" ").map(parseFloat)

  return { width, height }
}

const extractSvg = (input: string) => {
  const regex = /<svg[\s\S]*?<\/svg>/
  const match = input.match(regex) || undefined
  return shouldBeDefined(match)[0]
}

interface SvgToReactParams {
  svg: string
  componentName: string
}

export const svgToReact = async ({ svg, componentName }: SvgToReactParams) => {
  const svgComponent = await transform(
    svg,
    {
      plugins: ["@svgr/plugin-jsx"],
    },
    { componentName: "MyComponent" }
  )

  const { width, height } = normalizeToMaxDimension(getSvgDimensions(svg))

  const cleanedSvg = extractSvg(svgComponent)
    .replace(/\s*width="[^"]*"/g, "")
    .replace(/\s*height="[^"]*"/g, "")
    .replace("svg", `svg width="${width}em" height="${height}em"`)
    .replace("svg", "svg {...props}")

  return [
    `import { SvgIconProps } from '@radzionkit/ui/icons/SvgIconProps'`,
    `const ${componentName} = (props: SvgIconProps) => ${cleanedSvg}`,
    `export default ${componentName}`,
  ].join("\n\n")
}

Streamlining Flag Component Generation with the generateFlags Script

Having established the essential svgToReact function, our attention can now shift to the generateFlags script. Situated in the countries/codegen directory, it bears a resemblance to the generateCountries function we explored earlier, but is housed within the ui package.

import { CountryCode, countryCodes } from "@radzionkit/utils/countries"
import { makeRecord } from "@radzionkit/utils/makeRecord"
import fs from "fs"
import path from "path"
import { capitalizeFirstLetter } from "@radzionkit/utils/capitalizeFirstLetter"
import { createTsFile } from "@radzionkit/codegen/utils/createTsFile"
import { svgToReact } from "../../codegen/svgToReact"

const getSvgFlagPath = (code: CountryCode) =>
  path.resolve(__dirname, "./flags", `${code.toLowerCase()}.svg`)

const flagsPath = path.resolve(__dirname, "../flags")

const getFlagComponentName = (code: CountryCode) =>
  `${capitalizeFirstLetter(code.toLowerCase())}Flag`

const generateFlags = async () => {
  const svgRecord = makeRecord(countryCodes, (code) =>
    fs.readFileSync(getSvgFlagPath(code), "utf8")
  )

  const generatedBy = "@radzionkit/ui/country/codegen/generateFlags.ts"

  await Promise.all(
    countryCodes.map(async (code) => {
      const svg = svgRecord[code]
      const content = await svgToReact({
        svg,
        componentName: getFlagComponentName(code),
      })

      return createTsFile({
        extension: "tsx",
        directory: flagsPath,
        fileName: getFlagComponentName(code as CountryCode),
        content,
        generatedBy,
      })
    })
  )

  // ...
}

generateFlags()

To start, we need to create a mapping between country codes and their associated SVGs. This can be achieved with the makeRecord function from RadzionKit. This utility aids in constructing the record by accepting an array of keys and a function which, given a key, outputs its corresponding value.

export const makeRecord = <T extends string, V>(
  keys: T[],
  getValue: (key: T) => V
) => {
  const record: Record<T, V> = {} as Record<T, V>

  keys.forEach((key) => {
    record[key] = getValue(key)
  })

  return record
}

We loop through the country codes and create a component for each. We use the svgToReact function to transform the SVG into a React component. After that, the createTsFile function writes the component to the file system.

Next, we need to create a dynamic component that displays the appropriate flag based on the given country code. This component should have a reference to all country codes and their corresponding components. However, to optimize performance, we shouldn't load every component simultaneously. Using the next/dynamic package, we can load them as needed. If a specific flag component isn't immediately available, we'd like to display CountryFlagFallback. But here's a challenge: we cannot forward the props passed to the dynamically loaded component to the fallback one. To address this, we leverage React's context.

// This file is generated by @radzionkit/ui/country/codegen/generateFlags.ts
import dynamic from "next/dynamic"
import { SvgIconProps } from "../../icons/SvgIconProps"
import { ComponentType } from "react"
import { CountryCode } from "@radzionkit/utils/countries"
import {
  CountryFlagDynamicFallback,
  CountryFlagFallbackPropsProvider,
} from "../CountryFlagDynamicFallback"

const countryFlagRecord: Record<CountryCode, ComponentType<SvgIconProps>> = {
  AF: dynamic(() => import("./AfFlag"), {
    ssr: false,
    loading: () => <CountryFlagDynamicFallback />,
  }),
  AL: dynamic(() => import("./AlFlag"), {
    ssr: false,
    loading: () => <CountryFlagDynamicFallback />,
  }),
  // ...
  ZM: dynamic(() => import("./ZmFlag"), {
    ssr: false,
    loading: () => <CountryFlagDynamicFallback />,
  }),
  ZW: dynamic(() => import("./ZwFlag"), {
    ssr: false,
    loading: () => <CountryFlagDynamicFallback />,
  }),
}

interface CountryFlagProps extends SvgIconProps {
  code: CountryCode
}

export const CountryFlag = (props: CountryFlagProps) => {
  const Component = countryFlagRecord[props.code]
  return (
    <CountryFlagFallbackPropsProvider value={props}>
      <Component {...props} />
    </CountryFlagFallbackPropsProvider>
  )
}

export default CountryFlag

A common scenario in React development is having a provider that only deals with a single value. To simplify this, I've developed a helper named getValueProviderSetup. This utility streamlines the creation of such providers, and you can view it in the RadzionKit repository.

import { getValueProviderSetup } from "../state/getValueProviderSetup"
import {
  CountryFlagFallback,
  CountryFlagFallbackProps,
} from "./CountryFlagFallback"

const {
  useValue: useCountryFlagFallbackProps,
  provider: CountryFlagFallbackPropsProvider,
} = getValueProviderSetup<CountryFlagFallbackProps>("CountryFlagFallbackProps")

export const CountryFlagDynamicFallback = () => {
  const props = useCountryFlagFallbackProps()

  return <CountryFlagFallback {...props} />
}

export { CountryFlagFallbackPropsProvider }

The fallback component is designed to display the country code centered within a rectangle. This rectangle maintains the same aspect ratio as our flag icon, ensuring there's no visual shift or jump when the actual flag loads.

import { CountryCode } from "@radzionkit/utils/countries"
import styled from "styled-components"
import { getColor } from "../theme/getters"
import { CountryFlagFrame } from "./CountryFlagFrame"
import { SvgIconProps } from "../icons/SvgIconProps"

export interface CountryFlagFallbackProps extends SvgIconProps {
  code: CountryCode
}

const Container = styled(CountryFlagFrame)`
  text {
    fill: ${getColor("textSupporting")};
    font-size: 0.4em;
  }
`

export const CountryFlagFallback = ({
  code,
  ...props
}: CountryFlagFallbackProps) => (
  <Container {...props}>
    <text x="50%" y="55%" textAnchor="middle" dominantBaseline="middle">
      {code}
    </text>
  </Container>
)

Let's circle back to our primary focus, the generateFlags script. First, we outline all necessary imports. Subsequently, for each country code, we dynamically generate a component. Finally, we craft the CountryFlag component, which leverages the CountryFlagFallbackPropsProvider to relay the props to our fallback component.

const imports = [
  `import dynamic from 'next/dynamic'`,
  `import { SvgIconProps } from '../../icons/SvgIconProps'`,
  `import { ComponentType } from 'react'`,
  `import { CountryCode } from '@radzionkit/utils/countries'`,
  `import { CountryFlagDynamicFallback, CountryFlagFallbackPropsProvider } from '../CountryFlagDynamicFallback'`,
].join("\n")

const countryFlagComponentRecord = makeRecord(countryCodes, (code) => {
  const componentName = getFlagComponentName(code)
  return `dynamic(() => import('./${componentName}'), { ssr: false, loading: () => <CountryFlagDynamicFallback /> })`
})

const content = [
  imports,
  `const countryFlagRecord: Record<CountryCode, ComponentType<SvgIconProps>> = {
      ${Object.entries(countryFlagComponentRecord)
        .map(([key, value]) => {
          return `${key}: ${value}`
        })
        .join(",")}
    }`,
  `interface CountryFlagProps extends SvgIconProps { code: CountryCode }`,
  `export const CountryFlag = (props: CountryFlagProps) => {
      const Component = countryFlagRecord[props.code]
      return (
        <CountryFlagFallbackPropsProvider value={props}>
          <Component {...props} />
        </CountryFlagFallbackPropsProvider>
      )
    }`,
  `export default CountryFlag`,
].join("\n\n")

await createTsFile({
  extension: "tsx",
  directory: flagsPath,
  fileName: "CountryFlag",
  content,
  generatedBy,
})

Demonstrating the Flag Components on RadzionKit's /flag Page

To showcase our solution, there's a /flag page on RadzionKit. This page displays a comprehensive list of countries alongside their flags. The TabNavigation component facilitates switching between viewing flags as SVGs or emojis. We use the countryCodes list to loop through all countries, and the countryNameRecord assists in displaying each country's name.

import { DemoPage } from "components/DemoPage"
import { useState } from "react"
import { TabNavigation } from "@radzionkit/ui/navigation/TabNavigation"
import { capitalizeFirstLetter } from "@radzionkit/utils/capitalizeFirstLetter"
import { HStack, VStack } from "@radzionkit/ui/layout/Stack"
import { countryCodes, countryNameRecord } from "@radzionkit/utils/countries"
import { Match } from "@radzionkit/ui/base/Match"
import { Text } from "@radzionkit/ui/text"
import { CountryFlag } from "@radzionkit/ui/countries/flags/CountryFlag"
import { CountryFlagEmoji } from "@radzionkit/ui/countries/CountryFlagEmoji"
import { makeDemoPage } from "layout/makeDemoPage"
import { SameWidthChildrenRow } from "@radzionkit/ui/layout/SameWidthChildrenRow"
import { IconWrapper } from "@radzionkit/ui/icons/IconWrapper"

const views = ["svg", "emoji"] as const
type View = (typeof views)[number]

export default makeDemoPage(() => {
  const [activeView, setActiveView] = useState<View>("svg")

  return (
    <DemoPage youtubeVideoId="s3ve27fqORk" title="Country flag">
      <VStack fullWidth gap={40}>
        <TabNavigation
          views={views}
          getViewName={capitalizeFirstLetter}
          activeView={activeView}
          onSelect={setActiveView}
          groupName="flags"
        />
        <SameWidthChildrenRow childrenWidth={240} gap={20}>
          {countryCodes.map((code) => (
            <HStack key={code} alignItems="center" gap={12}>
              <Text height="small" size={24} color="contrast">
                <Match
                  value={activeView}
                  emoji={() => <CountryFlagEmoji code={code} />}
                  svg={() => (
                    <IconWrapper style={{ borderRadius: 4 }}>
                      <CountryFlag code={code} />
                    </IconWrapper>
                  )}
                />
              </Text>
              <Text size={14}>{countryNameRecord[code]}</Text>
            </HStack>
          ))}
        </SameWidthChildrenRow>
      </VStack>
    </DemoPage>
  )
})

The Match component from RadzionKit ensures that the appropriate content is rendered based on the active tab selection. To bestow a border radius upon the CountryFlag, we nest it inside the IconWrapper component. This component, a styled span element, is designed to fit its content perfectly, and it conceals any overflow.

import styled from "styled-components"

export const IconWrapper = styled.span`
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  width: fit-content;
  height: fit-content;
  overflow: hidden;
`

Emojis vs SVGs: Choosing the Right Flag Representation

While it might have been simpler to use emojis for flags, they come with design considerations, such as the waving shape on Mac, which might not be suitable for all UIs. For those seeking a flag representation using emojis, the provided function transforms a country code into its corresponding flag emoji.

import { CountryCode } from "."

export const getCountryFlagEmoji = (countryCode: CountryCode) => {
  const codePoints = countryCode
    .toUpperCase()
    .split("")
    .map((char) => 127397 + char.charCodeAt(0))
  return String.fromCodePoint(...codePoints)
}