iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🕰

Handling Time Zones in React SSG Sites: Pitfalls and Solutions

に公開

When handling time in Gatsby's GraphQL using formatString, the date and time may not be displayed correctly depending on the user's timezone. This article explains how to display the correct date while handling timezones.

While I'll introduce examples using Gatsby, similar solutions can be applied to other React frameworks as well.

(I am using Gatsby v5)

Background

In my personal blog on a Gatsby-based portfolio site, I handle creation and update dates, but I struggled with timezone considerations.

https://github.com/bicstone/portfolio

For example, consider the following schema:

type BlogPost {
  created: DateTime! # "1960-01-01T00:00+00:00"
}

query Query {
  allBlogPost: [BlogPost!]!
}

Trying to format with Gatsby's GraphQL

First, when handling DateTime in Gatsby's GraphQL, I learned that a convenient function called formatString can be used, so I decided to try it.

https://www.gatsbyjs.com/docs/graphql-reference/

query Query {
  allBlogPost {
    a: created
    b: created(formatString: "YYYY-MM-DD HH:mm:ss")
  }
}

What do you think the result of this will be?

It looks like this:

{
  "a": "1960-01-01T00:00+09:00",
  "b": "1959-12-31 15:00:00"
}

formatString returns the time in UTC, regardless of the build environment or user environment. Also, you cannot specify a timezone.

It seems convenient since it is completed within GraphQL, but it is not usable unless UTC is acceptable.

Retrieving with Timezone and Formatting in JS

Therefore, I decided to let GraphQL handle only data retrieval and format it in JS.

I'll use date-fns as a library. (Confirmed with version 2.29.3)

https://github.com/date-fns/date-fns

I created a utility function like this.

formatDateTime.ts
import { format as formatFn, isValid, parseISO } from "date-fns";

export const formatDateTime = (value: string, format: string): string => {
  const parsedDate = parseISO(value);

  if (!isValid(parsedDate)) return "";

  return formatFn(parsedDate, format);
};

It receives a date/time in ISO format, formats it according to the local timezone, and returns it.

formatDateTime(post.created, "yyyy/MM/dd HH:mm:ss"); // "1960/01/01 00:00:00"

By passing it through this utility function, I was able to successfully display the date and time adjusted to the user's timezone.

However, there was a problem: if the timezone of the build environment and the user's browsing environment differed, it would lose consistency with the built HTML, leading to a hydration error.

Attempting to suppress hydration error

First, I considered how to suppress the hydration error.

In React, you can suppress hydration errors by using suppressHydrationWarning.

https://beta.reactjs.org/reference/react-dom/client/hydrateRoot#suppressing-unavoidable-hydration-mismatch-errors

I decided to put null in the built HTML and return the component only when the client side renders.

<div suppressHydrationWarning>
  {typeof window === "undefined" ? null : (
    <time dateTime={post.created}>{createdDate}</time>
  )}
</div>

The hydration error was suppressed. However, this approach carries the risk that the component might not be displayed if the client-side hydration is delayed.

Since it was a component displayed at the top of the blog, I was particularly concerned about the flickering, so I could not adopt this method.

Fixing the Timezone

Since this personal blog is one where assuming the JST timezone causes no issues, I decided to fix it to JST as a last resort.

I'll use date-fns-tz as the library for fixing the timezone. (Confirmed with version 1.3.7)

https://www.npmjs.com/package/date-fns-tz

I rewrote the previous utility function as follows:

formatDateTime.ts
import { isValid } from "date-fns";
import { format as formatFn, utcToZonedTime } from "date-fns-tz";
import { ja } from "date-fns/locale";

const timeZone = "Asia/Tokyo";

export const formatDateTime = (value: string, format: string): string => {
  const parsedDate = utcToZonedTime(value, timeZone);

  if (!isValid(parsedDate)) return "";

  return formatFn(parsedDate, format, { locale: ja, timeZone });
};

With this method, HTML formatted in JST is generated regardless of the build environment. No hydration error occurs even without suppression, and the component is displayed even if hydration is delayed, so no flickering occurs.

Summary

I was able to display the correct date and time without depending on the client's environment by receiving a date/time with a timezone and processing it. I will keep in mind that some processing is necessary when receiving dates with timezones (note to self).

Also, if you are planning to build a Gatsby blog, please feel free to refer to my personal blog's implementation.

https://github.com/bicstone/portfolio

(Bonus) Unit Testing with Different Timezones

When you want to fix the timezone in unit tests, it's easy to think that changing process.env.TZ would be sufficient, but it seems it doesn't work unless the timezone is set before the Node process starts.

https://github.com/nodejs/node/issues/3449

Therefore, it is necessary to set the environment variable TZ via a task runner before calling Jest.

(I am using cross-env to avoid dependency on any specific platform.)

package.json
  "scripts": {
    "test": "jest",
    "test:vietnam": "cross-env TZ=\"Asia/Ho_Chi_Minh\" npm run test"
  },

Discussion