Бесконечный цикл при сохранении объекта из async await

Когда я создаю и объект из операции async / await ...

export const getData = async datas => {
  const a1 = await getData1(datas);
  return { a1 };
};

... а затем сохраните его с помощью useState ...

import { useState, useEffect } from "react";
import { getData } from "./getData";

export const useData = ababab => {
  const [data, setData] = useState();

  useEffect(() => {
    const loadData = async () => {
      const newData = await getData(ababab);
      setData(newData);
    };
    console.log(Date.now().toString());
    loadData();
  }, [ababab]);

  return data;
};

... У меня бесконечный цикл. Я не понимаю. Если вы закомментируете setData - он не будет зацикливаться. Если вы вернете только a1, цикл не будет.

Вот где используется useData:

import React from "react";
import "./styles.css";
import { useAbabab } from "./usaAbabab";
import { useData } from "./useData";

export default function App() {
  const ababab = useAbabab();
  const data = useData(ababab);
  return (
    <div className="App">
      <h1>Hello CodeSandbox {data && data.a1}</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

А вот содержимое useAbabab:

import { useState } from "react";

export const useAbabab = () => {
  const [aaa, setAaa] = useState(0);
  const [bbb, setBbb] = useState(5);

  return { aaa, bbb, setAaa, setBbb };
};

Пример тестовой среды кода

 Редактировать cool-sun-wq1f6


person mikes    schedule 10.05.2020    source источник


Ответы (2)


Как вы, наверное, догадались, бесконечный цикл вызывается useEffect в useData, который запускается изменением на ababab (можно показать, удалив ababab из массива зависимостей).

В то время как ababab на самом деле всего лишь два полных useState выхода вместе в объекте, сам объект переопределяется при каждом рендеринге, вызывая запуск useEffect.

Самый простой способ исправить это - обернуть возвращаемое значение useAbabab в useMemo, например:

import { useState, useMemo } from "react";

export const useAbabab = () => {
  const [aaa, setAaa] = useState(0);
  const [bbb, setBbb] = useState(5);

  return useMemo(() => ({ aaa, bbb, setAaa, setBbb }), [aaa, bbb]);
};

person Henry Woody    schedule 10.05.2020
comment
@mikes Я думаю, что это в основном та же идея, но поскольку a1 - это строка, ее можно просто проверить на наличие изменений, используя равенство неглубоких строк, но когда он заключен в объект, React проверяет равенство идентичности объекта, а не просто наличие тех же ключей / значений . Если вы используете объект, лучше обернуть его useMemo или поместить определенные значения в массивы зависимостей вместо того, чтобы помещать объект целиком. - person Henry Woody; 10.05.2020
comment
Я проверил его еще раз, и он действительно не работал после того, как я переключился с объекта. Не знаю, почему это иногда срабатывало. В любом случае, ваш ответ устранил проблему. Большое спасибо! :) - person mikes; 10.05.2020

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

const identity = x => x

const useAsync = (runAsync = identity, deps = []) => {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)

  useEffect(_ => { 
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
      .finally(_ => setLoading(false))
  }, deps)

  return { loading, error, result }
}

Использование нашего пользовательского хука usAsync выглядит так:

function App() {
  const ababab =
    useAbabab()

  const { loading, error, result } = 
    useAsync(getData, [ababab]) // async function, args to function

  if (loading)
    return <p>Loading...</p>

  if (error)
    return <p>Error: {error.message}</p>

  return <div>Got data: {result}</div>
}

useAsync - это универсальный универсальный хук, который может быть специализирован другими полезными способами:

const fetchJson = (url = "") =>
  fetch(url).then(r => r.json()) // <-- stop repeating yourself

const useJson = (url = "") =>
  useAsync(fetchJson, [url]) // <-- useAsync

const MyComponent = ({ url = "" }) => {
  const { loading, error, result } =
    useJson(url)                       // <-- dead simple

  if (loading)
    return <pre>loading...</pre>

  if (error)
    return <pre className="error">error: {error.message}</pre>

  return <pre>result: {result}</pre>
}

ReactDOM.render(
  <MyComponent url="https://httpbin.org/get?foo=bar" />,
  document.body
)

Запустите приведенный ниже фрагмент, чтобы увидеть, как useAsync и useJson работают в вашем собственном браузере -

const { useState, useEffect } =
  React

// fake fetch slows response down so we can see loading
const _fetch = (url = "") =>
  fetch(url).then(x =>
    new Promise(r => setTimeout(r, 2000, x)))

const identity = x => x

const useAsync = (runAsync = identity, deps = []) => {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)

  useEffect(_ => { 
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
      .finally(_ => setLoading(false))
  }, deps)

  return { loading, error, result }
}

const fetchJson = (url = "") =>
  _fetch(url).then(r => r.json())

const useJson = (url = "") =>
  useAsync(fetchJson, [url])

const MyComponent = ({ url = "" }) => {
  const { loading, error, result } =
    useJson(url)

  if (loading)
    return <pre>loading...</pre>

  if (error)
    return <pre style={{color: "tomato"}}>error: {error.message}</pre>

  return <pre>result: {JSON.stringify(result, null, 2)}</pre>
}

const MyApp = () =>
  <main>
    ex 1 (success):
    <MyComponent url="https://httpbin.org/get?foo=bar" />

    ex 2 (error):
    <MyComponent url="https://httpbin.org/status/500" />
  </main>

ReactDOM.render(<MyApp />, document.body)
pre {
  background: ghostwhite;
  padding: 1rem;
  white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

person Mulan    schedule 10.05.2020
comment
Спасибо за ваше время, предложение и ответ! Я использовал ababab, чтобы абстрагироваться от проблемы, и, как показал ответ @ henry-woody, все дело в том, чтобы не запоминать полученный объект из хука useAbabab. Это вызвало ситуацию, когда каждый рендер возвращал новый экземпляр, который запускал useEffect - и он никогда не останавливался. - person mikes; 10.05.2020
comment
@mikes это не столько о мемоизации, сколько о том, являются ли a и b скалярными аргументами. В исходном коде вы вызывали useEffect(..., [ababab]), где ababab - каждый раз новый объект. Это сильно отличается от useEffect(..., ababab), который представляет собой плоский массив. И это тоже другое, const [a, b, setA, setB] = useAbaba(); useEffect(..., [a, b]) где функции setA и setB не передаются (потому что они не являются зависимостями). Последний способ - правильный способ использования useEffect. - person Mulan; 10.05.2020
comment
То есть, я сомневаюсь, что у вас есть реальное применение useMemo. Да, вы можете подумать, что это работает в этом конкретном сценарии, но вам бы это никогда не понадобилось, если бы вы изначально не использовали useEffects неправильно. - person Mulan; 10.05.2020
comment
Я так не думал об этом: О Да, отличный совет, и я поиграю с этим, используя ваши предложения! Спасибо! - person mikes; 11.05.2020