React Hook Form に MUI の DateTime Picker を組み込んだところ、いくつか悩んだことがあったため、メモを残します。
目次
環境
- React 17.0.2
- React Router 6.2.1
- @mui/material 5.2.4
- @mui/lab 5.0.0-alpha.60
- date-fns 2.27.0
- react-hook-form 7.22.2
前回の記事の環境に対し、 yarn upgrade-interactive
にて今回使いそうなライブラリのバージョンを上げています。
2022/07/18 追記 ここから
2022/07/18時点の最新パッケージを使う場合、 import
するパッケージや locale
に変更が入っています。こちらにまとめていますので、よろしければご確認ください。
React17 + MUI DateTimePicker + React Hook Form なアプリを yarn upgrade --latest したら破壊的変更が入っていたので修正した - メモ的な思考的な
2022/07/18 追記 ここまで
作るもの
日時が表示されています。
EDIT DATETIME
ボタンをクリックすると、モーダル に MUIのDateTime Picker が表示されます。
内容を変更します。
保存するとモーダルが閉じ、最初の画面の日時が更新されます。
実装の流れ
MUI で Modal を作る
MUI の Modal のサンプルコードを見ながら、 Modal を作ってみます。
React Modal component - MUI
サンプルコードと異なり、 style を適用せず Modal を表示してみます。
MuiModal.tsx
import {Button, Modal} from '@mui/material' import {useState} from 'react' const Component = (): JSX.Element => { const [open, setOpen] = useState(false) const handleOpen = () => setOpen(true) const handleClose = () => setOpen(false) return ( <> <Button onClick={handleOpen} variant="contained"> Open </Button> <Modal open={open} onClose={handleClose}> <h2>hello</h2> </Modal> </> ) } export default Component
OPENボタンをクリックすると、ボタンとモーダルが重なった形で表示されました。また、モーダルエリアの色がなく、分かりづらいです。
Boxコンポーネントの sx
prop を使って、サンプルコード通りのスタイルを当ててみます。
React Box component - MUI
MuiModalWithStyle.tsx
import {Box, Button, Modal} from '@mui/material' import {useState} from 'react' const style = { position: 'absolute' as 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4 } const Component = (): JSX.Element => { const [open, setOpen] = useState(false) const handleOpen = () => setOpen(true) const handleClose = () => setOpen(false) return ( <> <Button onClick={handleOpen} variant="contained"> Open </Button> <Modal open={open} onClose={handleClose}> <Box sx={style}> <h2>hello</h2> </Box> </Modal> </> ) } export default Component
モーダルの位置が調整され、モーダルエリアも分かりやすくなりました。
React Hook Form を使ってフォームを作る
続いて、 React Hook Form を使ってフォームを作ります。
Home | React Hook Form - Simple React forms validation
今回は Get Started に従って作ります。
Get Started | React Hook Form - Simple React forms validation
まずは、 useForm
hook にて register
と handleSubmit
メソッドを使えるようにします。
- useForm | React Hook Form - Simple React forms validation
- useForm - register | React Hook Form - Simple React forms validation
- useForm - handleSubmit | React Hook Form - Simple React forms validation
const {register, handleSubmit} = useForm()
次に
- formに
handleSubmit(onSubmit)
を追加 .....register('name')
にて、name
という名前の Input を追加
をコンポーネントに追加します。
<form onSubmit={handleSubmit(onSubmit)}> <input {...register('name')} /> <input type="submit" /> </form>
最後に、 handleSubmit
に渡す onSubmit
を定義します。
今回はコンソールにデータを出力するのみとします。
なお、onSubmit で型指定が必要になるため、input name
と型を指定します。
type FormInput = { name: string } const Component = (): JSX.Element => { // ... const onSubmit: SubmitHandler<FormInput> = (data) => console.log(data) return ( // ...
全体像はこんな感じです。
MuiModalWithReactHookForm.tsx
import {Box, Button, Modal} from '@mui/material' import {useState} from 'react' import {SubmitHandler, useForm} from 'react-hook-form' const style = { position: 'absolute' as 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4 } type FormInput = { name: string } const Component = (): JSX.Element => { const [open, setOpen] = useState(false) const handleOpen = () => setOpen(true) const handleClose = () => setOpen(false) const {register, handleSubmit} = useForm() const onSubmit: SubmitHandler<FormInput> = (data) => console.log(data) return ( <> <Button onClick={handleOpen} variant="contained"> Open </Button> <Modal open={open} onClose={handleClose}> <Box sx={style}> <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name')} /> <input type="submit" /> </form> </Box> </Modal> </> ) } export default Component
OPENボタンをクリックし、値を入力後に送信ボタンをクリックすると、コンソールに入力値が表示されました。
MUI DateTime Picker と組み合わせる
セットアップ
続いて、React Hook Form を MUI の DateTime Picker と組み合わせて使ってみます。
React Date Time Picker component - MUI
なお、DateTime Picker は @mui/lab
のインストールが必要です。
About the lab - MUI
% yarn add @mui/lab
他にも日付を処理するライブラリが必要になります。
今回は以前使用した date-fns
を使います。
Modal に DateTime Picker を表示
先ほどの React Hook Form では、<input {...register('name')} />
のように、 uncontrolled component を hook に登録していました。
- Register fields - Get Started | React Hook Form - Simple React forms validation
- Uncontrolled Components – React
ただ、MUI などの UI コンポーネントライブラリを使いたい場合は、Controller コンポーネントを使うことで React Hook Form に組み込めます。
- Integrating with UI libraries - Get Started | React Hook Form - Simple React forms validation
- Controller | React Hook Form - Simple React forms validation
Modal の外に LocalizationProvider、Modal の中に Controller を配置
まずは、Modal の中に LocalizationProvider
と Controller
を配置してみます。
Controller コンポーネントについては
- name
- input を識別するためのユニークな名前
- defaultValue
- DateTime Picker のデフォルト値
- render
- 描画するコンポーネント (今回は MUI の DateTime Picker)
を指定します。
LocalizationProviderInside.tsx
<Modal open={open} onClose={handleClose}> <LocalizationProvider dateAdapter={AdapterDateFns}> <Box sx={style}> <form onSubmit={handleSubmit(onSubmit)}> <Controller name="inputValue" control={control} defaultValue={new Date()} render={({field}) => { return ( <DateTimePicker {...field} label="input" renderInput={(props) => <TextField {...props} />} onChange={(newValue) => { setValue('inputValue', newValue) }} /> ) }} /> </form> </Box> </LocalizationProvider> </Modal>
画面には MUI DataTime Picker が表示できたものの、Warning も出ています。
Warning: Failed prop type: Invalid prop
children
supplied toUnstable_TrapFocus
. Expected an element that can hold a ref. Did you accidentally use a plain function component for an element instead? For more information see https://mui.com/r/caveat-with-refs-guide
そのため、 Modal の外に LocalizationProvider
を、 Modal の中に Controller
を配置することで、Warning が出なくなります。
LocalizationProviderOutside.tsx
<Modal open={open} onClose={handleClose}> <LocalizationProvider dateAdapter={AdapterDateFns}> <Box sx={style}> <form onSubmit={handleSubmit(onSubmit)}> <Controller /> </form> </Box> </LocalizationProvider> </Modal>
Localization のために mask と inputFormat を指定
DateTime Picker の Localization は、 LocalizationProvider に locale
を渡せばよいです。
Localization - React Date Picker component - MUI
ただ、
<LocalizationProvider dateAdapter={AdapterDateFns} locale={ja}>
と指定しただけでは、画面は正しく表示されるものの、Warning が出ます (例: LocalizationProviderOutsideWithLocale.tsx)。
The mask "// :" you passed is not valid for the format used P HH:mm. Falling down to uncontrolled not-masked input.
そこで、Controller の中にある DateTime Picker に mask を追加しますが、それでもまだ同じエラーが出ます。
LocalizationProviderOutsideWithLocaleMask.tsx
<Controller render={({field}) => { return ( <DateTimePicker {...field} label="input" mask="____/__/__ __:__:__" renderInput={(props) => <TextField {...props} />} onChange={(newValue) => { setValue('inputValue', newValue) }} /> ) }} />
さらに、mask
と inputFormat
を指定します。
今回は date-fns
を使っているため、 date-fns
のフォーマットで指定します。
date-fns - modern JavaScript date utility library
LocalizationProviderOutsideWithLocaleMaskInputFormat.tsx
<Controller render={({field}) => { return ( <DateTimePicker {...field} label="input" mask="____/__/__ __:__:__" inputFormat="yyyy/MM/dd HH:mm:ss" renderInput={(props) => <TextField {...props} />} onChange={(newValue) => { setValue('inputValue', newValue) }} /> ) }} />
Modal をコンポーネント化
ここまでで機能ができたため、最後に
- 入力した日時を表示するコンポーネント
- DateTimePickerWithReactHookForm.tsx
- モーダルでフォームを表示する Modal コンポーネント
- DateTimePickerModal.tsx
の2つに分けてみます。
まずは、入力した日時を表示するコンポーネントについてです。
コンポーネントでは、Modal の開閉に関係する state や、表示する日時の state を、 Modal のコンポーネントに渡します。
DateTimePickerWithReactHookForm.tsx
import {Button} from '@mui/material' import {useState} from 'react' import DateTimePickerModal from '@/components/pages/datetime_picker_with_react_hook_form/DateTimePickerModal' import {format} from 'date-fns' const Component = (): JSX.Element => { const [dateTimeLabel, setDateTimeLabel] = useState<Date | null>(new Date()) const [open, setOpen] = useState(false) const handleOpen = () => setOpen(true) const handleClose = () => setOpen(false) return ( <> <h1>Datetime Picker with React Hook Form</h1> <h2>Current value: {dateTimeLabel && format(dateTimeLabel, 'yyyy/MM/dd HH:mm:ss')}</h2> <Button onClick={handleOpen} variant="contained"> Edit DateTime </Button> <DateTimePickerModal datetimeLabel={dateTimeLabel} setDateTimeLabel={setDateTimeLabel} open={open} handleClose={handleClose} /> </> ) } export default Component
続いて、 Modal のコンポーネントです。
- 日時の state を Controller の
defaultValue
に設定 - DateTimePicker の
onChange
にて、 React Hook Form のsetValue
を使って Controller のname
に指定した項目inputValue
に値を設定 - form の
onSubmit
で指定した関数 (onSubmit
) にて、日時の set hook を使って値を設定し、 Modal を閉じる
などしています。
DateTimePickerModal.tsx
import {Controller, SubmitHandler, useForm} from 'react-hook-form' import {Box, Button, Modal, TextField} from '@mui/material' import {DateTimePicker, LocalizationProvider} from '@mui/lab' import AdapterDateFns from '@mui/lab/AdapterDateFns' import {ja} from 'date-fns/locale' import {Dispatch, SetStateAction} from 'react' type Props = { datetimeLabel: Date | null setDateTimeLabel: Dispatch<SetStateAction<Date | null>> open: boolean handleClose: () => void } type Input = { inputValue: Date | null } const style = { position: 'absolute' as 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4 } const Component = ({open, handleClose, datetimeLabel, setDateTimeLabel}: Props): JSX.Element => { const {control, handleSubmit, setValue} = useForm<Input>() const onSubmit: SubmitHandler<Input> = (data) => { console.log(data) setDateTimeLabel(data['inputValue']) handleClose() } return ( <> <LocalizationProvider dateAdapter={AdapterDateFns} locale={ja}> <Modal open={open} onClose={handleClose}> <Box sx={style}> <form onSubmit={handleSubmit(onSubmit)}> <Controller name="inputValue" control={control} defaultValue={datetimeLabel} render={({field}) => { return ( <DateTimePicker {...field} label="input" inputFormat="yyyy/MM/dd HH:mm:ss" mask="____/__/__ __:__:__" renderInput={(props) => <TextField {...props} />} onChange={(newValue) => { setValue('inputValue', newValue) }} /> ) }} /> <Button type="submit" variant="contained"> Save </Button> </form> </Box> </Modal> </LocalizationProvider> </> ) } export default Component
以上にて、作るものが完成しました。
ソースコード
Github に上げました。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample
今回のプルリクはこちらです。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample/pull/7