منشور في

Salaan React, Salaan useEffect (Eebbe ka soo degay, sidaas ha ahaato)

الكتاب

Fi ɣiɣ article, a gna nẓṛ kif nstɛml React ɣir useEffect fi lɛṛab n lḥalāt.

Lamma twassalt "Goodbye, useEffect" n David Khoursid, tḥzzat 🤯 ɣṛay ɣɣṛay ɣir 😀 fi cchi way n lḥsan. Aghn mlih nẓṛ kif n stɛml useEffect fi waqt iwa, a yaḥḍr code na diṛ w yaɛṛb ɣir n nḥḍr. Aghn nstɛml useEffect ɣir waqt iwa, w a yaḥḍr aḥḍṛ n mistɛml diya. Aghn ḥḍr aḥḍṛ n React fi code na yaḥḍr w yaɛṛb ɣir n nḥḍr.

Kif n stɛml useEffect?

useEffect g hu hook ya khṭṛna n nḍṛ side effects fi function components. Ya ḥḍṛ componentDidMount, componentDidUpdate, w componentWillUnmount fi API hwa. G hu hook n tḥḍṛ n ya khṭṛna n nḍṛ n chi waqt iwa. Mawn g hu hook n ḥḍṛ ya khṭṛna n ykhṭṛ n chi bugs.

Lmṛa g hu useEffect n ḥḍṛ?

A gna nẓṛ lḥal n lḥal hwa:

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

G hu counter n tḥḍṛ ya khṭṛ n yẓḍṛ kul tanniwt. Ya stɛml useEffect n yḥḍṛ interval. Ya stɛml useEffect n yfrṛ interval lma component ya tfṛṛ. Lḥal n code hwa g hu lḥal n tḥḍṛ n tḥḍṛ n useEffect. G hu lḥal n tḥḍṛ, mawn g hu lḥal n tḥḍṛ.

Lmṛa n lḥal hwa g hu interval ya ḥḍṛ kul lma component ya tḥḍṛ. Lma component ya tḥḍṛ fi chi way, interval a ya ḥḍṛ thani. Interval a ya ḥḍṛ j n waqt fi tanniwt. G hu lmṛa fi lḥal n tḥḍṛ hwa, mawn ya khṭṛ n ykhṭṛ lma interval ya khṭṛ. Ya khṭṛ n ykhṭṛ memory leaks.

Kif n yfrṛ diya?

G hu n waqt n yfrṛ lḥal hwa. Waqt n waqt g hu n stɛml useRef n nḥḍṛ interval.

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

Lḥal n code hwa a mlih ɣir lḥal n code n waqt iwa. Ya ḥḍṛ interval kul lma component ya tḥḍṛ. Mawn ya khṭṛ n yfrṛ diya. G hu thani n tḥḍṛ. Ya stɛml useEffect, g hu hook n ḥḍṛ.

useEffect g hu ɣir n effects

Kif n ḥḍr ɣir useEffect, ya ḥḍṛ componentDidMount, componentDidUpdate, w componentWillUnmount fi API hwa. A gna nḍṛ n chi lḥalāt diya:

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

G hu ya ɛṛb n tḥḍṛ. useEffect ya stɛml n nḍṛ side effects lma component ya tḥḍṛ, ya tḥḍṛ thani, w ya tfṛṛ. Mawn g hu ɣir n stɛml n nḍṛ side effects. Ya stɛml thani n nḍṛ side effects lma component ya tḥḍṛ. G hu ḥḍṛ n tḥḍṛ n nḍṛ side effects lma component ya tḥḍṛ. A mlih n stɛml hooks n waqt n nḍṛ side effects lma component ya tḥḍṛ.

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 g hu ɣir state setter

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

Aghn ya nḥḍṛ lḥal n documentation hwa: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Imperative vs Declarative

Imperative: Lma chi way ya khṭṛ, a nḍṛ lḥal hwa.

Declarative: Lma chi way ya khṭṛ, a ya khṭṛ n tḥḍṛ n state, w n tḥḍṛ (dependency array) n chi parts n state ya tḥḍṛ, lḥal hwa a ya ḥḍṛ, mawn ɣir lma chi condition ya ḥḍṛ. W React ya khṭṛ n yḥḍṛ diya thani ɣir n chi way n concurrent rendering.

Concept vs Implementation

Concept:

useEffect(() => {
  doSomething()

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

Implementation:

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

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

Real-world implementation:

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

G hu ya khṭṛ n nḍṛ lḥal n code hwa. Thani wa, a ya khṭṛ n tḥḍṛ fi codebase na w ya ɛṛb diya. 😱🤮

Fi chi way ya khṭṛ effects?

React 18 ya ḥḍṛ effects j n waqt fi mount (fi strict mode). Mount/effect (╯°□°)╯︵ ┻━┻ -> Unmount (simulated)/cleanup ┬─┬ /( º _ º /) -> Remount/effect (╯°□°)╯︵ ┻━┻

Ya khṭṛ n yḥḍṛ diya ɣir n component? useEffect n tḥḍṛ? Uh... awkward. Hmm... 🤔 Ya khṭṛ n yḥḍṛ diya fi render lma ya khṭṛ n ykhṭṛ ɣir n side-effects diya lma render g hu ɣir kif lḥan n lḥisāb. Ya khṭṛ n ykhṭṛ ɣir n ntij n lḥisāb.

Kif n stɛml useEffect?

Synchronization

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)
  }
}, [])

Action effects vs Activity effects

 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

Fi chi way ya khṭṛ action effects?

Event handlers. Sorta.

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

G hu n tḥḍṛ n tḥḍṛ fi Beta React.js. Aghn ya nḥḍṛ diya. Surt "Can event handlers have side effects?" part](https://beta.reactjs.org/learn/responding-to-events#can-event-handlers-have-side-effects).

Absolutely! Event handlers are the best place for side effects.

Another great resource I want to mention is Where you can cause side effects

In React, side effects usually belong inside event handlers.

If you've exhausted all other options and can't find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort.

"Effects happen outside of rendering" - David Khoursid.

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

UI g hu function n state. Kif kul n state ya tḥḍṛ, a ya khṭṛ UI n tḥḍṛ. N waqt n waqt, lma event ya khṭṛ, a ya khṭṛ state n tḥḍṛ. W lma state ya tḥḍṛ, a ya bni UI n tḥḍṛ. Lḥal n paradigm hwa g hu core n React.

Fi chi waqt ya khṭṛ effects?

Middleware? 🕵️ Callbacks? 🤙 Sagas? 🧙‍♂️ Reactions? 🧪 Sinks? 🚰 Monads(?) 🧙‍♂️ Whenever? 🤷‍♂️

State transitions. Always.

(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")

Fi chi way ya khṭṛ action effects? Event handlers. State transitions.

Ya khṭṛ n yḥḍṛ diya fi waqt n waqt kif event handlers.

Ya khṭṛ n ykhṭṛ effects

Ya khṭṛ n stɛml useEffect lma ya ḥḍṛ ɣir n chi API n tḥḍṛ n React ya khṭṛ n yfrṛ lḥal hwa.

G hu n tḥḍṛ n tḥḍṛ n tḥḍṛ n topic hwa: You Might Not Need an Effect

Ya khṭṛ n ykhṭṛ useEffect n transforming data.

useEffect ➡️ useMemo (mwn ya khṭṛ n ykhṭṛ useMemo fi lɛṛab n lḥalāt)

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

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

  // ...
}

A gna nḍṛ w a gna nḥḍṛ diya thani fi chi way n tḥḍṛ 🧐.

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

  // ...
}

N waqt n stɛml useEffect n yḥḍṛ total, ya khṭṛ n stɛml useMemo n ymemoize total. Mwn lma variable g hu ɣir n ḥisāb n tḥḍṛ, ya khṭṛ n ykhṭṛ n stɛml useMemo n ymemoize diya lma a tna ḥḍṛ performance ɣir memory.

Kul lma a gna nẓṛ setState fi useEffect, g hu sign n tḥḍṛ ya khṭṛ n yfrṛ diya.

Effects ɣir external stores? useSyncExternalStore

useEffect ➡️ useSyncExternalStore

❌ Way n tḥḍṛ:

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

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

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

  // ...
}

✅ Best way:

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

  // ...
}

Ya khṭṛ n ykhṭṛ useEffect n communicating ɣir parents.

useEffect ➡️ eventHandler

❌ Way n tḥḍṛ:

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

📈 Best way:

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

✅ Best way g hu n ykhṭṛ custom hook:

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

Ya khṭṛ n ykhṭṛ useEffect n initializing global singletons.

useEffect ➡️ justCallIt

❌ Way n tḥḍṛ:

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

  // ...
}

🔨 A gna nfrṛ diya:

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

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

    storeApi.authenticate()

    didAuthenticateRef.current = true
  }, [])

  // ...
}

➿ Another way:

let didAuthenticate = false

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

    storeApi.authenticate()

    didAuthenticate = true
  }, [])

  // ...
}

🤔 Kif lma:

storeApi.authenticate()

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

🍷 SSR, huh?

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

🧪 Testing?

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

  appRoot.render(<Store />)
}

Ya khṭṛ n ykhṭṛ n yḥḍṛ kul chi fi component.

Ya khṭṛ n ykhṭṛ useEffect n fetching data.

useEffect ➡️ renderAsYouFetch (SSR) or useSWR (CSR)

❌ Way n tḥḍṛ:

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

  useEffect(() => {
    let isCanceled = false

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

      setItems(data)
    })

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

  // ...
}

💽 Remix way:

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) ɣir async/await fi Server Component way:

// 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) ɣir useSWR fi Client Component way:

// 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) fi SSR way:

// 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) fi CSR way:

// 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 way:

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)

  // ...
}

⁉️ Really ⁉️ Fi chi way ya khṭṛ n stɛml? useEffect? useQuery? useSWR?

or... just use() 🤔

use() g hu function n tḥḍṛ n React ya khṭṛ n yḥḍṛ promise fi chi way n tḥḍṛ kif await. use() ya ḥḍṛ promise ya ḥḍṛ ɣir function fi chi way ya khṭṛ n yḥḍṛ ɣir components, hooks, w Suspense. A gna nẓṛ thani ɣir use() fi 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>
  )
}

Fetching fi useEffect problems

🏃‍♂️ Race conditions

🔙 No instant back button

🔍 No SSR or initial HTML content

🌊 Chasing waterfall

  • Reddit, Dan Abramov

Conclusion

From fetching data to fighting with imperative APIs, side effects are one of the most significant sources of frustration in web app development. And let's be honest, putting everything in useEffect hooks only helps a little. Thankfully, there is a science (well, math) to side effects, formalized in state machines and statecharts, that can help us visually model and understand how to orchestrate effects, no matter how complex they get declaratively.

Resources