Skip to main content

Create a country selector

Note

This guide might not be compatible with features introduced in Hydrogen version 2025-05 and above. Check the latest documentation if you encounter any issues.

In this guide you will learn how to create a country selector so that buyers can change the store language and currency.



Anchor to Step 1: Provide a list of available countriesStep 1: Provide a list of available countries

Create a JSON file with a list of available countries that will be rendered at every page. You can use the /app/data/countries.js file in the Hydrogen demo store as a point of reference.

For performance and SEO reasons, Shopify recommends using a static JSON variable for the countries. Optionally, you can create a build script that generates this file on build.

The following is an example of the countries data structure:

/app/data/countries

/app/data/countries.js

export const countries = {
default: {
language: 'EN',
country: 'US',
label: 'United States (USD $)', // Labels to be shown in the country selector
host: 'hydrogen.shop', // The host and pathPrefix are used for linking
},
'en-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (CAD $)',
host: 'ca.hydrogen.shop',
},
'fr-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (Français) (CAD $)',
host: 'ca.hydrogen.shop',
pathPrefix: '/fr',
},
'en-au': {
language: 'EN',
country: 'AU',
label: 'Australia (AUD $)',
host: 'hydrogen.au',
},
};
import type {
CountryCode,
LanguageCode,
} from '@shopify/hydrogen/storefront-api-types';

export type Locale = {
language: LanguageCode;
country: CountryCode;
label: string;
host: string;
pathPrefix?: string
};

export const countries: Record<string, Locale> = {
default: {
language: 'EN',
country: 'US',
label: 'United States (USD $)', // Labels to be shown in the country selector
host: 'hydrogen.shop', // The host and pathPrefix are used for linking
},
'en-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (CAD $)',
host: 'ca.hydrogen.shop',
},
'fr-ca': {
language: 'EN',
country: 'CA',
label: 'Canada (Français) (CAD $)',
host: 'ca.hydrogen.shop',
pathPrefix: '/fr',
},
'en-au': {
language: 'EN',
country: 'AU',
label: 'Australia (AUD $)',
host: 'hydrogen.au',
},
};

Anchor to Step 2: Create getLocaleFromRequest utilityStep 2: Create getLocaleFromRequest utility

Create the getLocaleFromRequest utility function. This function will read the request and determine the locale to be used throughout the app.

You can use the /app/lib/utils.js file in the demo Hydrogen demo store as an example.

/app/lib/utils

/app/lib/utils.js

import {countries} from '~/data/countries';

export function getLocaleFromRequest(request) {
const url = new URL(request.url);

switch (url.host) {
case 'ca.hydrogen.shop':
if (/^\/fr($|\/)/.test(url.pathname)) {
return countries['fr-ca'];
} else {
return countries['en-ca'];
}
break;
case 'hydrogen.au':
return countries['en-au'];
break;
default:
return countries['default'];
}
}
import {type Locale, countries} from '~/data/countries';

export function getLocaleFromRequest(request: Request): Locale {
const url = new URL(request.url);

switch (url.host) {
case 'ca.hydrogen.shop':
if (/^\/fr($|\/)/.test(url.pathname)) {
return countries['fr-ca'];
} else {
return countries['en-ca'];
}
break;
case 'hydrogen.au':
return countries['en-au'];
break;
default:
return countries['default'];
}
}

Anchor to Step 3: Add the selected locale in the ,[object Object], loader functionStep 3: Add the selected locale in the root loader function

This step gets the user's request and finds the associated locale. You should make the selected locale available throughout the app with the loader.

/app/root

/app/root.jsx

export async function loader({context, request}) {
...
return defer({
...,
selectedLocale: await getLocaleFromRequest(request),
});
}
export async function loader({context, request}: LoaderArgs) {
...
return defer({
...,
selectedLocale: await getLocaleFromRequest(request),
});
}

Anchor to Step 4: Create a resource route for the available countriesStep 4: Create a resource route for the available countries

A Remix resource route is useful when the UI fetches the available countries to display.

You can use the /routes/($locale).api.countries.js in the The Hydrogen demo store as an example.

/routes/($locale).api.countries.js

/routes/($locale).api.countries.js

import {json} from '@remix-run/server-runtime';
import {CacheLong, generateCacheControlHeader} from '@shopify/hydrogen';
import {countries} from '~/data/countries';

export async function loader() {
return json(
{...countries},
{headers: {'cache-control': generateCacheControlHeader(CacheLong())}},
);
}

// no-op
export default function CountriesResourceRoute() {
return null;
}
import {json} from '@remix-run/server-runtime';
import {CacheLong, generateCacheControlHeader} from '@shopify/hydrogen';
import {countries} from '~/data/countries';

export async function loader() {
return json(
{...countries},
{headers: {'cache-control': generateCacheControlHeader(CacheLong())}},
);
}

// no-op
export default function CountriesResourceRoute() {
return null;
}

Anchor to Step 5: Render the available countries as a formStep 5: Render the available countries as a form

Create a CountrySelector component using Remix Forms:

You can use the app/components/CountrySelector.js file in the the Hydrogen demo store as an example.

/app/components/CountrySelector

/app/components/CountrySelector.jsx

import {Form, useMatches, useLocation, useFetcher} from '@remix-run/react';
import {useEffect, useState} from 'react';

export function CountrySelector() {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
const {pathname, search} = useLocation();

const [countries, setCountries] = useState({});

// Get available countries list
const fetcher = useFetcher();
useEffect(() => {
if (!fetcher.data) {
fetcher.load('/api/countries');
return;
}
setCountries(fetcher.data);
}, [countries, fetcher.data]);

const strippedPathname = pathname.replace(selectedLocale.pathPrefix, '');

return (
<details>
<summary>{selectedLocale.label}</summary>
<div className="overflow-auto border-t py-2 bg-contrast w-full max-h-36">
{countries &&
Object.keys(countries).map((countryKey) => {
const locale = countries[countryKey];
const hreflang = `${locale.language}-${locale.country}`;

return (
<Form method="post" action="/locale" key={hreflang}>
<input type="hidden" name="language" value={locale.language} />
<input type="hidden" name="country" value={locale.country} />
<input
type="hidden"
name="path"
value={`${strippedPathname}${search}`}
/>
<button type="submit">{locale.label}</button>
</Form>
);
})}
</div>
</details>
);
}
import {Form, useMatches, useLocation, useFetcher} from '@remix-run/react';
import {useEffect, useState} from 'react';
import type {Locale} from '~/data/countries';

export function CountrySelector() {
const [root] = useMatches();
const selectedLocale = root.data.selectedLocale;
const {pathname, search} = useLocation();

const [countries, setCountries] = useState<Record<string, Locale>>({});

// Get available countries list
const fetcher = useFetcher();
useEffect(() => {
if (!fetcher.data) {
fetcher.load('/api/countries');
return;
}
setCountries(fetcher.data);
}, [countries, fetcher.data]);

const strippedPathname = pathname.replace(selectedLocale.pathPrefix, '');

return (
<details>
<summary>{selectedLocale.label}</summary>
<div className="overflow-auto border-t py-2 bg-contrast w-full max-h-36">
{countries &&
Object.keys(countries).map((countryKey) => {
const locale = countries[countryKey];
const hreflang = `${locale.language}-${locale.country}`;

return (
<Form method="post" action="/locale" key={hreflang}>
<input type="hidden" name="language" value={locale.language} />
<input type="hidden" name="country" value={locale.country} />
<input
type="hidden"
name="path"
value={`${strippedPathname}${search}`}
/>
<button type="submit">{locale.label}</button>
</Form>
);
})}
</div>
</details>
);
}

Anchor to Step 6: Handle form submitStep 6: Handle form submit

Create the /app/routes/($locale).jsx route that will handle the form submit action

/app/routes/($locale)

/app/routes/($locale).jsx

import {redirect} from '@shopify/remix-oxygen';
import invariant from 'tiny-invariant';
import {countries} from '~/data/countries';

export const action = async ({request, context}) => {
const {session} = context;
const formData = await request.formData();

// Make sure the form request is valid
const languageCode = formData.get('language');
invariant(languageCode, 'Missing language');

const countryCode = formData.get('country');
invariant(countryCode, 'Missing country');

// determine where to redirect to relative to where user navigated from
// ie. hydrogen.shop/collections -> ca.hydrogen.shop/collections
const path = formData.get('path');
const toLocale = countries[`${languageCode}-${countryCode}`.toLowerCase()];

const cartId = await session.get('cartId');

// Update cart buyer's country code if there is a cart id
if (cartId) {
await updateCartBuyerIdentity(context, {
cartId,
buyerIdentity: {
countryCode,
},
});
}

const redirectUrl = new URL(
`${toLocale.pathPrefix || ''}${path}`,
`https://${toLocale.host}`
);

return redirect(redirectUrl, 302);
};

async function updateCartBuyerIdentity({storefront}, {cartId, buyerIdentity}) {
const data = await storefront.mutate<{
cartBuyerIdentityUpdate: {cart};
}>(UPDATE_CART_BUYER_COUNTRY, {
variables: {
cartId,
buyerIdentity,
},
});

const UPDATE_CART_BUYER_COUNTRY = `#graphql
mutation CartBuyerIdentityUpdate(
$cartId: ID!
$buyerIdentity: CartBuyerIdentityInput!
) {
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
cart {
id
}
}
}
`;
import type {
CountryCode,
LanguageCode,
CartBuyerIdentityInput,
Cart,
} from '@shopify/hydrogen/storefront-api-types';
import {redirect, type AppLoadContext, type ActionFunction} from '@shopify/remix-oxygen';
import invariant from 'tiny-invariant';
import {countries} from '~/data/countries';

export const action: ActionFunction = async ({request, context}) => {
const {session} = context;
const formData = await request.formData();

// Make sure the form request is valid
const languageCode = formData.get('language') as LanguageCode;
invariant(languageCode, 'Missing language');

const countryCode = formData.get('country') as CountryCode;
invariant(countryCode, 'Missing country');

// Determine where to redirect to relative to where user navigated from
// ie. hydrogen.shop/collections -> ca.hydrogen.shop/collections
const path = formData.get('path');
const toLocale = countries[`${languageCode}-${countryCode}`.toLowerCase()];

const cartId = await session.get('cartId');

// Update cart buyer's country code if there is a cart id
if (cartId) {
await updateCartBuyerIdentity(context, {
cartId,
buyerIdentity: {
countryCode,
},
});
}

const redirectUrl = new URL(
`${toLocale.pathPrefix || ''}${path}`,
`https://${toLocale.host}`,
).toString();

return redirect(redirectUrl, 302);
};

async function updateCartBuyerIdentity(
{storefront}: AppLoadContext,
{
cartId,
buyerIdentity,
}: {
cartId: string;
buyerIdentity: CartBuyerIdentityInput;
},
) {
const data = await storefront.mutate<{
cartBuyerIdentityUpdate: {cart: Cart};
}>(UPDATE_CART_BUYER_COUNTRY, {
variables: {
cartId,
buyerIdentity,
},
});

invariant(data, 'No data returned from Shopify API');

return data.cartBuyerIdentityUpdate.cart;
}

const UPDATE_CART_BUYER_COUNTRY = `#graphql
mutation CartBuyerIdentityUpdate(
$cartId: ID!
$buyerIdentity: CartBuyerIdentityInput!
) {
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
cart {
id
}
}
}
`;

Anchor to Step 7: Make sure re-rendering happens at the root HTMLStep 7: Make sure re-rendering happens at the root HTML

Make sure to provide a key to the components that will change due to localization. Specially for URL path localization schemes.

Sometimes, React won't know when to re-render a component. To avoid this problem, add localization as key in the App.

/app/root

/app/root.jsx

export default function App() {
const data = useLoaderData();
const locale = data.selectedLocale;

return (
<html lang={locale.language}>
<head>
<Seo />
<Meta />
<Links />
</head>
<body>
<Layout
layout={data.layout}
key={`${locale.language}-${locale.country}`} . // key by hreflang
>
<Outlet />
</Layout>
<Debugger />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
const data = useLoaderData<typeof loader>();
const locale = data.selectedLocale;

return (
<html lang={locale.language}>
<head>
<Seo />
<Meta />
<Links />
</head>
<body>
<Layout
layout={data.layout as LayoutData}
key={`${locale.language}-${locale.country}`} . // key by hreflang
>
<Outlet />
</Layout>
<Debugger />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

Was this page helpful?