私は現在lodash+firebase+next.js+materialUIで簡単な連絡帳アプリを構築しています。
その中でも私が特に躓いた部分を記事にしました。
今回は、連絡先の更新機能で躓いた部分を紹介します。
実際に構築したもの
今回紹介するもの
前提としてデータ構造は下記のような形になっています。
interface Contact{
id: string
firstName: string
lastName: string
phoneNumber: string
email?: string
company?: string
avatarUrl?: string
address:{
zip?: string
prefecture?: string
city?: string
building?: string
street?: string
}
}
[STEP:01]useStateでフォームの内容を管理する
この時に、間違って×マークを押して編集画面を閉じてしまっても、編集画面をもう一度クリックしたら、編集されていた内容があるようにしたい。
※一部抜粋
lodashのcloneDeepを使用することでネストが深いところまで見てコピーしてくれる
const [updateContact, setUpdateContact] = useReducer(
(state: Contact, data: Partial<Contact>) => _.merge({}, state, data),
_.cloneDeep(contact)
)
実際にはこのような形でコピーされるが、lodashのcloneDeepを使用することによって1行かける。
const [updateContact, setUpdateContact] = useReducer(
(state: Contact, data: Partial<Contact>) => _.merge({}, state, data),
{
id: contact.id,
firstName: contact.firstName,
lastName: contact.lastName,
email: contact.email,
phoneNumber: contact.phoneNumber,
company: contact.company,
address: {
zip: contact.address?.zip,
prefecture: contact.address?.prefecture,
city: contact.address?.city,
building: contact.address?.building,
street: contact.address?.street,
},
}
)
[注意]lodashにはcloneとcloneDeepがあり、大元のデータを変更しないcloneDeepを使用する。
//これだと、大元のcontactの値を変更してしまう恐れがある。
_.clone(contact)
//完全に別物にして、オリジナルのものを壊さない為には、cloneDeepを使う
_.cloneDeep(contact)
clone
https://lodash.com/docs/4.17.15#clone
cloneDeepについて
https://lodash.com/docs/4.17.15#cloneDeep
[STEP:02]フォームの内容が書き換えられたら、useStateを更新する
const onChangeUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
//ここでuseStateを更新する。
setUpdateContact(_.set({}, e.target.name, e.target.value))
}
return (
---省略---
<TextField
value={updateContact.firstName}
name='firstName'
label='名前'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
---省略---
<TextField
name='address.street'
value={updateContact.address?.street}
label='町・番地'
fullWidth
onChange={onChangeUpdate}
margin='normal'
placeholder='ねこマム番地'
/>
---省略---
)
}
lodashのset関数の第二引数はpathを指定する必要がある。
[STEP:03]更新ボタンを押したらcloudfirestoreを更新する
※ withConverterは別で書きます。
const handleUpdate = async () => {
const ref = doc(
db,
'users',
currentUser?.uid as string,
'contacts',
contact.id
).withConverter<Contact>(new IdRemover<Contact>())
await updateDoc(ref, updateContact)
router.reload()
}
全コード(Updateモーダルのみ)
interface UpdateDialogProps {
contact: Contact
isUpdateModalOpen: boolean
setIsUpdateModalOpen: React.Dispatch<React.SetStateAction<boolean>>
}
const UpdateDialog: React.FC<UpdateDialogProps> = ({
contact,
isUpdateModalOpen,
setIsUpdateModalOpen,
}) => {
const [updateContact, setUpdateContact] = useReducer(
(state: Contact, data: Partial<Contact>) => _.merge({}, state, data),
_.cloneDeep(contact)
)
const onChangeUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
setUpdateContact(_.set({}, e.target.name, e.target.value))
}
const router = useRouter()
const { db, currentUser } = useContext(FirebaseContext)
const handleUpdate = async () => {
const ref = doc(
db,
'users',
currentUser?.uid as string,
'contacts',
contact.id
).withConverter<Contact>(new IdRemover<Contact>())
await updateDoc(ref, updateContact)
router.reload()
}
return (
<Dialog fullScreen open={isUpdateModalOpen}>
<AppBar sx={{ position: 'relative' }}>
<Toolbar>
<IconButton
edge='start'
color='inherit'
onClick={() => setIsUpdateModalOpen(false)}>
<CloseIcon />
</IconButton>
<Typography sx={{ ml: 2, flex: 1 }} variant='h6' component='div'>
連絡先を編集中
</Typography>
<Button color='inherit' onClick={handleUpdate}>
更新する
</Button>
</Toolbar>
</AppBar>
<Container>
<Typography sx={{ my: 2, flex: 1 }} variant='h5' component='div'>
基本情報
</Typography>
<Grid container rowSpacing={1} justifyContent='start'>
<TextField
value={updateContact.firstName}
name='firstName'
label='名前'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
value={updateContact.lastName}
name='lastName'
label='苗字'
placeholder='マムシ'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
value={updateContact.phoneNumber}
name='phoneNumber'
label='電話番号'
placeholder='1234567890'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
value={updateContact.email}
name='email'
label='メールアドレス'
placeholder='mamushi@mamushi.com'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
value={updateContact.company}
name='company'
label='会社名'
placeholder='マムシ株式会社'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
</Grid>
<Typography sx={{ my: 2, flex: 1 }} variant='h5' component='div'>
住所
</Typography>
<Grid container rowSpacing={1} sx={{ mb: 5 }} justifyContent='start'>
<TextField
name='address.zip'
value={updateContact.address?.zip}
label='郵便番号'
placeholder='2010017'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
name='address.prefecture'
value={updateContact.address?.prefecture}
label='都道府県'
placeholder='東京都'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
name='address.city'
value={updateContact.address?.city}
label='市町村'
placeholder='マムシ区マムシ町'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
name='address.building'
value={updateContact.address?.building}
label='ビル・建物名'
placeholder='マムシビル'
onChange={onChangeUpdate}
fullWidth
margin='normal'
/>
<TextField
name='address.street'
value={updateContact.address?.street}
label='町・番地'
fullWidth
onChange={onChangeUpdate}
margin='normal'
placeholder='ねこマム番地'
/>
<Button onClick={handleUpdate} sx={{ my: 3 }} variant='contained'>
更新する
</Button>
</Grid>
</Container>
</Dialog>
)
}