

dayjs.extend(localizedFormat)はどこで呼ぶのが正解か
この記事は、構成と文章の整理に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つある。
- アプリのエントリポイント(
app.tsxやindex.tsx)で一度だけ登録する - どうしてもフック内に置きたいなら、モジュールスコープのフラグでガードする
let isLocalizedFormatLoaded = false
function useLocale() { useEffect(() => { if (!isLocalizedFormatLoaded) { dayjs.extend(localizedFormat) isLocalizedFormatLoaded = true } // ... }, [locale])}結論
プラグイン登録は「コンポーネントの描画より前に、一度だけ」実行されればどこでもいい。app.tsxの先頭でimportと同時にdayjs.extend()を呼んでおくのが一番素直。
import dayjs from 'dayjs'import localizedFormat from 'dayjs/plugin/localizedFormat'
dayjs.extend(localizedFormat)
function App() { // ...}index.tsxがルートをマウントするだけのファイルなら、そっちに書いても同じ効果になる。重要なのはファイルの場所じゃなくて「描画前に一度」という条件だけ。