1

When I create and object out of async/await operation...

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

...and then save it with 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;
};

...I get an infinite loop. I don't get it. If you comment out the setData - it won't loop. If you return just a1, it will not loop.

Here is where useData is used:

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

And here are the contents of useAbabab:

import { useState } from "react";

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

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

Code Sandbox example

Edit cool-sun-wq1f6

Henry Woody
  • 14,024
  • 7
  • 39
  • 56
Michał J. Gąsior
  • 1,457
  • 3
  • 21
  • 39

2 Answers2

2

As you've probably gathered, the infinite loop is caused by the useEffect in useData, which is triggered by a change to ababab (can be shown by removing ababab from the dependency array).

While ababab is really just two full useState outputs together in an object, the object itself is redefined on each render, triggering the useEffect to run.

The simplest way I can think to fix this is to wrap the return value of useAbabab in a useMemo, like this:

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

Henry Woody
  • 14,024
  • 7
  • 39
  • 56
  • 1
    @mikes I think that's basically the same idea, but since `a1` is a string it can just be checked for changes using shallow string equality, but when wrapped in an object, React checks for object identity equality rather than just having the same keys/values. If you're using object, it's good to wrap in `useMemo` or put specific values in dependency arrays instead of putting the whole object. – Henry Woody May 10 '20 at 19:54
  • I have checked it again and it actually wasn't working after I switched from an object. I don't know why it sometimes worked. Anyway, Your answer fixed the issue. Big thanks! :) – Michał J. Gąsior May 10 '20 at 19:57
1

It's hard to tell precisely what your code is doing because of the ababa variable names, but from what I can read in your code, it looks like you're want a generic hook around an asynchronous resource -

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

Using our custom hook usAsync looks like this -

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 is a versatile generic hook that can be specialized in other useful ways -

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
)

Run the snippet below to see useAsync and useJson working in your own browser -

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>
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thank You for Your time, suggestion and answer! I used ababab to abstract the problem out, and as the answer provided by @henry-woody showed, it was all about not memoizing the resulting object from useAbabab hook. That caused a situation where every render returned a new instance which triggered the useEffect - and it would never stop. – Michał J. Gąsior May 10 '20 at 20:46
  • 1
    @mikes it's less about memoization and more whether `a` and `b` are scalar arguments are not. In your original code you were calling `useEffect(..., [ababab])` where `ababab` is a new object each time. This is very different than `useEffect(..., ababab)` which a flat array. And this is also different, `const [a, b, setA, setB] = useAbaba(); useEffect(..., [a, b])` where the `setA` and `setB` functions are not passed (because they're not dependencies). This last way is the correct way to use `useEffect`. – Mulan May 10 '20 at 20:54
  • 1
    Ie, I doubt you have an actual use for `useMemo`. Yes you may think it works in this particular scenario, but you never would've needed it if you weren't using `useEffects` incorrectly in the first place. – Mulan May 10 '20 at 20:56
  • 1
    I haven't thought about this that way :O Yeah, great advice and I'll play around with this using your suggestions! Thx! – Michał J. Gąsior May 10 '20 at 21:16