dayjs.extend(localizedFormat)はどこで呼ぶのが正解か

  • Day.js
  • i18next
  • React

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

何を作っていたか

Reduxで持ってるロケール(locale)が変わったら、i18nextの言語切り替えとdayjsのロケール切り替えを両方やってくれるカスタムフックを書いてた。

function useLocale() {
const locale = useAppSelector((state) => state.locale.currentLang)
useEffect(() => {
const formattedLang = locale.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
if (formattedLang !== i18n.language) {
i18n.changeLanguage(formattedLang)
}
dateLocales[formattedLang]().then(() => {
dayjs.locale(formattedLang)
})
}, [locale])
return locale
}

これにdayjs(date).format('L')みたいなロケール依存フォーマットを足したくて、dayjs/plugin/localizedFormatを使うことにした。

dayjs.extend()をどこで呼ぶか問題

プラグインはdayjs.extend(localizedFormat)で登録する。これをuseLocaleフックの中で呼んでも一応動く。dayjsはシングルトン的な挙動をするモジュールで、extend()やグローバルロケール設定(dayjs.locale())はモジュール単位で共有される。逆にdayjs()で作る個々のインスタンスは独立している。

なので「フック内で毎回dayjs.extend()を呼んでも壊れはしない」んだけど、フックはレンダリングのたびに評価されるので、同じ登録処理が何度も走ることになる。ムダ。

対処法は2つある。

  1. アプリのエントリポイント(app.tsxやindex.tsx)で一度だけ登録する
  2. どうしてもフック内に置きたいなら、モジュールスコープのフラグでガードする
let isLocalizedFormatLoaded = false
function useLocale() {
useEffect(() => {
if (!isLocalizedFormatLoaded) {
dayjs.extend(localizedFormat)
isLocalizedFormatLoaded = true
}
// ...
}, [locale])
}

結論

プラグイン登録は「コンポーネントの描画より前に、一度だけ」実行されればどこでもいい。app.tsxの先頭でimportと同時にdayjs.extend()を呼んでおくのが一番素直。

app.tsx
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
dayjs.extend(localizedFormat)
function App() {
// ...
}

index.tsxがルートをマウントするだけのファイルなら、そっちに書いても同じ効果になる。重要なのはファイルの場所じゃなくて「描画前に一度」という条件だけ。