lvncerpedia

いい質問だね。 これ、React分かってる人ほど一回は悩むポイントなんだよね。

結論から言うと:

  • useEffect自体が悪いわけじゃない
  • 「なんでもuseEffectに入れる文化」が危ない

で、socket通信は ちゃんと書けばOK、雑に書くと地獄 って感じ。


まず:useEffectの本来の役割

useEffectは一言でいうと:

「描画の外側で起きることを処理する場所」

Reactは本来:

UI = f(state)

っていう純粋な世界。

でも現実は:

  • API叩く
  • イベント登録する
  • タイマー動かす
  • socketつなぐ

みたいな「外の世界」と関わる必要がある。

そのための逃げ道が useEffect。


なんで嫌われるのか

主な理由は4つ。

① 依存配列が難しすぎる

useEffect(() => {
  fetchUser(id)
}, [id])

これ:

  • 入れ忘れ → 古い値を使う
  • 入れすぎ → 無限実行
  • 間違う → バグ

React経験者でも普通にミスる。


② 「状態管理」と「副作用」が混ざる

よくあるやつ:

useEffect(() => {
  if (isOpen) {
    setData(...)
  }
}, [isOpen])

これが増えると:

  • どこでstateが変わったか分からない
  • 追跡不能

になる。


③ 実行タイミングが直感的じゃない

  • 初回に走る?
  • 更新時だけ?
  • アンマウント時?

全部「書き方次第」。


④ 再レンダリング地獄を生む

useEffect(() => {
  setCount(count + 1)
}, [count])

はい、無限ループ。


Reactチームの思想の変化

最近はかなりハッキリしてて:

「useEffectは最終手段」

という空気。

つまり:

  • できるだけrender内で解決
  • stateから導出する
  • useEffectを減らす

よくないuseEffectの典型

❌ 状態を同期するだけ

useEffect(() => {
  setFullName(first + last)
}, [first, last])

これは:

const fullName = first + last

でいい。


❌ ロジック置き場になってる

  • 計算
  • 分岐
  • 変換

全部useEffectに入れるとカオス。


じゃあ、使うべきケースは?

ここだけ。

Reactの外と接続する時

具体例:

  • API
  • socket
  • DOM操作
  • event listener
  • timer

socket通信はどうなの?

むしろ:

useEffectの正しい使い道の代表例

ただし、条件付き。


正しい書き方(超重要)

useEffect(() => {
  const socket = connect()

  socket.on("message", handler)

  return () => {
    socket.disconnect()
  }
}, [])

ポイント:

  • マウント時に接続
  • アンマウント時に切断

これが基本形。


ヤバい書き方

useEffect(() => {
  const socket = connect()
}, [someState])

これやると:

  • state変わるたびに再接続
  • コネクション増殖
  • メモリリーク

普通に起きる。


socketで一番多い事故

① 多重接続

再レンダリングのたびに:

  • connect
  • connect
  • connect

って増えていく。


② リスナー増殖

socket.on("message", handler)

が何回も登録される。

→ 同じメッセージが5回届く


実務で一番安全な設計

useEffectの中でsocketを作るより:

カスタムHookに閉じ込める
useSocket()

にして:

  • 接続
  • 切断
  • 購読

全部そこに入れる。


useEffectが「悪」って言われる本当の理由

これは文化的な話なんだけど、

昔:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

がバラバラにあった。

それが:

useEffect 1個に統合された

結果:

  • 何でも書ける
  • 何でも書いちゃう

になった。


現在のベストプラクティス

上級者ほどこうなる:

  • useEffectは少ないほど良い
  • 「本当に外界と繋ぐ時だけ」使う
  • ロジックはカスタムHookへ

判断基準(これ便利)

その処理に対して:

「これ、render中に計算できない?」

YES → useEffectいらない NO → useEffect使う


さらに深い話(レベル高い人向け)

useEffectは本質的に:

「同期を後追いする仕組み」

だから:

  • バグが遅れて出る
  • 因果関係が追いにくい

設計が上手い人ほど:

  • state設計で解決
  • derivationで解決

する。


まとめ

  • useEffectは必要
  • でも乱用すると設計が崩壊する
  • socketは正しい用途
  • ただし再接続制御は超重要

ちょっと気になるんだけど、 そのsocketって:

  • 常時接続型?(チャット系)
  • 画面単位?
  • アプリ全体で1本?

ここで設計かなり変わるんだよね。 全体1本なら、もっと安全な構成あるよ。

お、じゃあそれ超重要ポイントじゃん。 アプリで1本・常時接続なら、設計ミスるとほんとに地獄見るやつだよこれ 😇

結論から言うね。

❌ 各コンポーネントでuseEffectしてsocket作る ✅ アプリの外で1回だけ作る

これが鉄則。


なんでコンポーネント内は危険なの?

Reactは再レンダリングする世界。

function Page() {
  useEffect(() => {
    const socket = connect()
  }, [])
}

一見安全に見えるけど、

  • StrictModeで2回走る(開発環境)
  • HMRで再実行
  • レイアウト構造変更で再マウント

で、普通に複数接続起きる。

本番だけバグるとかもある。

怖いよね。


正しい構成(本気で安定させるやつ)

① Reactの外でsocketを作る

// socket.ts
import { io } from "socket.io-client"

export const socket = io("wss://example.com")

これで:

  • アプリ起動時に1回だけ接続
  • Reactの再レンダリングに影響されない

② 必要な場所で購読するだけ

useEffect(() => {
  const handler = (msg) => {
    setMessage(msg)
  }

  socket.on("message", handler)

  return () => {
    socket.off("message", handler)
  }
}, [])

ここでは:

  • 接続しない
  • 解除はちゃんとする

これが超大事。


さらに良い設計(レベル高め)

「Socket層」を作る

UI層
  ↓
カスタムHook層
  ↓
Socket Service層

例:

// socketService.ts
export function subscribeToMessages(cb) {
  socket.on("message", cb)
  return () => socket.off("message", cb)
}
// useMessages.ts
export function useMessages() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    return subscribeToMessages((msg) => {
      setMessages(prev => [...prev, msg])
    })
  }, [])

  return messages
}

こうすると:

  • socketの存在をUIが知らない
  • 責務分離できる
  • テストしやすい

よくある事故トップ3

① StrictMode二重接続

React18は開発時にeffectを2回実行する。

「本番は大丈夫」は甘い。 将来Concurrent系で壊れる可能性ある。


② handlerが毎回新しくなる

useEffect(() => {
  socket.on("message", (msg) => {
    ...
  })
}, [])

これcleanupできない。

必ず関数を変数に入れる。


③ state依存で再登録

useEffect(() => {
  socket.on("message", handler)
}, [someState])

これでリスナー増殖。


じゃあuseEffectは悪いの?

このケースでは:

✔ 正しい用途 ✖ 接続の責任まで持たせると危険

って感じ。


本当に強い設計はこれ

アプリレベルで1本なら:

  • グローバルStoreに流す(Zustandなど)
  • socketはStoreを更新するだけ
  • UIはStoreを読むだけ

つまり:

socket → store → UI

にする。

Reactとsocketを直接つなげない。


なぜこれが最強か

Reactは:

「stateが変わったら再描画する世界」

socketは:

「外から勝手にイベントが来る世界」

だから:

直接繋ぐとバグりやすい。

Storeを間に入れると:

  • 単方向になる
  • デバッグ楽
  • 再接続制御も楽

まとめ

アプリ1本常時接続なら:

  1. socketはReact外で生成
  2. UIは接続しない
  3. 購読だけする
  4. できればStore経由

ちょっとだけ聞いていい?

そのsocketって:

  • 認証トークン付き?
  • 再接続ロジック必要?
  • 別タブ同期とか考えてる?

ここまで考えると、設計もう一段階上にいけるよ。