lodashとNextでつまずいたところをかく。

私は現在lodash+firebase+next.js+materialUIで簡単な連絡帳アプリを構築しています。
その中でも私が特に躓いた部分を記事にしました。
今回は、連絡先の更新機能で躓いた部分を紹介します。

## 実際に構築したもの
![iPhone 12 Pro Max – 20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1466470/b6aedf2d-e9db-0d01-a1b6-7fcd6f23dae7.png)


## 今回紹介するもの
![iPhone 12 Pro Max – 22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1466470/3dd0ce45-3285-0014-1faf-1661acf6a2dc.png)

前提としてデータ構造は下記のような形になっています。

```react
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でフォームの内容を管理する
この時に、間違って×マークを押して編集画面を閉じてしまっても、編集画面をもう一度クリックしたら、編集されていた内容があるようにしたい。
※一部抜粋

![iPhone 12 Pro Max – 21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1466470/3c52d807-c480-bf45-4d2e-d2f178676208.png)

### lodashのcloneDeepを使用することでネストが深いところまで見てコピーしてくれる

```react
const [updateContact, setUpdateContact] = useReducer(
    (state: Contact, data: Partial<Contact>) => _.merge({}, state, data),
    _.cloneDeep(contact)
  )
```

実際にはこのような形でコピーされるが、lodashのcloneDeepを使用することによって1行かける。


```react
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を使用する。

```react
//これだと、大元の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を更新する

```react
  
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を指定する必要がある。
![iPhone 12 Pro Max – 23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1466470/e5e85abd-c1c7-9b68-2391-0e90e9755b9b.png)


>lodash 
>https://lodash.com/docs/4.17.15#set

## [STEP：03]更新ボタンを押したらcloudfirestoreを更新する

※ withConverterは別で書きます。

```react
 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モーダルのみ)
```react

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>
  )
}

```