Reactでおみくじのようなランダム表示はuseMemoを使用する

Reactでおみくじのようなランダム表示を正しく処理する方法

ReactでおみくじのようなJavaScriptのMath.random()を使用したランダム表示のコードを以下のように書かれているコードをたまに見かけます。

App.tsx
import { useState } from 'react'
import './App.css'

const Omikuji = () => {
  const fortunes = ['大吉', '中吉', '小吉', '', '末吉', '']
  const result = fortunes[Math.floor(Math.random() * fortunes.length)]

  return <p>あなたの運勢は「{result}」です!</p>
}

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <h1>React おみくじ(悪い例)</h1>
      <Omikuji />
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
      </div>
    </>
  )
}

export default App

結論から言うと、これだとカウントボタンを押した際に発生する再レンダリングで、Omikuji内のコードが再度処理されて結果が変わってしまいます。

React おみくじ(悪い例)

useMemoで再処理を防ぐ

同じランダムな結果が再レンダリング時に変わらないようにするには、useMemoを利用してコンポーネントの初回レンダリング時にのみランダムな結果を生成するようにします。

App.tsx
import { useState, useMemo } from 'react'
import './App.css'

const Omikuji = () => {
  const result = useMemo(() => {
    const fortunes = ['大吉', '中吉', '小吉', '', '末吉', '']
    return fortunes[Math.floor(Math.random() * fortunes.length)]
  }, [])

  return <p>あなたの運勢は「{result}」です!</p>
}

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <h1>React おみくじ(改善例)</h1>
      <Omikuji />
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
      </div>
    </>
  )
}

export default App

React おみくじ(改善例)

ちなみにReact Compilerを有効にしてメモ化を自動化している場合は、useMemoを追加しなくても再レンダリング時に結果が変わることはありません。

ボタンを押したときにランダムの表示を変更する方法

初回レンダリング時だけでなく、おみくじを引き直すボタンを追加して、ボタンを押したときに新たなランダムな結果を表示したいケースがあります。

そんなときは以下のようにランダムな結果を返す関数(getFortune)を作成して、useStateでランダムな結果を管理すれば実装できます。

App.tsx
import { useState } from 'react'
import './App.css'

const fortunes = ['大吉', '中吉', '小吉', '', '末吉', '']

const Omikuji = () => {
  const getFortune = () => {
    const index = Math.floor(Math.random() * fortunes.length)
    return fortunes[index]
  }
  const [result, setResult] = useState<string>(getFortune())
  const draw = () => {
    setResult(getFortune())
  }

  return (
    <>
      <p>あなたの運勢は「{result}」です!</p>
      <button onClick={draw}>おみくじを引き直す</button>
    </>
  )
}

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <h1>React おみくじ(引き直し版)</h1>
      <Omikuji />
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
      </div>
    </>
  )
}

export default App

React おみくじ(引き直し版)

まとめ

Reactでランダムな表示を扱う際、何も考えずにMath.random()を直接コンポーネント内部で実行してしまうと、再レンダリングのたびに結果が変わるという問題が起きます。

これを防ぐには、useMemoを使って初回だけランダム値を生成するようにしたり、ユーザー操作で引き直したい場合にはuseStateと関数を組み合わせて管理することで、意図した挙動が実現できます。