🏝️

関数コンポーネントのライフサイクルを「マボロシじま」で理解しよう

2021/08/07に公開

タイトルから分かる通り「ネタ枠」ですが

初心者などに説明する時には、こういうエントリの方が理解しやすいかも??🤔

と思い、書いてみました

マボロシじま?なにそれ?」って方はゴメンなさい...(ポケモンルビー・サファイアで、たまにしか現れない島のことです)

「マボロシじま」をつくる

ざっとこんな感じで再現してみました

コンポーネント内でステートの変化をつけたかったので、むりやりcntというステートを持たせています

MaboroshiIsland.jsx
import { useEffect, useState } from "react"

export const MaboroshiIsland = () => {
  const [cnt, setCnt] = useState(0)
  
  // 「MaboroshiIslandの点滅」と連動
  useEffect(() => {
    console.log('なっ なんと! きょうは マボロシじま みえるのじゃ!')

    // ステートを自動更新する仕組み
    const id = setInterval(() => {
      setCnt((c) => c + 1)
    }, 1000)

    return () => {
      clearInterval(id)
      console.log('きょうは マボロシじま みえんのう・・・・')
    }
  }, [])

  // 「cntの変化」と連動
  useEffect(() => {
    console.log('現在のカウント: ', cnt)

    return () => {
      console.log('カウント更新')
    }
  }, [cnt])

  return (
    <div>Maboroshi</div>
  )
}

ここで押さえておいて欲しいのは2点です

「マボロシじま」を点滅させる

後はこの「マボロシじま」を定期的に出したり、消したりする仕組みをつくりましょう

App.jsx
import { MaboroshiIsland } from './MaboroshiIsland'

const App = () => {
  const [island, setIsland] = useState(false)

  // 3秒ごとに「点滅」
  useEffect(() => {
    const id = setInterval(() => {
      setIsland(!island)
    }, 3000)

    return () => clearInterval(id)
  })
  
  return (
    <div className="App">
      {island && (<MaboroshiIsland />)}
    </div>
  )
}

これを実行してみるとこうなります

この二つは、「useEffectの第二引数、[]の中に値が指定されているかどうか?」で決まります

[]が空っぽならば、コンポーネントの内部のステートには関係なく、マボロシじまの点滅時にだけ呼び出され、[]にステートが指定されていれば、そのステートが変わるごとに呼び出されます

第二引数を指定しないと「全ての変更時に呼び出される」ので、パフォーマンス上良くありません!(後述)

マボロシじまにトレーナーを上陸させてみる

コンポーネントが更新されるのは「内部のステートが切り替わった時だけ」ではありません。

親から渡されるpropsに変化があった時」にも更新処理が走ります

というわけで「trainer(ポケモントレーナー)」をpropsに定義してみましょう

MaboroshiIsland.jsx
import { useEffect, useState } from "react"

- export const MaboroshiIsland = () => {
+ export const MaboroshiIsland = ({ trainer }) => {

  // 中略
  
+  // 「trainerの変化」と連動
+  useEffect(() => {
+    if (!trainer) return
+
+    console.log(`トレーナー${trainer}上陸!`)
+
+    return () => {
+      console.log(`トレーナー${trainer}撤退!`)
+    }
+  }, [trainer])

  return (
    <div>Maboroshi</div>
  )
}

trainerの値を途中で変化させる方法として、苦肉の策ですがボタンで切り替えられるようにしてみました

App.jsx
   const [island, setIsland] = useState(false)
+  const [trainer, setTrainer] = useState('')

  // 中略

+  const toggle = () => {
+    if (!trainer) {
+      setTrainer('satoshi')
+    } else {
+      setTrainer('')
+    }
+  }

  // 中略
  
  return (
    <div className="App">
+      <button onClick={toggle}>toggle</button>
-      {island && (<MaboroshiIsland />)}
+      {island && (<MaboroshiIsland trainer={trainer} />)}
    </div>
  )
}

実行してみます

途中からボタンを5回連打しましたが、その間**「トレーナーの値に連動する箇所」だけ出力が走っていることがわかります**

useEffectを正しく使わないと何故パフォーマンスに悪いのか?

では最後にアンチパターンを書いてみましょう

MaboroshiIslandに以下のコードを追加します

MaboroshiIsland.jsx

  // 中略

+  // 第二引数指定しない
+  useEffect(() => {
+    console.log('全ての変更で呼び出されるよ!')
+
+    return () => {
+      console.log('こっちも!')
+    }
+  })

+  // 直書き
+  console.log('直書き!')

  return (
    <div>Maboroshi</div>
  )
}

これでさっきの動作をもう一度やってみます

...もはや多すぎて何が何やらですが、このようなことになります

今回のようにconsole.logで出力するだけならほぼパフォーマンスには差し支えはないですが、これがもしもっと複雑で「重たい処理」だったらどうでしょう?

必要もないのに何度も呼び出され、その度に「重たい処理」が走るので、当然ながら描画速度も落ちます

なので結論としては

おわりに

いかがでしたでしょうか?

useEffectの説明のため、ちょっとお試しにこういうエントリで例えてみました!

こういう記事が役立つかどうかはわかりませんが...、もし気になるところなどございましたらお気軽にメッセージください!

Discussion