HeadlessUIでModalを実装 propsをスプレッド構文で渡してスッキリ!
Modalの実装って意外とめんどくさい。そんな中で発見があったので記事にします。
実装内容
HeadlessUIのModalを使用して複数のModalを実装する。
環境
"@headlessui/react": "^1.5.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"tailwindcss": "^3.0.23",
"react-hook-form": "^7.29.0",
実装のイメージ
このような感じで複数のitem毎にmodal
そして更に各memoを編集するためのmodalを実装します。
modalの状態管理
まず手始めにmodalの状態をuseStateで管理します。
const [isOpen, setIsOpen] = useState(false);
Reactではよく見る実装ですね。しかし、このステートを管理する場所が重要なのです。
最初はトップレベルのコンポーネントでステートを保持していたのですが、そうすると各itemでmodalを開く処理を行ったときにどのmodalを開いたのか分からなくなってしまい、エラーになってしまうのです。
図で見るとこんな感じ
これだとitemList内で管理されているModalの状態管理なので各itemの編集modalを開くためには各itemコンポーネント内でstateを管理しなければならないのです。
コードを書いていく
itemListをmapメソッドを使ってイテレートします。
ポイントとしてはイテレートした中で一つ一つにuseStateでの状態管理を持たせるために一つコンポーネントを挟む事です。Providerという名前にしておきます。(今回はItemModal用なのでItemModalProvider)
関数などの説明は省きます。
import React, { useState, useEffect } from 'react';
import { Modal } from './Modal';
import { nanoid } from 'nanoid';
import { useForm } from 'react-hook-form';
import { ItemModal } from './ItemModal';
export const App = () => {
const { register, handleSubmit, errors } = useForm();
const [itemList, setItemList] = useState([]);
const prevItemList = [
{
id: '1',
name: 'item1',
memoList: [
{ memoId: '1', memo: 'memo1' },
{ memoId: '2', memo: 'memo2' },
],
},
{
id: '2',
name: 'item2',
memoList: [
{ memoId: '3', memo: 'memo3' },
{ memoId: '4', memo: 'memo4' },
],
},
];
useEffect(() => {
setItemList(prevItemList);
}, []);
const handleClickAddItem = () => {
const newItem = {
id: nanoid(),
name: `item${itemList.length + 1}`,
memoList: [{ memoId: nanoid(), memo: 'aaa' }],
};
setItemList([...itemList, newItem]);
console.log('itemが追加されました');
};
const handleChangeItemName = (id, name) => {
const newItemList = itemList.map((item) => {
if (item.id === id) {
item.name = name;
}
return item;
});
setItemList(newItemList);
};
const handleEditMemo = (itemId, memoId, name) => {
const newItemList = itemList.map((item) => {
if (item.id === itemId) {
item.memoList = item.memoList.map((memo) => {
if (memo.memoId === memoId) {
memo.memo = name;
}
return memo;
});
}
return item;
});
setItemList(newItemList);
};
const onSubmit = (data, id) => {
console.log(data);
const newItemList = itemList.map((item) => {
if (item.id === id) {
item.memoList.push({ memoId: nanoid(), memo: data[`itemId:${id}`] });
}
return item;
});
setItemList(newItemList);
};
return (
<>
<div className='m-4 flex flex-col items-center'>
<h1 className='text-4xl font-semibold text-gray-700'>
Headless UI Dialog
</h1>
<p>{`itemの数は${itemList.length}です`}</p>
<button
className='my-4 p-2 border-solid border-2'
onClick={handleClickAddItem}
>
itemを追加する
</button>
{itemList.map((item) => (
<div
key={item.id}
className='mt-4 border-solid border-2 rounded-md p-4'
>
<ItemModalProvider
item={item}
handleChangeItemName={handleChangeItemName}
handleSubmit={handleSubmit}
handleEditMemo={handleEditMemo}
onSubmit={onSubmit}
register={register}
/>
<input
type='text'
value={item.name}
onChange={(e) => handleChangeItemName(item.id, e.target.value)}
/>
<form onSubmit={handleSubmit((data) => onSubmit(data, item.id))}>
<input
className='border-solid border-2'
{...register(`itemId:${item.id}`)}
/>
<button type='submit'>memoを追加する</button>
</form>
<ul>
{item.memoList.map((memo) => (
<li key={memo.memoId}>{memo.memo}</li>
))}
</ul>
</div>
))}
</div>
</>
);
};
const ItemModalProvider = ({
item,
handleChangeItemName,
handleSubmit,
handleEditMemo,
onSubmit,
register,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<ItemModal
isOpen={isOpen}
setIsOpen={setIsOpen}
item={item}
handleChangeItemName={handleChangeItemName}
handleSubmit={handleSubmit}
handleEditMemo={handleEditMemo}
onSubmit={onSubmit}
register={register}
/>
<button
className='w-64 inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm'
onClick={() => setIsOpen(!isOpen)}
>
Open Modal
</button>
</>
);
};
このようにすることで各item毎のmodalのstateを保持することができました。
やっていることは新たなコンポーネントを挟んでuseStateを追加しているだけなのでやってしまえば簡単です。なのにpropsをわざわざ全部書いててめんどくさい・・・。
そこで活躍するのがスプレッド構文です。
リファクタリング スプレッド構文を使ってpropsを展開
const ItemModalProvider = ({
item,
handleChangeItemName,
handleSubmit,
handleEditMemo,
onSubmit,
register,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<ItemModal
isOpen={isOpen}
setIsOpen={setIsOpen}
item={item}
handleChangeItemName={handleChangeItemName}
handleSubmit={handleSubmit}
handleEditMemo={handleEditMemo}
onSubmit={onSubmit}
register={register}
/>
<button
className='w-64 inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm'
onClick={() => setIsOpen(!isOpen)}
>
Open Modal
</button>
</>
);
};
この部分非常にわかりづらい。というかめんどくさい。ここって実はこんな風にかけてしまうのです。
const ItemModalProvider = (props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<ItemModal isOpen={isOpen} setIsOpen={setIsOpen} {...props} />
<button
className='w-64 inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm'
onClick={() => setIsOpen(!isOpen)}
>
Open Modal
</button>
</>
);
};
propsとして受け取った物を分割代入しないでそのまま{...props}としてItemModalコンポーネントへ展開して渡せるのです。このように書くことでItemModalProviderはItemModalのstateを管理する責務を持つだけのコンポーネントであることが分かるため、非常に可読性がよくなります。
Discussion