laravel10¦フレームワークなしのReactでバリデーションエラー時の入力フォームの値を保持する
背景
ほとんどがLaravelのbladeを使用しており、一部だけReactとTypeScriptで書かれているプロジェクト(MPA)に入った時に結構手こずったのでわかる範囲でまとめてみました。
自分もアサインするまでは恥ずかしながら知らなかったのですがReactではフレームワークを使用することを推奨しています。
やはり部分部分で厳しいなと思ったのですが、カバーできることもあると思ったのでtipsとして残しておこうと思いました。
やりたいこと
おそらくこういった場合に悩むことのひとつはバリデーションエラー時に入力欄に値を表示させたいが、React側に渡すのが難しいことではないでしょうか
sessionStorageを使用しているパターンを見かけましたが、あまり使用したくなく、使用しなくても解決できたのでその方法になります。
今回はファイルアップロードについてですが、同様の方法で<input type="text"/>
のような入力欄にも対応可能と思われます。
環境
php 8.1
Laravel 10
TypeScript 5.3
React 18.2
react-dom 18.2
vite 4.0
mui-file-input 4.0
実装
前提としてファイルアップロードとDB保存はAPIになっており、複数箇所で使用するためblade側ではblade componentを使用し、ファイルアップロードのコンポーネントを作成し使用することにしました。
またpost時のバリデーションとしてfile_id
がチェックされるため、file_id
をサーバー側へ送る必要があります。
サーバー側でファイルの情報を返してもらえない場合の実装
React
ReactではuseState
を使用し、ファイルを選択したらAPIをコールし、S3へ保存->保存が成功したらDBへ保存、返却値のidをsaveId
、ファイル自体をfileData
に入れています。
type FileType = {
id: number;
fileData: File | null;
saveId: string; // DB保存APIの返却値のID
}
export const FileUpload = (props: FileUploadProps) => {
const [files, setFiles] = useState<FileType[]>(getInitialParams(oldFileIds))
// 省略
return (
<>
{FileSystem.map((file) => {
<div key={file.id}>
<MuiFileInput
//省略
/>
{file.saveId && (
<>
<input
type="hidden"
name={
isMultiple
? `${name}[${file.id}][file_id]`
: `${name}[file_id]`
}
value={file.saveId}
readOnly
/>
<input
type="hidden"
name={
isMultiple
? `${name}[${file.id}][file_name]`
: `${name}[file_name]`
}
value={file.fileData?.name}
readOnly
/>
</>
)}
</div>
})}
</>
)
}
const elements = document.querySelectorAll('[id^="file-upload-"]')
if (elements) {
elements.forEach((elem) => {
const props = elem.getAttribute('data-props')
const reactProps: FileUploadProps = props ? Json.parse(props) : null
const root = createRoot(element)
root.render(<FileUpload {...reactProps} />)
})
}
今回のファイルアップロードはひとつだけファイルをアップロードする場合と、複数のファイルをアップロードする場合があり、ひとつだけファイルをアップロードする場合は配列の中に連想配列がある形でバリデーションがかかっています。
public function rules(): array
{
return [
'user.file_id' => ['required', 'string', 'exists:....']
];
}
複数のファイルをアップロードする場合は
public function rules(): array
{
return [
'user_file_ids.*' => [...]
];
}
blade(controllerから呼ばれるファイル)
blade側のformの中にファイルアップロード・削除のコンポーネントを入れます。
<form method="POST" action="{{ route('user.upload') }}">
@csrf
<x-file
label="名前"
name="user"
// 省略
:isMultiple="false"
:oldFileIds="is_null(old('user.file_id')) ? [] : [['file_id' => old('user.file_id'), 'file_name' => old('user.file_name')]]"
/>
</form>
name
はReactの<input type="hidden" readOnly/>の箇所で使用されているnameに該当します。
oldFileIds
はバリデーションエラーの時に使用するもので、バリデーションエラー時にfile_id
とfile_name
をReact側へ渡します。
view component
class Upload extends Component
{
public function __construct(
public string $name = '',
public string $label = '',
// 省略
public bool $isMultiple = true,
public ?array $oldFileIds = [],
) {
$this->name = $name;
$this->label = $label;
$this->isMultiple = $isMultiple;
$oldIds = [];
if (is_array($oldFileIds) && ! empty($oldFileIds)) {
foreach ($oldFileIds as $oldFiles) {
$oldIds[] = $oldFiles;
}
}
$this->oldFileIds = $oldIds;
}
}
$oldFileIds
をforeachで何をしているのかというと、そのまま$oldFileIds
を渡すとオブジェクトの中にオブジェクトが入ってしまっており、下記のような配列の中にオブジェクトが入っているようにしたかったためこうしています。
[
{ file_id: xxxxxxxx, file_name: yyyyyy.png }
]
blade(component)
/components/file
にview componentの変数をセットします。
<div
id="file-upload-{{ $name }}"
data-props="{{ json_encode([
'name' => $name,
'isMultiple' => $isMultiple,
'oldFileIds' => $oldFileIds,
]) }}"
>
ReactのuseStateの初期値getInitialParamsでしていること
getInitialParamsはoldFileIdsがある場合は、ファイルがあるように擬似的にファイルを作成し、返します。oldFileIdsがない場合は初期値を返すようにしています。
const getInitialParams = (oldFileIds) => {
if (!oldFileIds || oldFileIds.length === 0) {
return [{ id: 1, fileData: null, saveId: null}]
}
const oldParams: FileType[] = [];
oldFileIds.map((old, index) => {
oldParams.push(convertOldInitialValue(old.file_id, old.file_name, index))
})
return oldParams
}
convertOldInitialValueで擬似的にファイルを作成していますが、ただのファイル名の配列でも良いかもです。
const mimetypes = [
{ extension: 'jpeg', mimetype: 'image/jpeg' }
// 省略
]
const convertOldInitialValue = (
fileId: string,
fileName: string,
index: number
): FileType => {
const extension = getExtension(fileName) // ファイル名から'png'とかをとってくる
const mime = mimetypes.find((obj) => obj.extension === extension)?.mimetype ?? 'image/jpg'
return {
id: index,
fileData: new File([''], fileName, { type: mime }),
saveId: fileId
}
}
こうすることでsessionStrageを使用しなくてもバリデーションエラー時にファイルの入力枠にファイルがあるように見せることができます。
サーバー側でファイルの情報を返してもらえる場合の実装
APIでファイルアップロード〜DBへの保存もしているので、サーバー側からfile_idやfile_nameを返してもらえる場合はもう少しシンプルになります。
React
サーバー側からfile_nameを返してもらえるため、file_nameを送る必要はなく、file_idだけ送れば良いです。
{file.saveId && (
<>
<input
type="hidden"
name={
isMultiple
? `${name}[${file.id}]`
: `${name}[file_id]`
}
value={file.saveId}
readOnly
/>
</>
)}
blade(controllerから呼ばれるファイル)
oldFileIds
にfile_idとfile_nameが入っている配列fileを渡すようにします。
<form method="POST" action="{{ route('user.upload') }}">
@csrf
<x-file
label="名前"
name="user"
// 省略
:isMultiple="false"
:oldFileIds="empty($file) ? [] : $file"
/>
</form>
🎩 🎩 🎩
実装している時はsessionStrageを使用するかすごく悩みましたが、あまり使いたくないなと思い どのような方法で実装すべきか非常に悩みました。。。
何かしらライブラリを使用しても良さそうですが、ライブラリを使用するよりも今あるものでどうにかならないかな〜と考えた結果このようになりました。
もしかしたらもっと良い方法があるのかもですが ひとまず自分には今はこれが精一杯だったので精進したいです( ´~` )
Discussion