公開日

ハロー、React、ハロー useEffect(そうなるといいんだけど)

著者

この記事では、ほとんどの場合に useEffect を React で置き換える方法を紹介します。

私は "Goodbye, useEffect" by David Khoursid を見ていて、🤯 それは 😀 良い意味で私の心を吹き飛ばしました。 useEffect は非常に多く使用されているため、コードが汚くなり、保守が難しくなることに同意します。 私は長い間 useEffect を使ってきましたが、誤用していることに罪悪感を抱いています。 React には、コードをよりクリーンで保守しやすくする機能があるはずです。

useEffect とは?

useEffect は、関数コンポーネントで副作用を実行できるようにするフックです。 componentDidMount、componentDidUpdate、componentWillUnmount を単一の API に統合します。 それは、多くのことを可能にする魅力的なフックです。 しかし、それは多くのバグを引き起こす可能性のある非常に危険なフックでもあります。

なぜ useEffect が危険なのですか?

次の例を見てみましょう。

import React, { useEffect } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setCount((c) => c + 1)
    }, 1000)
    return () => clearInterval(interval)
  }, [])

  return <div>{count}</div>
}

これは、1 秒ごとに増加する単純なカウンターです。 useEffect を使用して間隔を設定しています。 また、useEffect を使用してコンポーネントがアンマウントされたときに間隔をクリアします。 上記のコードスニペットは、useEffect の一般的なユースケースです。 これは簡単な例ですが、ひどい例でもあります。

この例の問題は、コンポーネントが再レンダリングされるたびに間隔が設定されることです。 コンポーネントが何らかの理由で再レンダリングされた場合、間隔は再び設定されます。 間隔は 1 秒間に 2 回呼び出されます。 この簡単な例では問題ありませんが、間隔がより複雑な場合は大きな問題になる可能性があります。 メモリリークが発生する可能性もあります。

修正方法?

この問題を解決する方法はたくさんあります。 1 つの方法は、useRef を使用して間隔を格納することです。

import React, { useEffect, useRef } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)
  const intervalRef = useRef()

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount((c) => c + 1)
    }, 1000)
    return () => clearInterval(intervalRef.current)
  }, [])

  return <div>{count}</div>
}

上記のコードは、以前の例よりもはるかに優れています。 コンポーネントが再レンダリングされるたびに間隔を設定しません。 しかし、まだ改善が必要です。 まだ少し複雑です。 また、useEffect を使用しており、それは非常に危険なフックです。

useEffect は効果のためのものではありません

useEffect については、componentDidMount、componentDidUpdate、componentWillUnmount を単一の API に統合していることを知っています。 いくつか例を挙げましょう。

useEffect(() => {
  // componentDidMount?
}, [])
useEffect(() => {
  // componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
  return () => {
    // componentWillUnmount?
  }
}, [])

理解しやすいです。 useEffect は、コンポーネントがマウント、更新、アンマウントされたときに副作用を実行するために使用されます。 しかし、副作用を実行するためにだけ使用されているわけではありません。 コンポーネントが再レンダリングされたときに副作用を実行するためにも使用されます。 コンポーネントが再レンダリングされたときに副作用を実行するのは良い考えではありません。 多くのバグを引き起こす可能性があります。 コンポーネントが再レンダリングされたときに副作用を実行するには、他のフックを使用する方が良いです。

import React, { useState, useEffect } from 'react'

const Example = () => {
  const [value, setValue] = useState('')
  const [count, setCount] = useState(-1)

  useEffect(() => {
    setCount(count + 1)
  })

  const onChange = ({ target }) => setValue(target.value)

  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {count}</div>
    </div>
  )
}

useEffect は状態設定器ではありません

import React, { useState, useEffect } from 'react'

const Example = () => {
  const [count, setCount] = useState(0)

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`
  }) // <-- this is the problem, 😱 it's missing the dependency array

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

このドキュメントを読むことをお勧めします: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

命令型 vs 宣言型

命令型: 何かが起こったら、この効果を実行します。

宣言型: 何かが起こったら、状態が変化し、状態のどの部分が変化したか(依存関係配列)によって、この効果が実行されますが、いくつかの条件が真の場合にのみ実行されます。 React は、理由もなく 同時レンダリングのために、再びそれを実行する可能性があります。

概念 vs 実装

概念:

useEffect(() => {
  doSomething()

  return () => cleanup()
}, [whenThisChanges])

実装:

useEffect(() => {
  if (foo && bar && (baz || quo)) {
    doSomething()
  } else {
    doSomethingElse()
  }

  // oops, I forgot the cleanup
}, [foo, bar, baz, quo])

現実世界の例:

useEffect(() => {
  if (isOpen && component && containerElRef.current) {
    if (React.isValidElement(component)) {
      ionContext.addOverlay(overlayId, component, containerElRef.current!);
    } else {
      const element = createElement(component as React.ComponentClass, componentProps);
      ionContext.addOverlay(overlayId, element, containerElRef.current!);
    }
  }
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
  if (removingValue && !hasValue && cssDisplayFlex) {
    setCssDisplayFlex(false)
  }
  setRemovingValue(false)
}, [removingValue, hasValue, cssDisplayFlex])

このコードを書くのは恐ろしいです。 さらに、それは私たちのコードベースで一般的になり、めちゃくちゃになります。 😱🤮

効果はどこに置くべきですか?

React 18 は、マウント時に(厳密モードで) 2 回効果を実行します。 Mount/effect (╯°□°)╯︵ ┻━┻ -> Unmount (シミュレートされた)/クリーンアップ ┬─┬ /( º _ º /) -> Remount/effect (╯°□°)╯︵ ┻━┻

それはコンポーネントの外側に置くべきですか? デフォルトの useEffect? うーん... 不格好です。 Hmm... 🤔 副作用があってはいけないので、render には置くことができませんでした。なぜなら、render は数学の方程式の右辺のようなものだからです。 それは、計算の結果だけであるべきです。

useEffect は何のためですか?

同期

useEffect(() => {
  const sub = createThing(input).subscribe((value) => {
    // do something with value
  })

  return sub.unsubscribe
}, [input])
useEffect(() => {
  const handler = (event) => {
    setPointer({ x: event.clientX, y: event.clientY })
  }

  elRef.current.addEventListener('pointermove', handler)

  return () => {
    elRef.current.removeEventListener('pointermove', handler)
  }
}, [])

アクション効果 vs アクティビティ効果

 Fire-and-forget            Synchronized
 (Action effects)        (Activity effects)

        0              ----------------------       ----------------- - - -
        o              o   |     A   |      o       o     | A   |   A
        o              o   |     |   |      o       o     | |   |   |
        o              o   |     |   |      o       o     | |   |   |
        o              o   |     |   |      o       o     | |   |   |
        o              o   |     |   |      o       o     | |   |   |
        o              o   |     |   |      o       o     | |   |   |
        o              o   V     |   V      o       o     V |   V   |
o-------------------------------------------------------------------------------->
                                       Unmount      Remount

アクション効果はどこに置くべきですか?

イベントハンドラ。 ある程度は。

<form
  onSubmit={(event) => {
    // 💥 side-effect!
    submitData(event)
  }}
>
  {/* ... */}
</form>

Beta React.js には素晴らしい情報があります。 ぜひ読んでみてください。 特に "イベントハンドラは副作用を持つことができますか?" の部分

もちろんです! イベントハンドラは、副作用のための最適な場所です。

もう 1 つ言及しておきたい素晴らしいリソースは 副作用が発生する場所 です。

React では、副作用は通常イベントハンドラ内におきます。

他のすべてのオプションを使い果たし、副作用に適したイベントハンドラが見つからない場合は、コンポーネント内の useEffect 呼び出しを使用して、返された JSX に副作用をアタッチできます。 これにより、React はレンダリング後に副作用を許可されるまで実行します。 ただし、このアプローチは最後の手段とする必要があります。

"効果はレンダリングの外で発生する" - David Khoursid。

(state) => UI
(state, event) => nextState // 🤔 Effects?

UI は状態の関数です。 現在の状態がすべてレンダリングされると、現在の UI が生成されます。 同様に、イベントが発生すると、新しい状態が作成されます。 状態が変化すると、新しい UI が構築されます。 このパラダイムは、React のコアです。

効果はいつ発生しますか?

ミドルウェア? 🕵️ コールバック? 🤙 サガ? 🧙‍♂️ リアクション? 🧪 シンク? 🚰 モナド(?) 🧙‍♂️ いつ? 🤷‍♂️

状態遷移。 常に。

(state, event) => nextState
          |
          V
(state, event) => (nextState, effect) // Here

![Rerender illustration image](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")

アクション効果はどこに置くべきですか? イベントハンドラ。 状態遷移。

それは、イベントハンドラと同じタイミングで実行されます。

効果がなくても良いかもしれません

React からすでにこの問題を解決できる組み込みの API があることを知らなかったため、useEffect を使用できた可能性があります。

このトピックに関する素晴らしいリソースがあります: You Might Not Need an Effect

データを変換するために useEffect は必要ありません。

useEffect ➡️ useMemo(ほとんどの場合、useMemo は必要ありません)

const Cart = () => {
  const [items, setItems] = useState([])
  const [total, setTotal] = useState(0)

  useEffect(() => {
    setTotal(items.reduce((total, item) => total + item.price, 0))
  }, [items])

  // ...
}

注意深く 🧐 再度読んで考えてみてください。

const Cart = () => {
  const [items, setItems] = useState([])
  const total = useMemo(() => {
    return items.reduce((total, item) => total + item.price, 0)
  }, [items])

  // ...
}

useEffect を使用して合計を計算する代わりに、useMemo を使用して合計をメモ化できます。 変数が高価な計算でなくても、useMemo を使用してメモ化する必要はありません。なぜなら、基本的にはパフォーマンスとメモリをトレードしているからです。

useEffect 内で setState を見かけるたびに、簡素化できる警告サインです。

外部ストアでの効果? useSyncExternalStore

useEffect ➡️ useSyncExternalStore

❌ 間違った方法:

const Store = () => {
  const [isConnected, setIsConnected] = useState(true)

  useEffect(() => {
    const sub = storeApi.subscribe(({ status }) => {
      setIsConnected(status === 'connected')
    })

    return () => {
      sub.unsubscribe()
    }
  }, [])

  // ...
}

✅ 最良の方法:

const Store = () => {
  const isConnected = useSyncExternalStore(
    // 👇 subscribe
    storeApi.subscribe,
    // 👇 get snapshot
    () => storeApi.getStatus() === 'connected',
    // 👇 get server snapshot
    true
  )

  // ...
}

親との通信のために useEffect は必要ありません。

useEffect ➡️ イベントハンドラ

❌ 間違った方法:

const ChildProduct = ({ onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(false)

  useEffect(() => {
    if (isOpen) {
      onOpen()
    } else {
      onClose()
    }
  }, [isOpen])

  return (
    <div>
      <button
        onClick={() => {
          setIsOpen(!isOpen)
        }}
      >
        Toggle quick view
      </button>
    </div>
  )
}

📈 より良い方法:

const ChildProduct = ({ onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(false)

const handleToggle = () => {
  const nextIsOpen = !isOpen;
  setIsOpen(nextIsOpen)

  if (nextIsOpen) {
    onOpen()
  } else {
    onClose()
  }
}

  return (
    <div>
      <button
        onClick={}
      >
        Toggle quick view
      </button>
    </div>
  )
}

✅ 最良の方法は、カスタムフックを作成することです。

const useToggle({ onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(false)

  const handleToggle = () => {
    const nextIsOpen = !isOpen
    setIsOpen(nextIsOpen)

    if (nextIsOpen) {
      onOpen()
    } else {
      onClose()
    }
  }

  return [isOpen, handleToggle]
}

const ChildProduct = ({ onOpen, onClose }) => {
  const [isOpen, handleToggle] = useToggle({ onOpen, onClose })

  return (
    <div>
      <button
        onClick={handleToggle}
      >
        Toggle quick view
      </button>
    </div>
  )
}

グローバルシングルトンの初期化のために useEffect は必要ありません。

useEffect ➡️ justCallIt

❌ 間違った方法:

const Store = () => {
  useEffect(() => {
    storeApi.authenticate() // 👈 This will run twice!
  }, [])

  // ...
}

🔨 修正しましょう。

const Store = () => {
  const didAuthenticateRef = useRef()

  useEffect(() => {
    if (didAuthenticateRef.current) return

    storeApi.authenticate()

    didAuthenticateRef.current = true
  }, [])

  // ...
}

➿ 別の方法:

let didAuthenticate = false

const Store = () => {
  useEffect(() => {
    if (didAuthenticate) return

    storeApi.authenticate()

    didAuthenticate = true
  }, [])

  // ...
}

🤔 もし:

storeApi.authenticate()

const Store = () => {
  // ...
}

🍷 SSR か?

if (typeof window !== 'undefined') {
  storeApi.authenticate()
}
const Store = () => {
  // ...
}

🧪 テスト?

const renderApp = () => {
  if (typeof window !== 'undefined') {
    storeApi.authenticate()
  }

  appRoot.render(<Store />)
}

コンポーネント内にすべてを配置する必要はありません。

データのフェッチのために useEffect は必要ありません。

useEffect ➡️ renderAsYouFetch (SSR) または useSWR (CSR)

❌ 間違った方法:

const Store = () => {
  const [items, setItems] = useState([])

  useEffect(() => {
    let isCanceled = false

    getItems().then((data) => {
      if (isCanceled) return

      setItems(data)
    })

    return () => {
      isCanceled = true
    }
  })

  // ...
}

💽 Remix の方法:

import { useLoaderData } from '@renix-run/react'
import { json } from '@remix-run/node'
import { getItems } from './storeApi'

export const loader = async () => {
  const items = await getItems()

  return json(items)
}

const Store = () => {
  const items = useLoaderData()

  // ...
}

export default Store

⏭️🧹 Next.js (appDir) で、サーバーコンポーネントの async/await を使用する方法:

// app/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/...')
  // The return value is *not* serialized
  // You can return Date, Map, Set, etc.

  // Recommendation: handle errors
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data')
  }

  return res.json()
}

export default async function Page() {
  const data = await getData()

  return <main></main>
}

⏭️💁 Next.js (appDir) で、クライアントコンポーネントの useSWR を使用する方法:

// app/page.tsx
import useSWR from 'swr'

export default function Page() {
  const { data, error } = useSWR('/api/data', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <div>hello {data}!</div>
}

⏭️🧹 Next.js (pagesDir) で、SSR の方法:

// pages/index.tsx
import { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async () => {
  const res = await fetch('https://api.example.com/...')
  const data = await res.json()

  return {
    props: {
      data,
    },
  }
}

export default function Page({ data }) {
  return <div>hello {data}!</div>
}

⏭️💁 Next.js (pagesDir) で、CSR の方法:

// pages/index.tsx
import useSWR from 'swr'

export default function Page() {
  const { data, error } = useSWR('/api/data', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <div>hello {data}!</div>
}

🍃 React Query (SSR の方法):

import { getItems } from './storeApi'
import { useQuery } from 'react-query'

const Store = () => {
  const queryClient = useQueryClient()

  return (
    <button
      onClick={() => {
        queryClient.prefetchQuery('items', getItems)
      }}
    >
      See items
    </button>
  )
}

const Items = () => {
  const { data, isLoading, isError } = useQuery('items', getItems)

  // ...
}

⁉️ 本当に ⁉️ 何を使うべきですか? useEffect? useQuery? useSWR?

または... use() 🤔

use() は、Promise を受け取る新しい React 関数であり、await に概念的に似ています。 use() は、コンポーネント、フック、Suspense と互換性のある方法で、関数から返された Promise を処理します。 use() については、React RFC をご覧ください。

function Note({ id }) {
  // This fetches a note asynchronously, but to the component author, it looks
  // like a synchronous operation.
  const note = use(fetchNote(id))
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
    </div>
  )
}

useEffect でのフェッチの問題

🏃‍♂️ 競合状態

🔙 インスタントバックボタンなし

🔍 SSR または初期 HTML コンテンツなし

🌊 ウォーターフォールチェイス

  • Reddit, Dan Abramov

まとめ

データのフェッチから命令型の API との戦いまで、副作用は Web アプリ開発で最も大きなフラストレーションの源の 1 つです。 正直に言って、すべてを useEffect フックに置くことは、わずかに役立つだけです。 幸いなことに、状態マシンとステートチャートで形式化された副作用の科学(まあ、数学)があり、それがどれほど複雑であっても、効果を宣言的にオーケストレーションする方法を視覚的にモデル化し、理解するのに役立ちます。

リソース