【React】親コンポーネントのState変更時に子コンポーネントのInput Focusが外れる問題

React
  • URLをコピーしました!

こんにちは!RYOTAです!

当記事をご覧くださりありがとうございます!

Reactのコンポーネント作成でたまにやらかしてしまうミスを備忘録としてまとめようと思います。

目次

はじめに

タイトルの通りですが親コンポーネントの状態を変更した時に、子コンポーネントのinputのフォーカスが外れてしまう現象が発生したのでその原因と解決策を記載していきます。

今回のコンポーネント構成と期待する動作は以下の通りです。

コンポーネント構成

  1. Parent: 下記の二つを出し分けるための親コンポーネント
  2. Form: 入力モードの時に表示されるコンポーネント
  3. Confirm: 確認モードの時に表示されるコンポーネント

期待する動作

  • フォームに文字を入力
  • 「確認画面へ」のボタンを押すと表示が切り替わる
  • 入力した文字が確認できる

完成後はこんな感じです。

フォーカスが外れてしまう

期待する動作とは異なりフォーカスが外れてしまって連続入力ができない事象が発生したので何故このようになってしまうのか解説します。

実際の挙動は下記の通りです。

1文字入力することにフォーカスが外れていますね。

これでは使いものになりません。

意図しない再レンダリング

では早速原因を探っていきましょう。(と言っても分かりきってるのですが…)

実際にDOMを確認してみると入力時に再レンダリング(rerender)がかかっていることがわかります。

はい。これが今回の問題です。

フォームに入力した値を親(Parent)のstateに代入しているのですがParentのstateが更新された際に子コンポーネントであるFormが再レンダリングされているためフォーカスが外れてしまいます。

原因

なぜこうなるかと言うと、、、

設計がクソだからです!!!

ではどうクソなのか実際のコードを見てみましょう…!

const Parent = () => {
  const [parentState, setParentState] = useState('')
  const [mode, setMode] = useState('form')

  const Mode = () => {
    return (
      <>
        {mode === 'form' && <div>入力モード</div>}
        {mode === 'confirm' && <div>確認モード</div>}
      </>
    )
  }

  // modeに応じて Form / Confirm を出し分け ※これがダメ
  const ChildPage = () => {
    return (
      <ChildPageContainer>
        {mode === 'form' && (
          <Form
            parentState={parentState}
            setParentState={setParentState}
            setMode={setMode}
          />
        )}
        {mode === 'confirm' && (
          <Confirm parentState={parentState} setMode={setMode} />
        )}
      </ChildPageContainer>
    )
  }
  // modeに応じて Form / Confirm を出し分け ※これがダメ

  return (
    <ParentContainer>
      <Mode />
      <ChildPage />
    </ParentContainer>
  )
}

export default Parent

15〜30行目でmodeに応じた出し分け(formの時はFormコンポーネント、confirmの時はConfirmコンポーネント)をしており、それを36行目で呼び出しています。

要はこのChildPage関数がstate変更されるたびに走ってしまっているということです。

実際にChildPage関数内にconsoleを仕込んであげると何度も発火していることが確認できます。

解決策

ではどうすれば良いかと言うと先程のChildPageちゃんで出し分けをせずに直接出し分けをしてあげればいいだけです。(関数化が必要ない)

Before

// modeに応じて Form / Confirm を出し分け ※これがダメ
const ChildPage = () => {
  console.log('ChildPageが実行されたぞ!')
  return (
    <ChildPageContainer>
      {mode === 'form' && (
        <Form
          parentState={parentState}
          setParentState={setParentState}
          setMode={setMode}
        />
      )}
      {mode === 'confirm' && (
        <Confirm parentState={parentState} setMode={setMode} />
      )}
    </ChildPageContainer>
  )
}
// modeに応じて Form / Confirm を出し分け ※これがダメ

return (
  <ParentContainer>
    <Mode />
    <ChildPage />
  </ParentContainer>
)

After

// 要らない子
// const ChildPage = () => {
//   console.log('ChildPageが実行されたぞ!')
//   return (
//     <ChildPageContainer>
//       {mode === 'form' && (
//         <Form
//           parentState={parentState}
//           setParentState={setParentState}
//           setMode={setMode}
//         />
//       )}
//       {mode === 'confirm' && (
//         <Confirm parentState={parentState} setMode={setMode} />
//       )}
//     </ChildPageContainer>
//   )
// }
// 要らない子

return (
  <ParentContainer>
    <Mode />
    {/* 直接出し分ける */}
    <ChildPageContainer>
      {mode === 'form' && (
        <Form
          parentState={parentState}
          setParentState={setParentState}
          setMode={setMode}
        />
      )}
      {mode === 'confirm' && (
        <Confirm parentState={parentState} setMode={setMode} />
      )}
    </ChildPageContainer>
    {/* 直接出し分ける */}
  </ParentContainer>
)

はい以上。なんてぇことはない。

さいごに

今回はReactでたまにやってしまうミスの意図しない再レンダリングについて触れてきました。

私自身このミスは何度もやっており、忘れた頃に「あれ?なんだこれ?」となってしまうので自戒の念を込めて記事にしてみました。

ReactやVueなどのJSフレームワークはDOMの生成のされ方や変更時の挙動が鍵になるのでもっと理解を深めていきたいなと考えております。

仮想DOMについては再度調べて記事にしていきます!

以上!

最後までご覧いただきありがとうございました!

React

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次