Dynamically import date-fns locale files for MUI DatePicker localization

Last Updated:
MUI DatePicker Localization date-fns locale

I recently worked on a project that required localized date pickers. Users would be able to pick their locale in the settings area of the website and it would be reflected across the app on all dates.

The app uses the newest version of Material UI (MUI v5.2.6) and its DatePicker component.

The MUI DatePicker is fairly simple to plug into localization (once you know what you are doing). However it requires to you provide your own locale files from one of the supported date packages. We had chosen to use date-fns. (If interested, MUI DatePicker localization documentation).

TLDR

To solve the issue of not wanting to load all date-fns locale files. Use the import function alongside a useEffect and some lazy loaded Webpack magic to dynamically load date-fns locale files. Pass the async locale file into the MUI DatePicker LocalizationProvider from a useState.

Full Solution: https://codesandbox.io/s/mui-datepicker-date-fns-localization-p9qbs

The Problem

As frontend developers, one problem we have to solve is page weight. I.E. if the user doesn’t need it, don’t load it. Don’t make the user have to pay for any unnecessary bytes. Both in the sense of waiting for the site to load, and more literally – if they have a pay per MB data plan!

All the documentation for the MUI DatePicker localization shows the date-fns locale files being loaded right into the app. This may be ok if the app only supports one or two locales – these are only a few more KB of data. However when you want to give the user an option of all 90 of the current locales available from date-fns it begins to add up. That is around 160kb.

I needed to dynamically load date-fns locale files into the MUI DatePicker React component when and only when they were needed.

The Solution

By using some Webpack magic alongside an awaited import function it is possible to load only the required date-fns files. This is all wrapped up in a useEffect to listen to any state changes if the user changes their locale. This lazy load of the date-fns locales makes the initial app chunk much smaller than it might have been.

import React, { useState, useEffect } from "react";
import DateAdapter from "@mui/lab/AdapterDateFns";
import DatePicker from "@mui/lab/DatePicker";
import LocalizationProvider from "@mui/lab/LocalizationProvider";
import TextField from "@mui/material/TextField";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import enLocale from "date-fns/locale/en-GB";

import localizations from "./allLocalizations";
import "./App.css";

function App() {
  // Set initial locale to en-GB.
  const [locale, setLocale] = useState(enLocale);
  const [selected, setSelected] = useState(localizations[0]);

  const [date, setDate] = useState(new Date());

  // If the user changes the locale from the select box,
  // listen to the change and import the locale that is now required.
  useEffect(() => {
    const importLocaleFile = async () => {
      // This webpack option stops all of the date-fns files being imported and chunked.
      const localeToSet = await import(
        /* webpackMode: "lazy", webpackChunkName: "df-[index]", webpackExclude: /_lib/ */
        `date-fns/locale/${selected.code}/index.js`
      );
      setLocale(localeToSet.default);
    };

    // If the locale has not yet been loaded.
    if (locale.code !== selected.code) {
      importLocaleFile();
    }
  }, [selected.code, locale.code]);

  const handleChange = (event) => {
    const selectedObject = localizations.find(
      ({ code }) => code === event.target.value
    );
    setSelected(selectedObject);
  };

  return (
    <div className="App">
      <FormControl fullWidth sx={{ width: 300 }}>
        <InputLabel>Locale</InputLabel>
        <Select value={selected.code} label="Locale" onChange={handleChange}>
          {localizations.map(({ code, name }) => (
            <MenuItem key={code} value={code}>
              {name}
            </MenuItem>
          ))}
        </Select>
      </FormControl>
      <LocalizationProvider dateAdapter={DateAdapter} locale={locale}>
        <DatePicker
          value={date}
          onChange={(newValue) => setDate(newValue)}
          renderInput={(params) => <TextField {...params} />}
        />
      </LocalizationProvider>
    </div>
  );
}

export default App;

To see this example working, take a look at the Code Sandbox build here: https://codesandbox.io/s/mui-datepicker-date-fns-localization-p9qbs

To note

Database/Edge Function Addition

This solution considers en-GB as the default/fall back locale and is always loaded. If your app uses a database and the users locale is saved, it may beneficial to use the above solution alongside edge functions to change the initially loaded locale. Something for a future post…

MUI DatePicker Error

You may come across the error:

The mask “__.__.____” you passed is not valid for the format used P. Falling down to uncontrolled not-masked input.

This is due to an ongoing issue in the MUI DatePicker: https://github.com/mui-org/material-ui-pickers/issues/1776

Related Posts

Helpful Bits Straight Into Your Inbox

Subscribe to the newsletter for insights and helpful pieces on React, Gatsby, Headless WordPress, and Jest testing type things.