Generating TypeScript Code for a Dynamic Country Flag React Component
Tags: typescript, codegeneration, reactcomponent, countryflags
Published: 2023-11-06 07:06:08

{% embed https://youtu.be/_z_kAB5LRgM %}

🐙 [GitHub](https://github.com/radzionc/reactkit)  |  🎮 [Demo](https://reactkit.radzion.com/flag)

## 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](https://reactkit.radzion.com/flag), and you can access the complete source code in my [ReactKit](https://github.com/radzionc/reactkit) repository.

![countries](https://cdn-images-1.medium.com/proxy/1*pnWKVOgH0Yp3OmSeeAUCrQ.jpeg)

## Crafting a User-friendly Country Selector in Increaser

Here's the challenge: I run a productivity app called [Increaser](https://increaser.org/). 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:

```ts
// This file is generated by @reactkit/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.

```json
  "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.

```ts
import path from "path"
import fs from "fs"
import { createTsFile } from "@reactkit/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: "@reactkit/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.

```tsx
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:

```tsx
// This file is generated by @reactkit/ui/country/codegen/generateFlags.ts
import { SvgIconProps } from "@reactkit/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 `@reactkit/ui/icons` directory. This type is a modified version of React's `SVGProps`, with the "ref" property excluded.

```tsx
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 [ReactKit](https://github.com/radzionc/reactkit) 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.

```tsx
import { Dimensions } from "@reactkit/utils/entities/Dimensions"
import { normalizeToMaxDimension } from "@reactkit/utils/normalizeToMaxDimension"
import { shouldBeDefined } from "@reactkit/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 '@reactkit/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.

```tsx
import { CountryCode, countryCodes } from "@reactkit/utils/countries"
import { makeRecord } from "@reactkit/utils/makeRecord"
import fs from "fs"
import path from "path"
import { capitalizeFirstLetter } from "@reactkit/utils/capitalizeFirstLetter"
import { createTsFile } from "@reactkit/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 = "@reactkit/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 [ReactKit](https://github.com/radzionc/reactkit). This utility aids in constructing the record by accepting an array of keys and a function which, given a key, outputs its corresponding value.

```ts
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.

```tsx
// This file is generated by @reactkit/ui/country/codegen/generateFlags.ts
import dynamic from "next/dynamic"
import { SvgIconProps } from "../../icons/SvgIconProps"
import { ComponentType } from "react"
import { CountryCode } from "@reactkit/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 [ReactKit](https://github.com/radzionc/reactkit) repository.

```tsx
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.

```tsx
import { CountryCode } from "@reactkit/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.

```tsx
const imports = [
  `import dynamic from 'next/dynamic'`,
  `import { SvgIconProps } from '../../icons/SvgIconProps'`,
  `import { ComponentType } from 'react'`,
  `import { CountryCode } from '@reactkit/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 ReactKit's `/flag` Page

To showcase our solution, there's a `/flag` page on [ReactKit](https://github.com/radzionc/reactkit). 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.

```tsx
import { DemoPage } from "components/DemoPage"
import { useState } from "react"
import { TabNavigation } from "@reactkit/ui/navigation/TabNavigation"
import { capitalizeFirstLetter } from "@reactkit/utils/capitalizeFirstLetter"
import { HStack, VStack } from "@reactkit/ui/layout/Stack"
import { countryCodes, countryNameRecord } from "@reactkit/utils/countries"
import { Match } from "@reactkit/ui/base/Match"
import { Text } from "@reactkit/ui/text"
import { CountryFlag } from "@reactkit/ui/countries/flags/CountryFlag"
import { CountryFlagEmoji } from "@reactkit/ui/countries/CountryFlagEmoji"
import { makeDemoPage } from "layout/makeDemoPage"
import { SameWidthChildrenRow } from "@reactkit/ui/layout/SameWidthChildrenRow"
import { IconWrapper } from "@reactkit/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 [ReactKit](https://github.com/radzionc/reactkit) 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.

```tsx
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.

```ts
import { CountryCode } from "."

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