Reactでブラウザバック時のフォームデータ保持方法

# はじめに

業務でReactアプリケーションのデバッグを行う中で、ブラウザバック時にフォームの値がリセットされる問題に直面しました。この問題の解決方法を共有します。

# 値を保持の方法とは

historyオブジェクトやlocalStorageを使う方法も考えられますが、今回はセッションごとにデータを管理できるsessionStorageを使うことにしました。

# `sessionStorage`を使って値を保持する

コードを書いていると気づいたのは、`useEffect`を利用して保存や復元をするのがやりやすいということです。この章では、いくつかの具体例を列挙していくので参考になりそうなコードがあれば流用してください。

## 初期値なしの保存・復元

```tsx
const [value, setValue] = useState<string>()

// 何かしらの処理実行
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const inputValue = e.target.value
  setValue(inputValue)
  sessionStorage.setItem('formValue', inputValue)
}

// valueの保存
useEffect(() => {
  if (value) {sessionStorage.setItem('value', value)}
}, [value])

// valueの復元
useEffect(() => {
	const savedValue = sessionStorage.getItem('value')
  if (savedValue) {setLabel(savedValue)}
}, [])
```

初期値がない状態で保存と復元を効率よく行いたい場合に、便利に利用できます。今回デバッグしていて、もっとも利用しました。

## 初期値ありの保存・復元

```tsx
const initLabel = '値を入力してください'
const [label, setLabel] = useState<string>(initLabel)

// labelの保存
useEffect(() => {
  // labelがinitLabelの場合は保存しない
  if (label && label !== initLabel) {
  sessionStorage.setItem('labelText', label)
  }
}, [label, initLabel])

// labelの復元
  useEffect(() => {
    const savedLabelText = sessionStorage.getItem('labelText')

    // 保存されている値がある場合は復元
    if (savedLabelText) {
      setLabel(savedLabelText)
    }
  }, [])
```

上記では、`label`に初期値が入っている場合は保存しません。そして`useEffect`を利用して、`label`の値が変化するたびに保存用の`useEffect`も実行されます。（正しくは`label`の復元も実行されますが…）

## 文字配列の保存・復元

```tsx
const [label, setLabel] = useState<string[]>()

...
// 何かしらの処理でsetLabel(○○)を利用
...

// labelの保存
useEffect(() => {
  if (label) {
  sessionStorage.setItem('labelText', JSON.stringify(label))
  }
}, [label])

// labelの復元
  useEffect(() => {
    const savedLabelText = sessionStorage.getItem('labelText')

    // 保存されている値がある場合は復元
    if (savedLabelText) {
      setLabel(JSON.parse(savedLabelText))
    }
  }, [])
```

こちらもデバッグしていて、よく記述したコードです。

## 辞書型の各データの保存と復元

```tsx
const {
    getValues,
    watch,
    reset,
    register,
    setValue
  } = useForm<VoucherSearchFormData>({
    defaultValues: {
      keyword: '',
      categories: []
    }
  })
  // categoriesの保存
  useEffect(() => {
    const subscription = watch((value) => {
      const { categories } = value
      if (categories && categories.length > 0) {
        sessionStorage.setItem('selectedCategories', JSON.stringify(categories))
      }
    })

    return () => subscription.unsubscribe() // クリーンアップ
  }, [watch])
  
  // categoriesの復元
  useEffect(() => {
    const selectedCategories = sessionStorage.getItem('selectedCategories')
    if (selectedCategories) {
      const categories: string[] = JSON.parse(selectedCategories)
      setValue('categories', categories)
    }
  }, [setValue])

  // keywordの保存
  useEffect(() => {
    const subscription = watch((value) => {
      const { keyword } = value
      if (keyword !== undefined && keyword !== '') {
        sessionStorage.setItem('keyword', keyword)
      }
    })

    return () => subscription.unsubscribe() // クリーンアップ
  }, [watch])

  // keywordの復元
  useEffect(() => {
    const savedKeyword = sessionStorage.getItem('keyword')
    if (savedKeyword) {
      setValue('keyword', savedKeyword)
    }
  }, [setValue])
```

最初は`getValues`を利用して値の保存をしていたのですが、うまく保存ができなかったので、`useFrom`の機能の一つである`watch`を利用することで、辞書型のデータが変化するたびにうまく保存してくれるようになりました。

# 最後に

今回は`sessionStorage` を利用して値を保持する方法の解説と具体例の列挙をしました。フロントエンドのデバックは苦労したので、これからも調べた知見をまとめていこうと思います。

ありがとうございました。

# 参考記事

[ブラウザバック時にReactでもフォームの値を保持したい！ - Qiita](https://qiita.com/tatan-tech/items/21a9831f83615b7800cf)