Inifinite loop when saving an object from async await - reactjs

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

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

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>

Related

custom hooks return value doesn't change in Component

My custom hook fetches data asynchronously. When it is used in a component, returned value doesn't get updated. It keeps showing default value. Does anybody know what is going on? Thank you!
import React, {useState, useEffect} from 'react'
import { getDoc, getDocs, Query, DocumentReference, deleteDoc} from 'firebase/firestore'
export const useFirestoreDocument = <T>(docRef: DocumentReference<T>) => {
const [value, setValue] = useState<T|undefined>(undefined)
const [isLoading, setIsLoading] = useState<boolean>(true)
const update = async () => {
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
const data = docSnap.data()
setValue(data)
}
setIsLoading(false)
}
useEffect(() => {
update()
}, [])
console.log(value, isLoading) // it can shows correct data after fetching
return {value, isLoading}
}
import { useParams } from 'react-router-dom'
const MyComponent = () => {
const {userId} = useParams()
const docRef = doc(db, 'users', userId!)
const {value, isLoading} = useFirestoreDocument(docRef)
console.log(value, isLoading) // keeps showing {undefined, true}.
return (
<div>
...
</div>
)
}
It looks like youe hook is only being executed once upon rendering, because it is missing the docRef as a dependency:
export const useFirestoreDocument = <T>(docRef: DocumentReference<T>) => {
const [value, setValue] = useState<T|undefined>(undefined)
const [isLoading, setIsLoading] = useState<boolean>(true)
useEffect(() => {
const update = async () => {
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
const data = docSnap.data()
setValue(data)
}
setIsLoading(false)
}
update()
}, [docRef])
console.log(value, isLoading) // it can shows correct data after fetching
return {value, isLoading}
}
In addition: put your update function definition inside the useEffect hook, if you do not need it anywhere else. Your linter will complaing about the exhaustive-deps rule otherwise.
The useEffect hook is missing a dependency on the docRef:
export const useFirestoreDocument = <T>(docRef: DocumentReference<T>) => {
const [value, setValue] = useState<T|undefined>(undefined);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
const update = async () => {
setIsLoading(true);
try {
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const data = docSnap.data();
setValue(data);
}
} catch(error) {
// handle any errors, log, etc...
}
setIsLoading(false);
};
update();
}, [docRef]);
return { value, isLoading };
};
The render looping issue is because docRef is redeclared each render cycle in MyComponent. You should memoize this value so a stable reference is passed to the useFirestoreDocument hook.
const MyComponent = () => {
const {userId} = useParams();
const docRef = useMemo(() => doc(db, 'users', userId!), [userId]);
const {value, isLoading} = useFirestoreDocument(docRef);
console.log(value, isLoading);
return (
<div>
...
</div>
);
};

TypeError: Cannot read property 'map' of undefined React Hooks

I need some help understanding why I'm getting the error from the title: 'TypeError: Cannot read property 'map' of undefined'. I need to render on the page (e.g state & country here) some data from the API, but for some reason is not working.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const APIFetch = () => {
const [user, setUser] = useState('');
const [info, setInfo] = useState([]);
const fetchData = async () => {
const data = await axios.get('https://randomuser.me/api');
return JSON.stringify(data);
}
useEffect(() => {
fetchData().then((res) => {
setUser(res)
setInfo(res.results);
})
}, [])
const getName = user => {
const { state, country } = user;
return `${state} ${country}`
}
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>
})}
</div>
)
}
Can you guys provide me some help? Thanks.
Try this approach,
const APIFetch = () => {
const [user, setUser] = useState("");
const [info, setInfo] = useState([]);
const fetchData = async () => {
const data = await axios.get("https://randomuser.me/api");
return data; <--- Heres is the first mistake
};
useEffect(() => {
fetchData().then((res) => {
setUser(res);
setInfo(res.data.results);
});
}, []);
const getName = (user) => {
const { state, country } = user.location; <--- Access location from the user
return `${state} ${country}`;
};
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>;
})}
</div>
);
};
Return data without stringify inside the fetchData.
Access user.location inside getName.
Code base - https://codesandbox.io/s/sharp-hawking-6v858?file=/src/App.js
You do not need to JSON.stringify(data);
const fetchData = async () => {
const data = await axios.get('https://randomuser.me/api');
return data.data
}
Do it like that
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const APIFetch = () => {
const [user, setUser] = useState('');
const [info, setInfo] = useState([]);
useEffect(() => {
const fetchData = async () => {
const res = await axios.get('https://randomuser.me/api');
setUser(res.data);
setInfo(res.data.results);
}
featchData();
}, [])
const getName = user => {
const { state, country } = user;
return `${state} ${country}`
}
return (
<div>
{info.map((info, id) => {
return <div key={id}>{getName(info)}</div>
})}
</div>
)
}
Codesandbox: https://codesandbox.io/s/vigorous-lake-w52vj?file=/src/App.js

How to fetch and rerender the component

Please tell me how to fetch new data into the array using useState. Each time useEffect is using, the array is initialized empty again. I need to pass this array to the table so that when the values change, the table is rendered again
const AccountContainer = () => {
let [isLoaded, setIsLoaded] = useState(false);
let [page, setPage] = useState(0);
let [accData, setAccData] = useState([]);
useEffect(() => {loadData()}, [page]);
const loadData = async () => {
const response = await fetch(API_URL);
const data = await response.json();
data.content.map(acc => {
setAccData([...accData, acc])
});
setIsLoaded(true);
};
return (
<table data={accData} />
)
};
export default AccountContainer;
added log
[] AccountContainer.js:30
[] AccountContainer.js:30
[] AccountContainer.js:30
[] AccountContainer.js:30
[] AccountContainer.js:30
[] AccountContainer.js:30
https://codesandbox.io/embed/great-rubin-poywh?fontsize=14&hidenavigation=1&theme=dark
Thanks in advance
here is the solution in the sandbox
// AccountList.jsx
import React from "react";
const AccountList = props => (
<div>
{console.warn(props)}
{props.dataAcc.map(acc => (
<div>{acc.email}</div>
))}
)
</div>
);
export default AccountList;
App.jsx
import React, { useState, useEffect } from "react";
import AccountList from "./AccountList";
import "./styles.css";
const AccountContainer = () => {
let [isLoaded, setIsLoaded] = useState(false);
let [page, setPage] = useState(0);
let [accData, setAccData] = useState([]);
const API_URL = `https://reqres.in/api/users?page=1`;
useEffect(() => {
loadData();
}, [page]);
const loadData = async () => {
const response = await fetch(API_URL);
const data = await response.json();
setAccData([...accData, ...data.data]);
setIsLoaded(true);
};
const renderTable = () => {
if (isLoaded) {
return (
<div>
{console.warn(accData)}
<AccountList dataAcc={accData} />
</div>
);
} else if (!isLoaded) return "Loading";
};
return renderTable();
};
export default AccountContainer;

How to pass state data from custom hooks to react component?

I have custom hook(useFetch) that takes URL as input and fetch data from that URL and returns data. I want to implement spinner (already made Spinner component ) on my other components and I tried by making state for the isLoading and setIsLoading of spinner.
my custom hook code:
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return dataArray;
};
export default useFetch;
This is the component that I want to implement spinner.
import React, { useState } from 'react';
import CONSTANTS from '../../constants/constants';
import CompanyLists from '../../components/company-lists/CompanyLists';
import Pagination from '../../components/pagination/Pagination';
import useFetch from '../../components/effects/use-fetch.effect';
import Spinner from '../../components/spinner/Spinner';
const CompanyListing = () => {
const [counter, setCounter] = useState(1);
const companies = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
return (
<>
<Container>
<div style={userStyle}>
{companies ? companies.map((company) => <CompanyLists key={company.id} {...company} />) : 'No companies'}
</div>
<Pagination props={companies} counter={counter} name="companies" setCounter={setCounter} />
<Spinner />
</Container>
</>
);
};
const userStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(1, 1fr)',
gridGap: '1rem',
};
export default CompanyListing;
Problem here is: How can I send those loading state from hook to CompanyListing component. Any help will be appreciated.
EDIT:
I have other component that also calls same hook and I want them not to be broken. As I didn't mention on original question .
My another case:
const jobsUrl = `${CONSTANTS.BASE_URL}/jobs?page=${counter}`;
const jobs = useFetch(jobsUrl);
AND
const { city, company_name, company_id, department, description, job_type, position, posted_at, url } = useFetch(
`${CONSTANTS.BASE_URL}/jobs/${id}`
);
How can I destructure in these two cases ?
You do not need anything special here. Just return the isLoading state with the dataArray from the useFetch hook.
As mentioned from the edit you need the useFetch to be more reusable and return data in different formats depending on the API response, hence the state should be initialized as null.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
// it is best to initialize the state as null because response.data
// may be an object or an array depending on the API response
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const value = await res.json();
setData(value.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return {data, isLoading};
};
export default useFetch;
In the components you want to use the custom hook, you can destructure
the value for data and isLoading but to futher destructure values from the returned data we have to check if data is null
// destructure the values
const {data, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
// in this case data will be an array based on your API response
// please make sure to check data.length before trying to loop over
// and render the content, for example
return (
<div>
{
data.length && data.map(company => (
<CompanyLists key={company.id} {...company} />
));
}
</div>
)
For the second case where you will be fetching data using useFetch(`${CONSTANTS.BASE_URL}/jobs/${id}`); you have to check if the returned data is not null before destructuring further. Example
const { data, isLoading } = useFetch(`${CONSTANTS.BASE_URL}/jobs/${id});
if (isLoading) {
return <div>Loading...</div>
}
if (data) {
const {
city,
company_name,
company_id,
department,
description,
job_type, position, posted_at, url } = data;
return (
// your jsx code
// for example
<h3>{company_name}</h3>
<p>{department}</p>
)
}
You need to return isLoading as well from the hook.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return {dataArray, isLoading};
};
export default useFetch;
And use this in your component like this
import React, { useState } from 'react';
import CONSTANTS from '../../constants/constants';
import CompanyLists from '../../components/company-lists/CompanyLists';
import Pagination from '../../components/pagination/Pagination';
import useFetch from '../../components/effects/use-fetch.effect';
import Spinner from '../../components/spinner/Spinner';
const CompanyListing = () => {
const [counter, setCounter] = useState(1);
const {dataArray: companies, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
return (
<>
<Container>
<div style={userStyle}>
{companies ? companies.map((company) => <CompanyLists key={company.id} {...company} />) : 'No companies'}
</div>
<Pagination props={companies} counter={counter} name="companies" setCounter={setCounter} />
{isLoading && <Spinner />}
</Container>
</>
);
};
const userStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(1, 1fr)',
gridGap: '1rem',
};
export default CompanyListing;
From your custom hook you can return both like
return {companies: dataArray, isLoading };
And destruct both
const {companies, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
First, you might need to change your useFetch effect a bit to update isLoading correctly. Then you could return both dataArray and isLoading :
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
await fetchData();
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
}, [url]);
return [dataArray, isLoading];
};
export default useFetch;
And use it like the following :
const [companies, isLoading] = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);

Multiple fetch data axios with React Hooks

I would like to get global information from Github user and his repos(and get pinned repos will be awesome). I try to make it with async await but It's is correct? I've got 4 times reRender (4 times console log). It is possible to wait all component to reRender when all data is fetched?
function App() {
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(`https://api.github.com/users/${username}`);
const respRepos = await axios(`https://api.github.com/users/${username}/repos`);
setData(respGlobal.data);
setRepos(respRepos.data);
};
fetchData()
}, []);
if (data) {
console.log(data, repos);
}
return (<h1>Hello</h1>)
}
Multiple state updates are batched but but only if it occurs from within event handlers synchronously and not setTimeouts or async-await wrapped methods.
This behavior is similar to classes and since in your case its performing two state update cycles due to two state update calls happening
So Initially you have an initial render and then you have two state updates which is why component renders three times.
Since the two states in your case are related, you can create an object and update them together like this:
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
Working demo
Figured I'd take a stab at it because the above answer is nice, however, I like cleanliness.
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const Test = () => {
const [data, setData] = useState([])
useEffect(() => {
(async () => {
const data1 = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
const data2 = await axios.get('https://jsonplaceholder.typicode.com/todos/2')
setData({data1, data2})
})()
}, [])
return JSON.stringify(data)
}
export default Test
Using a self invoking function takes out the extra step of calling the function in useEffect which can sometimes throw Promise errors in IDEs like WebStorm and PHPStorm.
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
he made some mistake here:
setGitData({ data: respGlobal.data, repos: respGlobal.data(respRepos.data //it should be respRepos.data});
For other researchers (Live demo):
import React, { useEffect, useState } from "react";
import { CPromise, CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
function MyComponent(props) {
const [error, setError] = useState("");
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
console.log("mount");
const promise = CPromise.from(function* () {
try {
console.log("fetch");
const [respGlobal, respRepos] = [
yield cpAxios(`https://api.github.com/users/${props.username}`),
yield cpAxios(`https://api.github.com/users/${props.username}/repos`)
];
setData(respGlobal.data);
setRepos(respRepos.data);
} catch (err) {
console.warn(err);
CanceledError.rethrow(err); //passthrough
// handle other errors than CanceledError
setError(err + "");
}
}, []);
return () => {
console.log("unmount");
promise.cancel();
};
}, [props.username]);
return (
<div>
{error ? (
<span>{error}</span>
) : (
<ul>
<li>{JSON.stringify(data)}</li>
<li>{JSON.stringify(repos)}</li>
</ul>
)}
</div>
);
}

Resources