ã¯ããã« TextField RadioGroup SelectForm CheckboxGroup DatePicker ã³ã³ããŒãã³ã䜿çšåŽå®è£
äŸ ãããã« æ¬èšäºãå·çããã«ããã£ãŠã ãããªã³ã¯ Tech Blogéå¶ ããã® React Hook Form(v7)ã䜿ã£ãã³ã³ããŒãã³ãèšèšæ¡ piyoko ããã® MUI v5 + React Hook Form v7 ã§ããã䜿ãã³ã³ããŒãã³ãéã飿ºããŠã¿ã ãšããèšäºãåèã«ãããŠããã ããŸããããã€ãéåžžã«ããããããèšäºãããããšãããããŸãã ã¯ããã« ããã«ã¡ã¯ã ã©ã¯ ã¹ããã³ããšã³ãéçºèª²ã®æè€ã§ãã React Hook Form v7 + MUI v5 + zod v3ã䜿ã£ããã䜿ã ã³ã³ããŒãã³ã ã®å®è£
äŸã調æ»ããŠããã ãã¡ãã®èšäº ãåèã«å®è£
ãé²ããŠã¿ãŸãããããã RadioGroup ã DatePicker ã atom åããããšãããšäœç¹ãããããã€ã³ãããã£ãã®ã§ãã©ãªããã®åèã«ãªãã°ãšæãæ¬èšäºãå·çããã«è³ããŸããã å®è£
äŸã玹ä»ããåã«å ã³ã³ããŒãã³ã ãå®è£
ããã«ããã£ãŠèæ
®ããããšããŸãšããŠãããŸããReact Hook FormïŒä»¥äžRHFïŒãçšããããã§æ±ãããã ã³ã³ããŒãã³ã ã¯ã©ããã£ããã®ããèãããšãã以äžã®æ¡ä»¶ãæºãããšè¯ãã®ã§ã¯ãªãããšèããŸããã ã³ã³ããŒãã³ã ãšããªããŒã·ã§ã³ããžãã¯ãåãé¢ãããŠãã ãããªã³ã¯ããã®èšäºã®ããã« ã³ã³ããŒãã³ã ãviewå±€ãšããžãã¯å±€ã«åãããŠãã ã³ã³ããŒãã³ã ã䜿çšããåŽã¯nameãšRHFã®controlããããã£ãæž¡ãã ãã§å€ã管çã§ãã CheckboxGroupã¯ãã§ãã¯ãããCheckboxã®å€ãstringåã®é
åã§è¿ãããã«ãã RadioGroupãSelectFormã¯éžæãããå€ãstringåã§è¿ãããã«ãã DatePickerã¯å€ãDateåã§è¿ã ããããæºããããã«å ã³ã³ããŒãã³ã ãå®è£
ããŠã¿ãã®ã§ã玹ä»ããŠãããŸãã ä»å玹ä»ããç°å¢ã¯create viteã§äœã£ãŠããŸãã yarn create vite yarn add react-hook-form @hookform/resolvers @mui/material @emotion/react @emotion/styled zod TextField TextField ã«é¢ããŠã¯ åèèšäº ãšã»ãŒåãå®è£
ã§ãã import { FormHelperText , TextField as MuiTextField , TextFieldProps as MuiTextFieldProps , } from "@mui/material" ; export type TextFieldProps = MuiTextFieldProps & { inputRef?: MuiTextFieldProps [ "ref" ] ; errorMessage?: string ; } ; export const TextField: React.FC < TextFieldProps > = ( { inputRef , errorMessage , ...rest } ) => { return ( <> < MuiTextField ref = { inputRef } error = { !! errorMessage } { ...rest } / > { !! errorMessage && < FormHelperText error > { errorMessage } < /FormHelperText > } < / > ); } ; import { DeepMap , FieldError , FieldValues , useController , UseControllerProps , } from "react-hook-form" ; import { TextField , TextFieldProps } from "./TextField" ; export type RhfTextFieldProps < T extends FieldValues > = TextFieldProps & UseControllerProps < T >; export const RhfTextField = < T extends FieldValues >( props: RhfTextFieldProps < T > ) => { const { name , control } = props ; const { field: { ref , ...rest } , fieldState: { error } , } = useController < T >( { name , control } ); return ( < TextField inputRef = { ref } { ...rest } { ...props } errorMessage = { ( error && error.message ) || props.errorMessage } / > ); } ; RadioGroup RadioGroup ã«ã¯RadioPropsListãšã㊠value ãšlabelãããããã£ã«æã€é
åãæž¡ããããã«ããŸããlabelãç»é¢ã«è¡šç€ºããã ã©ãžãªãã¿ã³ æšªã®æèšã§ã value ãå®éã«RHFãåãåãå€ã«ãªããŸããäŸãã° ã©ãžãªãã¿ã³ ã§ããããããéžæãããšããããã¯ãšã³ãåŽã«ã¯ã apple ããšè±åã§æ
å ±ãéä¿¡ãããå Žåãè¯ãããã®ã§ãã®ããã«åããŠããŸãã import { FormControl , FormControlLabel , FormHelperText , Radio , RadioGroup as MuiRadioGroup , } from "@mui/material" ; import type { RadioGroupProps as MuiRadioGroupProps } from "@mui/material" ; type RadioProps = { value: string ; label: string ; } ; export type RadioGroupProps = MuiRadioGroupProps & { inputRef?: MuiRadioGroupProps [ "ref" ] ; errorMessage?: string ; radioPropsList: RadioProps [] ; } ; export const RadioGroup: React.FC < RadioGroupProps > = ( { inputRef , radioPropsList , errorMessage , ...rest } ) => { return ( < div > < FormControl error = { !! errorMessage } > < MuiRadioGroup ref = { inputRef } { ...rest } > { radioPropsList.map (( el ) => ( < FormControlLabel key = { el.value } value = { el.value } label = { el.label } control = { < Radio / > } / > )) } < /MuiRadioGroup > < /FormControl > { !! errorMessage && < FormHelperText error > { errorMessage } < /FormHelperText > } < /div > ); } ; import { useController } from "react-hook-form" ; import type { FieldValues , UseControllerProps , DeepMap , FieldError , } from "react-hook-form" ; import { RadioGroup , RadioGroupProps } from "./RadioGroup" ; export type RhfRadioGroupProps < T extends FieldValues > = RadioGroupProps & UseControllerProps < T >; export const RhfRadioGroup = < T extends FieldValues >( props: RhfRadioGroupProps < T > ) : JSX. Element => { const { name , control , ...rest } = props ; const { field: { ref , ...restControllerProps } , } = useController < T >( { name , control } ); return < RadioGroup inputRef = { ref } { ...restControllerProps } { ...rest } / >; } ; SelectForm SelectForm ã¯æ©èœãšããŠã¯RadioGroupã«è¿ãã®ã§ã»ãŒåãå®è£
ã«ãªã£ãŠããŸãããã selectedValueãããããã£ãšããŠæž¡ããããã«ããªããšãçŸåšéžæãããŠããå€ããã©ãŒã äžã«è¡šç€ºããããšãã§ããªãã®ã§æž¡ããŠããŸããRHFã䜿ããšãçŸåšéžæãããŠããå€ãã®æ
å ±ã¯useControllerããæã£ãŠãããã®ã§ããã®ãŸãŸviewå±€ã«æž¡ããŸãã ãŸãMuiã® Select ã³ã³ããŒãã³ã ãã¹ã¿ã€ãªã³ã°ããã«ãã®ãŸãŸäœ¿ããšã以äžã®gifã®ããã«widthãæ¥µç«¯ã«å°ããã³ã³ããŒããšãªã£ãŠããŸããŸãããå®éã«äœ¿ããšãã«ã¯ã奜ã¿ã®ã¹ã¿ã€ãªã³ã°ææ³ã§widthãèšå®ããã»ããè¯ãã§ãããããã Muiã® ã³ã³ããŒãã³ã ã«ã¹ã¿ã€ã«ãåœãŠããå Žåã¯å
¬åŒã«çšæãããŠãã styled() ã䜿ãã®ããªã¹ã¹ã¡ã§ãã import { FormControl , FormHelperText , InputLabel , MenuItem , Select , } from "@mui/material" ; import type { SelectProps as MuiSelectProps } from "@mui/material" ; type SelectProps = { label: string ; value: string ; } ; export type SelectFormProps = MuiSelectProps & { inputRef?: MuiSelectProps [ "ref" ] ; errorMessage?: string ; selectPropsList: SelectProps [] ; selectedValue: string ; } ; export const SelectForm: React.FC < SelectFormProps > = ( { inputRef , errorMessage , selectPropsList , selectedValue , label , ...rest } ) => { return ( < div > < FormControl > < InputLabel > { label } < /InputLabel > < Select ref = { inputRef } value = { selectedValue } label = { label } { ...rest } > { selectPropsList.map (( props ) => ( < MenuItem key = { props.value } value = { props.value } > { props.label } < /MenuItem > )) } < /Select > < /FormControl > { !! errorMessage && < FormHelperText error > { errorMessage } < /FormHelperText > } < /div > ); } ; import { useController } from "react-hook-form" ; import type { FieldValues , UseControllerProps , DeepMap , FieldError , } from "react-hook-form" ; import { SelectForm , SelectFormProps } from "./SelectForm" ; export type RhfSelectFormProps < T extends FieldValues > = Omit < SelectFormProps , "selectedValue" > & UseControllerProps < T >; export const RhfSelectForm = < T extends FieldValues >( props: RhfSelectFormProps < T > ) : JSX. Element => { const { name , control } = props ; const { field: { ref , onChange , value: selectedValue , ...rest } , fieldState: { error } , } = useController < T >( { name , control } ); return ( < SelectForm inputRef = { ref } onChange = { ( e ) => onChange ( e ) } { ...rest } { ...props } selectedValue = { selectedValue } errorMessage = { ( error && error.message ) || props.errorMessage } / > ); } ; CheckboxGroup CheckboxGroup ã«ã¯ãã§ãã¯ããå€ãé
åãšããŠè¿ã£ãŠããããã«ããŠããŸããäŸãã°ããããããã°ãªãªãããã§ãã¯ãããš ["apple", "banana"] ãè¿ã£ãŠããŸãã ããžãã¯ãšããŠã¯ RhfCheckboxGroup ã® handleChange ãšRHFã® onChange 颿°ã䜿ã£ãŠå®çŸããŠããŸãã handleChange ã§ãã§ãã¯ããå€ã®é
åãäœãã onChange 颿°ã®åŒæ°ã«æž¡ãããšã§RHFã«äœæããé
åã®æ
å ±ãæž¡ãããšãã§ããŸãã import React from "react" ; import { Checkbox , FormControlLabel , FormGroup , FormHelperText , } from "@mui/material" ; import type { FormGroupProps } from "@mui/material" ; type CheckboxProps = { value: string ; label: string ; } ; export type CheckboxGroupProps = FormGroupProps & { inputRef?: FormGroupProps [ "ref" ] ; errorMessage?: string ; checkBoxPropsList: CheckboxProps [] ; checkedValues: string [] ; } ; export const CheckboxGroup: React.FC < CheckboxGroupProps > = ( { inputRef , checkBoxPropsList , checkedValues , errorMessage , ...rest } ) => { return ( < div > < FormGroup ref = { inputRef } { ...rest } > { checkBoxPropsList.map (( props ) => ( < FormControlLabel key = { props.value } control = { < Checkbox value = { props.value } checked = { checkedValues.includes ( props.value ) } / > } label = { props.label } / > )) } < /FormGroup > { !! errorMessage && < FormHelperText error > { errorMessage } < /FormHelperText > } < /div > ); } ; import React from "react" ; import { DeepMap , FieldError , useController } from "react-hook-form" ; import type { FieldValues , UseControllerProps } from "react-hook-form" ; import { CheckboxGroup , CheckboxGroupProps } from "./CheckboxGroup" ; export type RhfCheckboxGroupProps < T extends FieldValues > = Omit < CheckboxGroupProps , "checkedValues" > & UseControllerProps < T >; export const RhfCheckboxGroup = < T extends FieldValues >( props: RhfCheckboxGroupProps < T > ) : JSX. Element => { const { name , control } = props ; const { field: { ref , onChange , value: checkedValues , ...rest } , fieldState: { error } , } = useController < T >( { name , control } ); const handleChange = ( e: React.ChangeEvent < HTMLInputElement >) => { let newCheckedValueList: string [] = [] ; if ( e.target.checked ) { // ãã§ãã¯ããã¯ã¹ããã§ãã¯ãããæããã§ãã¯ãããå€ãéè€å€ã®ç¡ãé
åã«è¿œå newCheckedValueList = [ ... new Set ( [ ...checkedValues , e.target.value ] ) ] ; } else { // ãã§ãã¯ããã¯ã¹ãå€ãããæã¯ããã§ãã¯ãå€ãããå€ãé
åããåé€ newCheckedValueList = [ ...checkedValues ] .filter ( ( value ) => value !== e.target.value ); } return newCheckedValueList ; } ; return ( < CheckboxGroup inputRef = { ref } onChange = { ( e: React.ChangeEvent < HTMLInputElement >) => onChange ( handleChange ( e )) } { ...rest } checkBoxPropsList = { props.checkBoxPropsList } checkedValues = {[ ...checkedValues ]} errorMessage = { ( error && error.message ) || props.errorMessage } / > ); } ; DatePicker DatePicker ã¯Muiã®æ§æäžviewå±€ãšããžãã¯å±€ãåããããšãã§ããŸããã§ãããrenderInputã§RhfTextFieldãçŽæ¥æž¡ãããšã§RHFã«å¯Ÿå¿ãããããã«ããŠããŸãããŸãMuiã®ããã©ã«ãã ãš DatePicker ã®placeholderã« y/mm/dd ãšè¡šç€ºãããŠããŸãã®ã§ãinputPropsããplaceholderãèšå®ããããã«ããŠããŸãããŸã defaultValue={undefined} ãæå®ããªããšåãšã©ãŒãåºãŠããŸãã®ã§èšå®ããŠããŸãã Muiã® DatePicker ã¯ã«ã¬ã³ããŒã¢ã€ã³ã³ããæ¥ä»ãéžæããããšãã§ããããTextFieldã«çŽæ¥æ¥ä»ãå
¥åããããšãã§ããŸããçŽæ¥æ¥ä»ãå
¥åããéã¯æååãDateåãšããŠparseãããã®ã§date-fnsã®parse颿°ãçšããŠããŸãããŸãæ°å€ä»¥å€ã®æååãå
¥åã§ãããšinvalid dateãšãªã£ãŠããŸãã®ã§ãonChangeå
ã§ æ£èŠè¡šçŸ ãçšããŠå
¥åã§ããªãããã«ããŠããŸãã ãŸãMuiã® DatePicker ã¯ããã¯ã¹ããŒã¹ãªã©ã§å
¥åããæ¥ä»ãå
šãŠæ¶ããšå€ãšããŠã¯nullãå
¥ãã®ã§zodåŽã§ã¯nullã蚱容ããããã«ããŠããŸãã import { DatePicker } from "@mui/x-date-pickers" ; import { parse } from "date-fns" ; import { useController } from "react-hook-form" ; import type { FieldValues , UseControllerProps } from "react-hook-form" ; import { RhfTextField } from "./RhfTextField" ; /** æ¥ä»ãã©ãŒãããyyyy/MM/ddãæååãšã¿ãªããæã®é·ãã¯10 */ const DATE_FORMAT_LENGTH = 10 ; export type RhfDatePickerProps < T extends FieldValues > = UseControllerProps < T >; export const RhfDatePicker = < T extends FieldValues >( props: RhfDatePickerProps < T > ) => { const { name , control } = props ; const { field: { onChange , value } , } = useController < T >( { name , control } ); const onSelectDate = ( e: Date | null ) => { onChange ( e ); } ; const onChangeText = ( value: string ) => { // MUIã®DatePickerã¯ããã©ã«ãã§10æåããå€ãå
¥åã§ããŠããŸãããã10æåãè¶
ããåã¯çç¥ãã // ex) yyyy/MM/dd{ä»»æã®æå}ã®ããã«å
¥åã§ããŠããŸã if ( value.length > DATE_FORMAT_LENGTH ) { onChange ( parse ( value.slice ( 0 , DATE_FORMAT_LENGTH ), "yyyy/MM/dd" , new Date ()) ); return; } onChange ( parse ( value , "yyyy/MM/dd" , new Date ())); } ; return ( < DatePicker value = { value || null } onChange = { ( e: Date | null ) => onSelectDate ( e ) } renderInput = { ( params ) => ( < RhfTextField { ...params } inputProps = {{ ...params.inputProps , placeholder: "yyyy/MM/dd" , }} error = { !! errors [ name ]} onChange = { ( e ) => { // æ°å€ä»¥å€ã匟ã if ( !/^\d*$/ .test ( e.target.value )) return; onChangeText ( e.target.value ); }} defaultValue = { undefined } name = { name } control = { control } / > ) } / > ); } ; ã³ã³ããŒãã³ã 䜿çšåŽå®è£
äŸ åèãŸã§ã«ãããŸã§ã«ç޹ä»ãã ã³ã³ããŒãã³ã ã®äœ¿çšåŽå®è£
äŸãæ²èŒããŠãããŸãã import "./App.css" ; import { styled , Button } from "@mui/material" ; import { z } from "zod" ; import { useForm , SubmitHandler } from "react-hook-form" ; import { zodResolver } from "@hookform/resolvers/zod" ; import { RhfTextField } from "./components/RhfTextField" ; import { RhfRadioGroup } from "./components/RhfRadioGroup" ; import { RhfSelectForm } from "./components/RhfSelectForm" ; import { RhfCheckboxGroup } from "./components/RhfCheckboxGroup" ; import { RhfDatePicker } from "./components/RhfDatePicker" ; const Form = styled ( "form" )( { display: "flex" , flexDirection: "column" , gap: "16px" , alignItems: "center" , width: "100%" , padding: "16px" , } ); const Flex = styled ( "div" )( { display: "flex" , gap: "16px" , } ); const schema = z. object ( { text: z. string () .min ( 1 , { message: "Required" } ), radio: z. string () .min ( 1 , { message: "Required" } ), select: z. string () .min ( 1 , { message: "Required" } ), checkbox: z. string () .array () .min ( 1 , { message: "Required" } ), date: z .date () .nullable () .refine (( date ) => date !== null , "Required" ), } ); type Inputs = z.infer <typeof schema >; const defaultValues: Inputs = { text: "" , radio: "" , select: "" , checkbox: [] , date: null , } ; const props = [ { label: "ããã" , value: "apple" , } , { label: "ã¿ãã" , value: "orange" , } , { label: "ã°ãªãª" , value: "banana" , } , ] ; function App () { const { control , handleSubmit , reset } = useForm < Inputs >( { defaultValues: defaultValues , resolver: zodResolver ( schema ), } ); const onSubmit : SubmitHandler < Inputs > = ( data ) => console .log ( data ); return ( < Form onSubmit = { handleSubmit ( onSubmit ) } > < RhfTextField label = "Text" name = "text" control = { control } / > < RhfRadioGroup name = "radio" control = { control } radioPropsList = { props } / > < RhfSelectForm label = "Select" name = "select" control = { control } selectPropsList = { props } / > < RhfCheckboxGroup name = "checkbox" control = { control } checkBoxPropsList = { props } / > < RhfDatePicker name = "date" control = { control } / > < Flex > < Button type= "submit" > éä¿¡ < /Button > < Button onClick = { () => reset () } > ãªã»ãã < /Button > < /Flex > < /Form > ); } export default App ; ãããã« ãã䜿ã ã³ã³ããŒãã³ã ãRHFåããŠå ã³ã³ããŒãã³ã ãšããŠäœæããããšãã§ããŸããããããã® ã³ã³ããŒãã³ã ãçµã¿åãããzodãšé£æºããããšã§æ§ã
ãªããªããŒã·ã§ã³æ©èœãæã£ããã©ãŒã ãäœæããããšãã§ãããšæããŸãã DatePicker ã«é¢ããŠã¯è¥å¹²ãŽãªæŒãã®å®è£
ã«ãªã£ãŠããŸã£ãæãåŠããŸãããèªåã®å®åã§ã¯ãããéçã§ãã...ã æªã ã«RHFã®åºãèŠããŠããªãã®ã§ã©ãã©ã䜿ãåããŠãã¹ã¿ãŒã§ããããã«ãªã£ãŠããããã§ãã ãšã³ãžã㢠äžéæ¡çš ãµã€ã ã©ã¯ ã¹ã§ã¯ããšã³ãžãã¢ã»ãã¶ã€ããŒã® äžéæ¡çš ãç©æ¥µçã«è¡ã£ãŠãããŸãïŒ ãèå³ãããŸãããæ¯éã確èªããé¡ãããŸãã https://career-recruit.rakus.co.jp/career_engineer/ ã«ãžã¥ã¢ã«é¢è«ãç³èŸŒã¿ãã©ãŒã ã©ã®è·çš®ã«å¿åããã°è¯ããããããªããšããæ¹ã¯ãã«ãžã¥ã¢ã«é¢è«ãéæè¡ã£ãŠãããŸãã 以äžãã©ãŒã ãããç³èŸŒã¿ãã ããã rakus.hubspotpagebuilder.com ã©ã¯ ã¹Developersç»é²ãã©ãŒã https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ ã€ãã³ãæ
å ± äŒç€Ÿã®é°å²æ°ãç¥ãããæ¹ã¯ãæ¯é±éå¬ããŠããã€ãã³ãã«ãåå ãã ããïŒ âTECH PLAY techplay.jp âconnpass rakus.connpass.com