How to render a React component that relies on API data? - javascript

I am trying to render a component within a component file that relies on data from an outside API. Basically, my return in my component uses a component that is awaiting data, but I get an error of dataRecords is undefined and thus cannot be mapped over.
Hopefully my code will explain this better:
// Component.js
export const History = () => {
const [dateRecords, setDateRecords] = useState(0)
const { data, loading } = useGetRecords() // A custom hook to get the data
useEffect(() => {
fetchData()
}, [loading, data])
const fetchData = async () => {
try {
let records = await data
setDateRecords(records)
} catch (err) {
console.error(err)
}
}
// Below: Render component to be used in the component return
const GameItem = ({ game }) => {
return <div>{game.name}</div>
}
// When I map over dateRecords, I get an error that it is undefined
const renderRecords = async (GameItem) => {
return await dateRecords.map((game, index) => (
<GameItem key={index} game={game} />
))
}
const GameTable = () => {
return <div>{renderRecords(<GameItem />)}</div>
}
return(
// Don't display anything until dateRecords is loaded
{dateRecords? (
// Only display <GameTable/> if the dateRecords is not empty
{dateRecords.length > 0 && <GameTable/>
)
)
}

If dateRecords is meant to be an array, initialize it to an array instead of a number:
const [dateRecords, setDateRecords] = useState([]);
In this case when the API operation is being performed, anything trying to iterate over dateRecords will simply iterate over an empty array, displaying nothing.

You've set the initial state of dateRecords to 0 which is a number and is not iterable. You should set the initial state to an empty array:
const [dateRecords, setDateRecords] = useState([]);

Related

How to use data.map function in Reactjs

I am working on Reactjs and using nextjs,Right now i am trying to fetch data using "map" function,How can i do this ? Here is my current code
import { useEffect, useState } from "react"
export default function Test() {
const [data, setData] = useState<any>();
useEffect(() => {
const callData = async () => {
const data = await fetch('https://dummyjson.com/products').then(data => data.json())
console.log(data);
setData(data)
}
callData()
}, [])
return (
//want to use map function here
);
}
Well dummyjson will return you an object wich will contain { products, total, skip, limit } in your return you can write
{data.products.map((product) => <p key={product.id}>{product.title}</p>)}
paragraph in map can be your ArticleItem or everything you want.
so you could do this, check if the state has any data in it then map through, if not show some other message.
Not sure how your data state is structured, but this should help
return(
<div>
{
data.length ? data.map(({id: number, title: string}) => <p key={id}>{title}</p>) : do something if data is empty
}
</div>)

Make React JS await for a async func to complete before running

I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.
I'm getting an array of objects
const { dogBreedsTest } = useApplicationData()
And I need it to be the default value of one of my states
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);
However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?
Here is how I am getting the data for useApplicationData()
const [dogBreedsTest, setDogBreeds] = useState(null);
const getDogBreeds = async () => {
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
setDogBreeds
}
And I am importing into my app and using:
import useApplicationData from "./hooks/useApplicationData";
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])
function handleOnDragEnd(result) {
if (!result.destination) return;
const items = Array.from(dogBreeds);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
for (const [index, item] of items.entries()) {
item['rank'] = index + 1
}
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0])
}
return (
<div className="flex-container">
<div className="App-header">
<h1>Dog Breeds 1</h1>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
{breedList1?.map(({id, name, rank}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
)
error: TypeError: Cannot read property 'map' of null
(I am mapping the data later in the program)
const getDogBreeds = async () => {
try {
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds() // -> you are not awaiting this
}, []);
Do this instead
useEffect(() => {
axios.get('https://dog.ceo/api/breeds/list/all')
.then(res => {
const newDogList = generateDogsArray(res.data['message']);
const generatedDogs = selectedDogs(newDogList);
setDogBreeds(generatedDogs);
})
.catch(err => console.log(err));
}, []);
I know this looks awful, but I don't think you should use async/await inside useEffect
Use this in your application
useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.
const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);
useEffect(() => {
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.
Where is your map function, to see the code? to show you it?.
I suppose that your data first is null. So you can use something like.
if(!data) return null
2nd Option:
In your map try this:
{breedList1 === null
? null
: breedList1.map(({id, name, rank}, index) => (
<Draggable
key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable> ))}
You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.
Option 1 use the optional chaining operator
dogBreedsTest?.map()
Option 2 check in the return if dogBreedsTest is an array
retrun (<>
{Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)
Option 3 return early
if (!Array.isArray(dogBreedsTest)) return null
retrun (<>
{dogBreedsTest.map()}
</>)
Option 4 set initial state
const [dogBreedsTest, setDogBreeds] = useState([]);
You could also add a loading state and add a loading spinner or something like that:
const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)
const getDogBreeds = async () => {
setLoading(true)
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
setLoading(false)
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
loading,
setDogBreeds
}
Edit
Try to use a useEffect hook to update the states when dogBreedsTest got set.
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])
useEffect(() => {
updateDogBreeds(dogBreedsTest?.[0] ?? [])
updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])

use SWR data in useState Hook

const fetcher = (url: string) => fetch(url).then((r) => r.json());
const { data, error } = useSWR(
"https://some.com/api",
fetcher,
);
is there any way to add data in a useState hook like this
const fetcher = (url: string) => fetch(url).then((r) => r.json());
const { data, error } = useSWR(
"https://meme-api.herokuapp.com/gimme/5",
fetcher,
);
const [memes,setMemes]=useState(data);
cause I want to concat the data at some point for inifnite scrolling
Since https://meme-api.herokuapp.com/gimme/5 always returns new data for each call, useSWR isn't a good fit for this and, moreover, the fact it retrieves from cache and gives that to your code and then revalidates and (possibly) calls your code to update, without telling you whether it's the first result or an update, makes it very hard to do what you're describing.
Instead, I'd just use fetch directly and not try to do the SWR thing; see comments:
// Start with no memes
const [memes,setMemes] = useState([]);
// Use a ref to track an `AbortController` so we can:
// A) avoid overlapping fetches, and
// B) abort the current `fetch` operation (if any) on unmount
const fetchControllerRef = useRef(null);
// A function to fetch memes
const fetchMoreMemes = () => {
if (!fetchControllerRef.current) {
fetchControllerRef.current = new AbortController();
fetch("https://meme-api.herokuapp.com/gimme/5", {signal: fetchControllerRef.current.signal})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(newMemes => {
setMemes(memes => memes.concat(newMemes.memes));
})
.catch(error => {
// ...handle/report error...
})
.finally(() => {
fetchControllerRef.current = null;
});
}
};
// Fetch the first batch of memes
useEffect(() => {
fetchMoreMemes();
return () => {
// Cancel the current `fetch` (if any) when the component is unmounted
fetchControllerRef.current?.abort();
};
}, []);
When you want to fetch more memes, call fetchMoreMemes.
Live Example:
const {useState, useEffect, useRef} = React;
const Example = () => {
// Start with no memes
const [memes,setMemes] = useState([]);
// Use a ref to track an `AbortController` so we can:
// A) avoid overlapping fetches, and
// B) abort the current `fetch` operation (if any) on unmount
const fetchControllerRef = useRef(null);
// A function to fetch memes
const fetchMoreMemes = () => {
if (!fetchControllerRef.current) {
fetchControllerRef.current = new AbortController();
fetch("https://meme-api.herokuapp.com/gimme/5", {signal: fetchControllerRef.current.signal})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(newMemes => {
// I'm filtering out NSFW ones here on SO
setMemes(memes => memes.concat(newMemes.memes.filter(({nsfw}) => !nsfw)));
})
.catch(error => {
// ...handle/report error...
})
.finally(() => {
fetchControllerRef.current = null;
});
}
};
// Fetch the first batch of memes
useEffect(() => {
fetchMoreMemes();
return () => {
// Cancel the current `fetch` (if any) when the component is unmounted
fetchControllerRef.current && fetchControllerRef.current.abort();
};
}, []);
const message = memes.length === 1 ? "1 meme:" : `${memes.length} memes:`;
return <div>
<div>{message} <input type="button" value="More" onClick={fetchMoreMemes}/></div>
<ul>
{/* `index` as key is ONLY valid because our array only grows */}
{memes.map(({postLink}, index) => <li key={index}>{postLink}</li>)}
</ul>
</div>
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
The fastest solution to transfer data from one variable to another is to use an useEffect hook. When data changes, update memes.
useEffect(() => { setMemes(data); }, [data])
Infinite scroling
A better solution would be to use SWR provided solutions for infinite scrolling. You have different options documented here.
Plain fetch
In this case, you can also consider using directly the fetch function and appending data to the memes list directly:
const [ memes, setMemes ] = useState([]);
async function fetchAnotherPage() {
const data = (await fetch('https://meme-api.herokuapp.com/gimme/5')).json();
setMemes(value => [...value, ...data.memes]);
}
useEffect(() => fetchAnotherPage(), []);

useEffect re-render different values making impossible build setting screen using async storage

i'm quite new to react-native, i'm trying to implementing a setting screen in my recipe search app. basically the user can choose different filter to avoid some kind of food (like vegan or no-milk ecc.), i thought to make an array with a number for each filter and then in the search page passing the array and apply the filter adding piece of strings for each filter. the thing is: useEffect render the array i'm passing with async-storage empty on the first render, it fulfill only on the second render, how can i take the filled array instead of the empty one?
const [richiesta, setRichiesta] = React.useState('');
const [data, setData] = React.useState([]);
const [ricerca, setRicerca] = React.useState("");
const [preferenza, setPreferenza] = React.useState([]);
let searchString = `https://api.edamam.com/search?q=${ricerca}&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
getMyValue();
getPreferences(preferenza);
},[])
const getMyValue = async () => {
try{
const x = await AsyncStorage.getItem('preferenza')
setPreferenza(JSON.parse(x));
} catch(err){console.log(err)}
}
const getPreferences = (preferenza) => {
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
//useEffect
useEffect(() => {
getRecipes();
}, [ricerca])
//fetching data
const getRecipes = async () => {
const response = await fetch(searchString);
const data = await response.json();
setData(data.hits);
}
//funzione ricerca (solo scrittura)
const onChangeSearch = query => setRichiesta(query);
//funzione modifica stato di ricerca
const getSearch = () => {
setRicerca(richiesta);
}
//barra ricerca e mapping data
return(
<SafeAreaView style={styles.container}>
<Searchbar
placeholder="Cerca"
onChangeText={onChangeSearch}
value={richiesta}
onIconPress={getSearch}
/>
this is the code, it returns "error" because on the first render the array is empty, but on the second render it fills with the value 1. can anyone help me out please?
By listening to the state of the preferenza. You need to exclude the getPreferences(preferenza); out of the useEffect for the first render and put it in it's own useEffect like this:
...
useEffect(() => {
getMyValue();
}, [])
useEffect(() => {
if( !preferenza.length ) return;
getPreferences(preferenza);
}, [preferenza])
i forgot to put the index of the array in
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
thanks for the answer tho, have a nice day!

Is accessing state in different component affect the state?

I have a component which return a form to CRUD with a state object. It perform well with data I entered manually. But when I update it with another object which I get data from excel. It doesn't perform as I expected. It doesn't access to the state object which clearly has data.
Here's the structure of it.
const ManageABC = () => {
const [obj, setObj] = useState({});
const [excelLoadedItems, setExcelLoadedItems] = useState({}); // loaded
useEffect(() => {
// fetching data for obj
}, []);
const ExcelListItem = ({index, name, onAdd}) => {
return (
<li key={index} onClick={onAdd}>{name}</li>
);
}
const handleOnAdd = (values) => {
// return a promise to add the item to obj
return new Promise((resolve) => {
console.log(obj);
// this log the obj when data entered manually but not when
// this function called from handleOnItemExcelAdd()
// which is important cuz I need it to check the condition below
if (values["id"] in obj) {
// not add
console.log('obj has this value');
resolve(true);
} else {
// add
console.log("obj doesn't have this value");
resolve(false);
}
}
}
const handleOnExcelItemAdd = (values, event) => {
handleOnAdd(values).then((res) => {
// remove li item if user says yes
if (res) {
const thisEle = event.target;
thisEle.closest("li").remove();
}
});
}
const handleExcelLoad(file) {
// read excel then
setExcelLoadedItems(
data.map((item, index) => (
<ExcelListItem
index={index}
name={item["name"]}
// this add and remove li good but it does not get the obj state to check the condition, the obj state remain nothing
onAdd={(event) => handleOnExcelItemAdd(item, event)}
/>
))
);
}
return (
{excelLoadedItems}
{obj.map(
// mapped out obj
)}
);
}
So because it does not check for condition, it auto modify the exist key in obj
You are just modifying elements from the actual DOM level. Since React has the mechanism of the virtual DOM. I guess this may cause the problem of bypassing this mechanism.
To let React knows that the DOM should be updated, use your setState hook to do so.
const handleOnExcelItemAdd = (values, event) => {
handleOnAdd(values).then((res) => {
if (res) {
const thisEle = event.target;
thisEle.closest("li").remove();
//construct a new object here
const yourNewObject = //some operation
//setObj makes the component render again, you will then see the new changes
setObj(yourNewObject);
}
});
}

Categories