🫥

React の state を key でリセットする

2023/10/30に公開1

株式会社IVRy (アイブリー)のエンジニアの kinashi です。

普段 React の key は使っていますでしょうか?
このように配列からリストなどをレンダーするときに指定するのが一般的な使い方かと思います。

data.map(datum => (
  <li key={datum.id}>{datum.name}</li>
))

この記事では key を使って特定の state をクリアする実装について考えてみようと思います。

// https://ja.react.dev/learn/preserving-and-resetting-state から引用
{isPlayerA ? (
  <Counter key="Taylor" person="Taylor" />
  ) : (
  <Counter key="Sarah" person="Sarah" />
)}

key による state のリセット

先日開発をしていて、特定の操作をトリガーにして複数のフォームをリセットしたいことがあり、調べていたら React の公式ドキュメントにたどり着きました。
(今年公式ドキュメントが新しくなり、ちゃんと読めていませんでしたが、丁寧に図が挿入されていたり、チャレンジ問題で理解を深める項目があるなど、すごく読みやすかったです)

https://ja.react.dev/learn/preserving-and-resetting-state#option-2-resetting-state-with-a-key

この中に key で state をリセットするという項目があり、こう記載されています。

key はリストのためだけのものではありません! どんなコンポーネントでも React がそれを識別するために使用できるのです。

まとめにも書いてありますが、同じコンポーネントが同じ位置でレンダーされている限り state は保持されるが、異なる key を与えることでサブツリーの state をリセットすることができます。

実際に使ってみる

key を使うことで state をリセットすることができるのが分かったので、実際に簡単な実装で試してみようと思います。
先程のドキュメントでは useState を使ったコンポーネントの state をリセットしていたので、もう少し実践的に React Hook Form と Recoil が保持している state をクリアしてみます。

こんな感じで、ボタンを押したらそれぞれのフォームの値(React Hook Form) と カウント(Recoil) をクリアするものを実装してみました。

環境

Vite の react-ts テンプレートで作った素の React 環境で試しました。

package.json
  "dependencies": {
    "react": "^18.3.0-canary-b8e47d988-20231023",
    "react-dom": "^18.3.0-canary-b8e47d988-20231023",
    "react-hook-form": "^7.47.0",
    "recoil": "^0.7.7"
  },
  "devDependencies": {
    "@types/react": "^18.2.31",
    "@types/react-dom": "^18.2.14",
    "@vitejs/plugin-react": "^4.1.0",
    "typescript": "^5.2.2",
    "vite": "^4.5.0"
  }

本検証とは全く関係ないですが、 use が試せるようになっていることに気付いたので、 React はカナリアバージョンを指定しています。

https://ja.react.dev/reference/react/use

既に型の定義もあって、 types に canary を指定することで TS 環境でも使用できました。

tsconfig.json
"types": ["react/canary", "react-dom/canary"],

実装

Context に key と key を リセットする関数を持たせました。
Recoil は RecoilRoot に key を指定しておくことで、 state をクリアできます。
key の生成には Date.now を使っていますが、重複しない値なら何でもよいです。

App.tsx
function App() {
  const [appKey, setAppKey] = useState(Date.now())

  return (
    <AppContext.Provider
      value={{
        appKey,
        clearApp: () => setAppKey(Date.now()),
      }}
    >
      <RecoilRoot key={appKey}>
        <Home />
      </RecoilRoot>
    </AppContext.Provider>
  )
}

フォームのリセット用にそれぞれの Form コンポーネントに key を指定しています。
クリアボタンを押したときに clearApp が呼ばれるようにしています。
(地味ですが、 use 使ってます)

Home.tsx
const Home: FC = () => {
  const { appKey, clearApp } = use(AppContext)

  return (
    <Container>
      <Layout>
        <Form1 key={appKey} />
        <Form2 key={appKey} />
        <Counter />
      </Layout>
      <ButtonLayout>
        <Button type="button" data-variant="danger" onClick={clearApp}>
          Clear
        </Button>
      </ButtonLayout>
    </Container>
  )
}

フォーム側はこんな感じで、とくに変わったことはしていません。

Form1.tsx
const Component: FC = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useFormContext<Schema>()
  const onSubmit: SubmitHandler<Schema> = async () => {
    await new Promise((resolve) => setTimeout(resolve, 300))
    console.log('submitted!', data)
  }

  return (
    <Container>
      <form onSubmit={handleSubmit(onSubmit)}>
        <FormGroup>
          <Label htmlFor="text">Text</Label>
          <TextInput id="text" type="text" {...register('text')} />
          <ErrorMessage>{errors.text?.message}</ErrorMessage>
        </FormGroup>

        <ButtonLayout>
          <Button type="submit" disabled={isSubmitting}>
            Submit
          </Button>
        </ButtonLayout>
      </form>
    </Container>
  )
}

export const Form1: FC = () => {
  const methods = useForm<Schema>({
    defaultValues,
    mode: 'onChange',
    resolver: valibotResolver(schema),
  })

  return (
    <FormProvider {...methods}>
      <Component />
    </FormProvider>
  )
}

カウンターは Recoil を使って値を保持しています。

Counter.tsx
export const Counter: FC = () => {
  const [count, setCount] = useRecoilState(counterState)
  const increment = () => setCount((prevCount) => prevCount + 1)

  return (
    <Container>
      <Count>{count}</Count>
      <ButtonLayout>
        <Button type="button" onClick={increment}>
          Increment
        </Button>
      </ButtonLayout>
    </Container>
  )
}

まとめ

今回は key を使って state をリセットする方法について考えました。
key を使わずに同じ挙動を実現する方法はありますし、実際には特定の値のみクリアしたい場合の方が多いので、こんな方法もあるんだなくらいに覚えておいて、綺麗にハマるケースで使えたらなと思います。

最後に

IVRyでは一緒に働いてくれるエンジニアを募集中です!
https://ivry-jp.notion.site/IVRy-e1d47e4a79ba4f9d8a891fc938e02271

IVRyテックブログ

Discussion

nap5nap5

React Hook Form と Recoil

legend-state x React Hook Formでトライしてみました

RHFにおけるエラーメッセージのクリアは確かにProviderからのkey更新が良さそうでした

実際には特定の値のみクリアしたい場合の方が多い

こちらのほうのデモにチャレンジしてみました