Как использовать React для замены useEffect

Как использовать React для замены useEffect

1 января 2023 г.

В этой статье я покажу вам, как использовать React для замены useEffect в большинстве случаев.

Я смотрел "Goodbye, useEffect" Дэвида Хурсида, и это 🤯 поразило меня 😀 хорошо путь. Я согласен с тем, что 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>
}

Это простой счетчик, который увеличивается каждую секунду. Он использует useEffect для установки интервала. Он также использует useEffect для очистки интервала размонтирования компонента. Приведенный выше фрагмент кода является широко распространенным вариантом использования useEffect.

Это простой пример, но это также и ужасный пример.

Проблема с этим примером заключается в том, что интервал устанавливается каждый раз, когда компонент выполняет повторную визуализацию. Если компонент по какой-либо причине повторно рендерится, интервал будет установлен снова. Интервал будет вызываться два раза в секунду. Это не проблема для этого простого примера, но может быть большой проблемой, когда интервал более сложный. Это также может привести к утечке памяти.

Как это исправить?

Есть много способов решить эту проблему. Один из способов — использовать 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 используется для выполнения побочных эффектов при монтировании, обновлении и размонтировании компонента. Но он используется не только для выполнения побочных эффектов. Он также используется для выполнения побочных эффектов при повторном рендеринге компонента. Не рекомендуется выполнять побочные эффекты при повторном рендеринге компонента. Это может вызвать множество ошибок. Лучше использовать другие хуки для выполнения побочных эффектов при повторном рендеринге компонента.

<цитата>

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-оптимизация-производительности-путем-пропуска-эффектов

Императивный и декларативный

Императив: когда что-то происходит, используйте этот эффект.

Декларативный: когда что-то происходит, это приводит к изменению состояния, и в зависимости (массив зависимостей) от того, какие части состояния изменились, этот эффект должен выполняться, но только если какое-то условие истинно. . И 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 дважды запускает эффекты на маунте (в строгом режиме). Маунт/эффект (╯°□°)╯︵ ┻━┻ -> Размонтировать (симулировать)/очистить ┬─┬ /( º _ º /) -> Перемонтировать/эффект (╯°□°)╯︵ ┻━┻

Следует ли размещать его за пределами компонента? По умолчанию useEffect? Э... неловко. Хм... 🤔 Мы не могли поместить это в рендер, так как там не должно быть побочных эффектов, потому что рендер - это как правая часть математического уравнения. Это должен быть только результат расчета.

Для чего нужен 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)
  }
}, [])

Эффекты действий и эффекты активности

 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>

В бета-версии React.js содержится отличная информация. Я рекомендую прочитать это. Особенно "Могут ли обработчики событий иметь побочные эффекты?" часть.

<цитата>

Абсолютно! Обработчики событий — лучшее место для побочных эффектов.

Я хочу упомянуть еще один замечательный ресурс: Где вы < a href="beta.reactjs.org/learn/keeping-components-pure#where-you-can-cause-side-effects">может вызвать побочные эффекты

<цитата>

В React побочные эффекты обычно относятся к обработчикам событий.

Если вы исчерпали все другие варианты и не можете найти правильный обработчик событий для вашего побочного эффекта, вы все равно можете прикрепить его к возвращенному JSX с помощью вызова useEffect в своем компоненте. Это говорит React выполнить его позже, после рендеринга, когда разрешены побочные эффекты. Однако этот подход должен быть вашим последним средством.

«Эффекты возникают вне рендеринга», — Дэвид Хурсид.

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

Пользовательский интерфейс является функцией состояния. Когда все текущие состояния отображаются, будет создан текущий пользовательский интерфейс. Точно так же, когда происходит событие, оно создает новое состояние. И когда состояние изменится, он создаст новый пользовательский интерфейс. Эта парадигма лежит в основе React.

Когда возникают эффекты?

~~Промежуточное ПО? 🕵️ Обратные звонки? 🤙 Саги? 🧙‍♂️ Реакции? 🧪 Раковины? 🚰 Монады(?) 🧙‍♂️ Когда? 🤷‍♂️~~

Переходы между состояниями. Всегда.

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

Rerender illustration image

Куда добавляются эффекты действия? ~~Обработчики событий.~~ Переходы между состояниями.

Которые выполняются одновременно с обработчиками событий

.

Нам могут не понадобиться эффекты

Мы могли бы использовать useEffect, поскольку не знаем, существует ли уже встроенный API React, способный решить эту проблему.

Вот отличный ресурс по этой теме: Вам может не понадобиться эффект

Нам не нужен 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 для ее запоминания, потому что мы в основном обмениваем производительность на память.

Всякий раз, когда мы видим setState в useEffect, это предупреждающий знак, что мы можем упростить его.

Эффекты с внешними хранилищами? 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>
  )
}

Нам не нужен 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 = () => {
  // ...
}

🍷 ССР, да?

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

⏭️🧹 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) с использованием SWR в виде клиентского компонента:

// 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? использоватьSWR?

или... просто используйте() 🤔

use() — это новая функция React, которая принимает обещание, концептуально похожее на ожидание. 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>
  )
}

Проблемы с извлечением in useEffect

<цитата>

🏃‍♂️ Условия гонки

🔙 Нет кнопки мгновенного возврата

🔍 Нет SSR или начального HTML-контента

🌊 В погоне за водопадом

  • Реддит, Дэн Абрамов

Заключение

Побочные эффекты — от получения данных до борьбы с императивными API — являются одним из самых серьезных источников разочарования при разработке веб-приложений. И давайте будем честными, использование всех хуков эффектов помогает лишь немного. К счастью, существует наука (ну, математика) о побочных эффектах, формализованная в конечных автоматах и ​​диаграммах состояний, которые могут помочь нам визуально смоделировать и понять, как управлять эффектами, какими бы сложными они ни были в декларативном плане.

Ресурсы


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE