React Hook Попередження для асинхронної функції в useEffect: функція useEffect повинна повертати функцію очищення або нічого


119

Я пробував приклад useEffect приблизно так, як показано нижче:

useEffect(async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}, []);

і я отримую це попередження на своїй консолі. Але очищення є необов’язковим для асинхронних дзвінків, я думаю. Я не впевнений, чому отримую це попередження. Посилання на пісочницю для прикладів. https://codesandbox.io/s/24rj871r0p введіть тут опис зображення

Відповіді:


166

Пропоную подивитися на відповідь Дана Абрамова (одного з основних супроводжувачів React) :

Я думаю, ти робиш це більш складним, ніж це має бути.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Більш довгостроково ми не будемо заохочувати цю закономірність, оскільки вона заохочує умови змагань. Наприклад, - між початком та закінченням вашого дзвінка може статися що завгодно, і ви могли отримати новий реквізит. Натомість ми рекомендуємо Suspense для отримання даних, які будуть виглядати більш схожими

const response = MyAPIResource.read();

і ніяких ефектів. Але тим часом ви можете перемістити асинхронний матеріал в окрему функцію і викликати його.

Детальніше про експериментальну напругу можна прочитати тут .


Якщо ви хочете використовувати функції зовні за допомогою eslint.

 function OutsideUsageExample() {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data')
    response = await response.json()
    dataSet(response)
  }, [])

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}

З useCallback useCallback . Пісочниця .

import React, { useState, useEffect, useCallback } from "react";

export default function App() {
  const [counter, setCounter] = useState(1);

  // if counter is changed, than fn will be updated with new counter value
  const fn = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  // if counter is changed, than fn will not be updated and counter will be always 1 inside fn
  /*const fnBad = useCallback(() => {
      setCounter(counter + 1);
    }, []);*/

  // if fn or counter is changed, than useEffect will rerun
  useEffect(() => {
    if (!(counter % 2)) return; // this will stop the loop if counter is not even

    fn();
  }, [fn, counter]);

  // this will be infinite loop because fn is always changing with new counter value
  /*useEffect(() => {
    fn();
  }, [fn]);*/

  return (
    <div>
      <div>Counter is {counter}</div>
      <button onClick={fn}>add +1 count</button>
    </div>
  );
}

Ви можете вирішити проблеми з умовою перегонів, перевіривши, чи не useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Річард

1
Ви також можете використовувати пакет із назвою use-async-effect . Цей пакет дозволяє використовувати синтаксис async await.
KittyCat

Використання функції самовиклику, яка не дозволяє асинхронному витоку визначити функцію useEffect або користувацьку реалізацію функції, яка ініціює виклик асинхронізації як обгортку навколо useEffect, є найкращим вибором на даний момент. Хоча ви можете включити новий пакет, як запропонований use-async-effect, я думаю, що це проста проблема для вирішення.
Тулані Чівандіква,

1
Гей, це добре, і те, що я роблю більшість разів. але eslintпросить мене зробити fetchMyAPI()залежністьuseEffect
Пракаш Редді Потлападу

51

Коли ви використовуєте асинхронну функцію типу

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

він повертає обіцянку і useEffectне очікує, що функція зворотного виклику поверне Promise, швидше за все, вона очікує, що нічого не повернеться або функція не повернеться.

Як обхідне рішення для попередження ви можете використовувати функцію самовиклику асинхронизації.

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

або, щоб зробити його більш чистим, ви можете визначити функцію, а потім викликати її

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

друге рішення полегшить читання та допоможе написати код для скасування попередніх запитів, якщо буде запущено новий, або збереже останню відповідь на запит у стані

Робочі коди і скринька


Пакет для полегшення цього зроблено. Ви можете знайти його тут .
KittyCat,

1
але еслінт не потерпить цього
Мухаймін ЦС

1
немає можливості виконати зворотний виклик очищення / зняття
David Rearte

1
@ShubhamKhatri, коли ви використовуєте, useEffectви можете повернути функцію для очищення, як скасування підписки на події. Коли ви використовуєте функцію асинхронізації, ви нічого не можете повернути, тому що useEffectне будете чекати результату
Девід Реарте

2
ти хочеш сказати, що я можу помістити функцію очищення в асинхронну? я спробував, але мою функцію очищення просто ніколи не викликають. Чи можете ви навести невеликий приклад?
David Rearte

32

Поки React не надає кращого способу, ви можете створити помічника useEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

Тепер ви можете передати асинхронну функцію:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);

8
Причиною того, що React не дозволяє автоматично використовувати функції асинхронізації в useEffect, є те, що у величезній частині випадків необхідне деяке очищення. Функція, useAsyncEffectяк ви її написали, може легко ввести когось в оману, якщо він поверне функцію очищення з їх асинхронного ефекту, яка буде запущена у відповідний час. Це може призвести до витоків пам'яті або ще гірших помилок, тому ми вирішили заохотити людей рефакторингувати свій код, щоб зробити "шов" асинхронних функцій, що взаємодіють із життєвим циклом React, більш помітним, а поведінка коду в результаті, сподіваємось, більш продуманою та правильною.
Софі Альперт,

8

Я прочитав це запитання і відчуваю, що найкращий спосіб реалізації useEffect не згадується у відповідях. Скажімо, у вас є телефонний дзвінок, і ви хотіли б щось зробити, як тільки отримаєте відповідь. Для простоти збережемо мережеву відповідь у змінній стану. Можливо, захочеться скористатися дією / редуктором для оновлення сховища з мережевою відповіддю.

const [data, setData] = useState(null);

/* This would be called on initial page load */
useEffect(()=>{
    fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(data => {
        setData(data);
    })
    .catch(err => {
        /* perform error handling if desired */
    });
}, [])

/* This would be called when store/state data is updated */
useEffect(()=>{
    if (data) {
        setPosts(data.children.map(it => {
            /* do what you want */
        }));
    }
}, [data]);

Довідка => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects


2

тут може бути використаний оператор void .
Замість:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);

або

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);

Ви можете написати:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);

Він трохи чистіший і гарніший.


Ефекти асинхронізації можуть спричинити витік пам’яті, тому важливо провести очистку при відключенні компонента. У разі отримання це може виглядати так:

function App() {
    const [ data, setData ] = React.useState([]);

    React.useEffect(() => {
        const abortController = new AbortController();
        void async function fetchData() {
            try {
                const url = 'https://jsonplaceholder.typicode.com/todos/1';
                const response = await fetch(url, { signal: abortController.signal });
                setData(await response.json());
            } catch (error) {
                console.log('error', error);
            }
        }();
        return () => {
            abortController.abort(); // cancel pending fetch request on component unmount
        };
    }, []);

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

1

спробуй

const MyFunctionnalComponent: React.FC = props => {
  useEffect(() => {
    // Using an IIFE
    (async function anyNameFunction() {
      await loadContent();
    })();
  }, []);
  return <div></div>;
};


1

Для інших читачів помилка може виникати через те, що відсутні дужки, що обгортають функцію асинхронізації:

Розглядаючи функцію асинхронізації initData

  async function initData() {
  }

Цей код призведе до вашої помилки:

  useEffect(() => initData(), []);

Але цей, не буде:

  useEffect(() => { initData(); }, []);

(Зверніть увагу на дужки навколо initData ()


1
Блискуче, чоловіче! Я використовую сагу, і ця помилка з’явилася, коли я викликав творця дії, який повернув єдиний об’єкт. Схоже, функція callEffect зворотного виклику не лиже такої поведінки. Я ціную вашу відповідь.
Gorr1995

2
Про всяк випадок, коли люди задаються питанням, чому це правда ... Без фігурних дужок повернене значення initData () неявно повертається функцією стрілки. З фігурними дужками нічого не повертається неявно, і, отже, помилка не відбудеться.
Marnix.hoh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.