paint-brush
React を使用して useEffect を置き換える方法@imamdev
3,449 測定値
3,449 測定値

React を使用して useEffect を置き換える方法

Imamuzzaki Abu Salam16m2022/12/31
Read on Terminal Reader

長すぎる; 読むには

useEffect は、関数コンポーネントで副作用を実行できるようにするフックです。 componentDidMount、componentDidUpdate、および componentWillUnmount を 1 つの API に結合します。これは、多くのことを可能にする魅力的なフックです。しかし、これは非常に危険なフックでもあり、多くのバグを引き起こす可能性があります。この記事では、ほとんどの場合、react を使用して useEffect を置き換える方法を紹介します。
featured image - React を使用して useEffect を置き換える方法
Imamuzzaki Abu Salam HackerNoon profile picture

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


私はDavid Khoursid の "Goodbye, useEffect" を見てきました。 useEffect があまりにも多く使用されているため、コードが汚れて保守が難しくなっていることに同意します。私は長い間 useEffect を使用してきましたが、それを誤用した罪があります。 React には、私のコードをよりクリーンで保守しやすくする機能があると確信しています。

useEffectとは?

useEffect は、関数コンポーネントで副作用を実行できるようにするフックです。 componentDidMount、componentDidUpdate、および componentWillUnmount を 1 つの 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> }

毎秒増加する単純なカウンターです。 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 を 1 つの API に結合します。その例をいくつか挙げましょう。

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

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


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


命令型と宣言型

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

宣言型 : 何かが発生すると、状態が変化し、状態のどの部分が変化したか (依存関係配列) に応じて、この効果を実行する必要がありますが、何らかの条件が真の場合に限ります。そして、React は、理由もなく同時レンダリングでそれを再度実行する可能性があります。


コンセプトと実装

コンセプト

 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 回効果を実行します (厳格モード)。マウント/効果 (╯°□°)╯︵ ┻━┻ -> アンマウント (シミュレート)/クリーンアップ ┬─┬ /( º _ º /) -> 再マウント/効果 (╯°□°)╯︵ ┻━┻

コンポーネントの外側に配置する必要がありますか?デフォルトの useEffect?うーん…ぎこちない。うーん... 🤔 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 ---------------------- ----------------- - - - oo | A | oo | A | A oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo V | V oo V | V | o--------------------------------------------------------------------------------> Unmount Remount

アクション効果はどこに行くの?

イベント ハンドラー。ちょっと。

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

Beta React.js には優れた情報があります。読むことをお勧めします。特に、 「イベント ハンドラーに副作用はありますか?」一部


絶対! <u>イベント ハンドラーは、副作用に最適な場所です。</u>


私が言及したいもう1つの優れたリソースは、副作用を引き起こす可能性がある場所です


React では、<u>通常、副作用はイベント ハンドラ内に属します。</u>

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


「エフェクトはレンダリングの外で発生します」 - David Khoursid.

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


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

効果はいつから?

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

状態遷移。いつも。

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

イラスト画像の再レンダリング

アクション効果はどこに行くの?イベント ハンドラー。状態遷移。

これはたまたまイベント ハンドラーと同時に実行されます。

エフェクトは必要ないかもしれません

この問題を解決できる React の組み込み API が既に存在することを知らないため、useEffect を使用できます。


このトピックについて読むための優れたリソースは次のとおりです。効果は必要ないかもしれません

データの変換に 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を使用して変数をメモする必要はありません。


setStateuseEffectがある場合は、単純化できるという警告サインです。

外部ストアとの影響? 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 ➡️ eventHandler

❌ 間違った方法:

 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> ) }

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

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 } }) // ... }

💽 リミックス方法:

 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

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

 // 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> }

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

 // 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> }

⏭️🧹 SSR 方式の Next.js (pagesDir):

 // 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> }

⏭️💁 CSR 方式の Next.js (pagesDir):

 // 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) // ... }

⁉️本当に⁉️


何を使うべきですか?使用効果?クエリを使用しますか? SWRを使用しますか?

または...単に()を使用します🤔


use() は、概念的に await に似た promise を受け入れる新しい React 関数です。 use() は、コンポーネント、フック、およびサスペンスと互換性のある方法で、関数によって返された promise を処理します。 React RFC の use() の詳細をご覧ください。

 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> ) }

使用中のフェッチ効果の問題

🏃‍♂️レース条件

🔙 すぐに戻るボタンはありません

🔍 SSRや初期HTMLコンテンツはありません

🌊追い滝

  • Reddit、ダン・アブラモフ

結論

データのフェッチから命令型 API との闘いまで、副作用は Web アプリ開発におけるフラストレーションの最も重大な原因の 1 つです。そして正直に言うと、すべてを useEffect フックに入れても、少ししか役に立ちません。ありがたいことに、ステート マシンとステートチャートで形式化された副作用に対する科学 (まあ、数学) があります。これは、視覚的にモデル化し、効果が宣言的にどれほど複雑になっても、効果を調整する方法を理解するのに役立ちます。

資力