目的
個人開発しているWebアプリでNext.jsを使っています。
ある日検索機能付きセレクトボックスとdatepickerがあるフォームを作りたい場面に出会いました。
Tailwind.cssを使おうと思ったのですが検索機能付きセレクトボックスを手っ取り早く実装できなさそう...
ということでMUIを使うことに...!
要件
- 表示前に複数エンドポイントからデータ取得
- 検索機能付きセレクトボックス
- datepicker
- 画像送信
TL;DR
バージョン
- Next.js 13.0.0
- react-hook-form 7.35.0
- mui 5.0.0系
- swr 1.3.0
- axios 0.27.2
実際のコード
form.tsx
import { useState } from 'react';
import type { NextPage } from 'next';
import { useRouter } from 'next/router'
import axios from 'axios';
import useSWR from 'swr';
import getMultipleFetcher from '@/api/fetchers/getMultipleFetcher';
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
import { Stack, Autocomplete, TextField, Button } from '@mui/material';
import { styled } from '@mui/material/styles'
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
type Inputs = {
user: {};
shop: {};
image: File;
date: Date;
};
const LabelStyle = styled('span')({
color: "rgba(0, 0, 0, 0.6)",
fontSize: 15,
marginRight: 15
});
const Form: NextPage = () => {
const baseURI = process.env.NEXT_PUBLIC_BASE_URL;
const usersURI = `${baseURI}/users`;
const shopsURI = `${baseURI}/shops`;
const urls = [usersURI, shopsURI];
const { data, error } = useSWR(urls, getMultipleFetcher);
const [currentImage, setImage] = useState(new Blob)
const [imageUrl, setImageUrl] = useState("")
const { control, handleSubmit, setValue } = useForm<Inputs>({
defaultValues: { date: new Date() },
});
const validationRules = {
user: {
required: 'user is required',
},
shop: {
required: 'shop is required',
},
date: {
validate: (val: Date | null) => {
if (val == null) {
return 'date is required';
}
if (Number.isNaN(val.getTime())) {
return 'invalid date';
}
return true;
},
},
};
if (error) return <div>An error has occurred.</div>;
if (!data) return <div>Loading...</div>;
const router = useRouter()
const onSubmit: SubmitHandler<Inputs> = async (data: Inputs) => {
const { shop, user, date } = data;
const formData = new FormData();
formData.append("shop", shop.id);
formData.append("user", user.id);
formData.append("date", date?.toString());
formData.append('image', currentImage, currentImage.name)
axios({
url: `${baseURI}/register`,
method: "post",
data: formData,
headers: {
"content-type": "multipart/form-data",
},
})
.then(() => router.push("/"))
.catch((error) => {
alert("エラーが発生しました。");
});
};
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Stack
component='form'
noValidate
onSubmit={handleSubmit(onSubmit)}
spacing={2}
sx={{ m: 2, width: '80ch' }}
>
<Controller
control={control}
name='user'
rules={validationRules.user}
render={({ props }) => (
<Autocomplete
fullWidth
options={data[0]}
renderInput={(params) => <TextField {...params} label='user' />}
onChange={(event, value) => {
setValue('user', value, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
}}
/>
)}
/>
<Controller
control={control}
name='shop'
rules={validationRules.shop}
render={({ props }) => (
<Autocomplete
fullWidth
options={data[1]}
getOptionLabel={(option) => option?.name}
renderInput={(params) => <TextField {...params} label='shop' />}
onChange={(event, value) => {
setValue('shop', value, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
}}
/>
)}
/>
<Controller
name='date'
control={control}
rules={validationRules.date}
render={({ field, fieldState }) => (
<DatePicker
mask='____/__/__'
inputFormat='yyyy/MM/dd'
label='Date'
renderInput={(params) => (
<TextField
{...params}
helperText={fieldState.error?.message}
/>
)}
{...field}
/>
)}
/>
<Controller
name='image'
control={control}
render={({ field, fieldState }) =>
<label htmlFor={"image"}>
<LabelStyle>
<span>Image</span>
</LabelStyle>
<Button variant='outlined' component="span">
SELECT
</Button>
<input type='file' id="image" style={{ display: 'none' }} onChange={e => {
setImage(e.target.files[0])
setImageUrl(URL.createObjectURL(e.target.files[0]))
}} />
<img src={imageUrl} style={{ marginTop: 20 }} />
</label>
}
/>
<Button variant='contained' type='submit' color="info">
Submit
</Button>
</Stack>
</LocalizationProvider >
);
};
export default Form;
getMultipleFetcher.ts
import axios from 'axios';
export const getMultipleFetcher = (...urls: string[]) => {
const getRequest = (url: string) => axios.get(url).then((res) => res.data);
return Promise.all(urls.map((url) => getRequest(url)));
};
export default getMultipleFetcher;
コード解説
通信
- 表示前のデータ取得はswrを使って複数のエンドポイントからデータを取得してPromise.allで解決
- postは素のaxiosでformDataにappendしまくる
描画
- MUIのAutocompleteで検索付きセレクトボックスを実現
- MUIのDatePickerでdate pickerを実現
- (dateFnsを入れなきゃいけなくてハマった)
- MUI v5 から導入されたstyled()でCSS in JSを実現
- 画像のinputを
display:none
にしてreactのhtmlFor
を使いボタンを別途用意 - 画像URLを状態管理してプレビューを見せる
感想
- 調べながらやればそんなに難しくない!
- MUIはv5になってcssの記法が増えて面白いなあと思いました。
- 業務はサーバーサイドの保守が多いのでフロントの開発楽しいと感じました。
- 型をもっとちゃんとしたい....