Fetch fires every second "onChange" - reactjs

Somehow my fetch only fires every second time, I select an item from list:
export default function App() {
...
useEffect(() => {
fetchBookData(books).then((payload) => setPayload(payload));
}, [books]);
const fetchBooksBySubject = () => {
const options = {
method: `GET`,
};
fetch(`${server}/books?subjects_like=${selectedValue}`, options)
.then((response) => {
if(response.ok){
return response.json().then(setBookList)
}
throw new Error('Api is not available')
})
.catch(error => {
console.error('Error fetching data: ', error)
setError(error)
})
}
const handleChange = e => {
setSelectedValue(e);
fetchBooksBySubject();
};
if (!payload) {
return <div>Please start the server</div>;
}
console.log(bookList)
return (
<div className="bg-blue-200">
<div className="container mx-auto py-36 bg-blue-200">
<div className="mt-12 px-96">
<SubjectSelector
options={payload}
selectedVal={selectedValue}
handleChange={handleChange}/>
<ShowcaseBooks selectedVal={selectedValue} />
<ul>
{bookList?.map((item) => {
return(
<li>{item.title}</li>
)
}
)}
</ul>
</div>
</div>
</div>
)
}
So fetchBooksBySubject delivers the bookList first as emty array, then logs correctly, howeverso I have to select two times, to see <li>{item.title}</li> updated. Why is this and how to fix it?

Your selectedValue isn't really updated yet (it will happen only at the next rerender) at the point when you call fetchBooksBySubject. It happens because setSelectedValue doesn't update the associated value immediately.
As a result, fetch(`${server}/books?subjects_like=${selectedValue}`, options) gets an old value.
You could fix it with i.e. useEffect hook:
useEffect(() => {
fetchBooksBySubject();
}, [selectedValue])
instead of calling fetchBooksBySubject immediately in the callback

Related

Fetching and rendering a component in a loop

I have a string array that contains usernames. I want to fetch those users in a loop, put them in an array and render a component for each user. I can retrieve the data from API and print it on console but the code below gives me a white screen.
Here I'm trying to fetch data one by one using fav_list array than contains usernames. Then I want to send the data to another component called InfluencerFavoritesCard and render them. Where I'm doing wrong?
import React from 'react'
import InfluencerFavoritesCard from '../components/influencerFavoritesCard/InfluencerFavoritesCard';
import "./indexPages.css"
import "./favorites.css"
const fav_list = ["cagritaner", "acunilicali", "neslihanatagul"];
async function ListFavorites() {
let array = new Array;
var fetches = [];
for (let i = 0; i < fav_list.length; i++) {
console.log(fav_list[i]);
let uname = fav_list[i];
fetches.push(
fetch(`http://localhost:3001/api/users/${uname}`)
.then(res => {return res.text(); })
.then(res => {
array.push(res);
console.log(res);
}
)
);
}
Promise.all(fetches).then(function () {
console.log(fetches);
console.log(array[0]);
console.log(array.length);
});
return (
<div>
{array.map(item => (< InfluencerFavoritesCard infCard = { item } /> ))}
</div>
);
}
function Favorites() {
return (
<div className='favoritesMain'>
<div className='favoritesTitle-div'>
<h3 className="favoritesTitle">FAVORİLER</h3>
</div>
<div className='favoritesContainer-wrapper'>
<div className='favoritesContainer'>
{<ListFavorites/>}
</div>
</div>
<div className='favoritesFooter'></div>
</div>
);
}
export default Favorites
InfluencerFavoritesCard.jsx
import React from 'react';
import './influencerFavoritesCard.css';
const InfluencerFavoritesCard = ({ infCard }) => {
return (
<div className='infCard'>
<div className='infCard-text-info' >
<div className='infCard-name'>
<h3>{infCard.name}</h3>
</div>
<div className='infCard-username'>
<h4>{infCard.username}</h4>
</div>
<div className='infCard-categories'>
<h4>{infCard.categories}</h4>
</div>
</div>
</div>
)
}
export default InfluencerFavoritesCard;
================================
UPDATED:
I have updated the parent component like below.
export function Favorites() {
const [users, setUsers] = useState([]);
const fav_list = ["cagritaner", "acunilicali", "neslihanatagul"];
useEffect(() => {
const tempUsersCollection = [];
fav_list.map((x, i) => {
fetch(`http://localhost:3001/api/users/${x}`)
.then(res => {return res.json(); })
.then(res => {
tempUsersCollection.push(res.data.tour);
console.log(res);
}
);
});
console.log(tempUsersCollection);
setUsers(tempUsersCollection);
}, []);
return (
<div className='favoritesMain'>
<div className='favoritesTitle-div'>
<h3 className="favoritesTitle">FAVORİLER</h3>
</div>
<div className='favoritesContainer-wrapper'>
<div className='favoritesContainer'>
{users.map((item, index) => (
<InfluencerFavoritesCard
infCard={item}
key={`influencer-${item.username}-${index}`}
/>
))}
</div>
</div>
<div className='favoritesFooter'></div>
</div>
);
}
output of the console.log(tempUsersCollection) (AFTER res.json() !! )
Array []
​
0: Object { username: "neslihanatagul", biography: "contact#neslihanatagul.com", profile_picture_url: "https://scontent.fist4-1.fna.fbcdn.net/v/t51.2885-15/5787050…0_AfDfN8wtsD18vya_aLLw6M4UpP8Xx16jb9b4Hsh6cJ3wjA&oe=63A713AF", … }
​
1: Object { username: "cagritaner", biography: "Hüzünlü Bir Ponçik ve Erkeklerin İç Sesi kitaplarının yazarı.\niş birlikleri için; #goygoynetworkinfo", profile_picture_url: "https://scontent.fist4-1.fna.fbcdn.net/v/t51.2885-15/4424442…0_AfCXmyBbUrKQjpzsGEHNEMHXoNP7HZ8UKaYXHAL4S0DFlA&oe=63A6926B", … }
​
2: Object { username: "acunilicali", biography: "Acun Ilıcalı Resmi Instagram Hesabıdır.", profile_picture_url: "https://scontent.fist4-1.fna.fbcdn.net/v/t51.2885-15/1193932…0_AfDuL0toVKsSGOrnXWISBMAR78G79QwNfxJmm5cNFBuW2A&oe=63A78969", … }
​
length: 3
​
<prototype>: Array []
Favorites.jsx:136
The thing is that return statement is being rendered before all the promises are resolved, meaning that it's empty. This is exactly how it should work, so no bug here.
What you need to do is as other mentioned, use useState and useEffect to control the data:
// This will hold your collection
const [users, setUsers] = useState([])
...
// And here you need to update that collection
useEffect(()=>{
const temp = []
fetch(`http://localhost:3001/api/users/${uname}`)
.then(res => {return res.text(); })
.then(res => {
temp.push(res);
}
)
setUsers(temp)
}, [])
Later on the return you can do this:
// This controls if there are no users
if(users.length <= 0){
return <>There are no users</>
}
return (
<div className='favoritesMain'>
<div className='favoritesTitle-div'>
<h3 className="favoritesTitle">FAVORİLER</h3>
</div>
<div className='favoritesContainer-wrapper'>
<div className='favoritesContainer'>
{users.map(item => (<InfluencerFavoritesCard infCard = { item } /> ))}
</div>
</div>
<div className='favoritesFooter'></div>
</div>
);
Created a CodeSandbox so you can see this working
UPDATE:
// Call the function after the first render
useEffect(() => {
fetchUsers();
}, []);
// Wrapped all the calls in a Promise.all and update the state
async function fetchUsers() {
const response = await Promise.all(
fav_list.map((x) =>
fetch(`https://pokeapi.co/api/v2/pokemon/${x}`)
.then((res) => res.json())
.then((user) => user)
)
);
console.log(response);
setUsers(response);
}
// Handle the case where nothing is retrieved
if (users.length <= 0) {
return <>There are no users</>;
}
To fetch data asynchronously, you need to use the useEffect hook. You should store the data using a useState hook and then set that data when you get a response from your fetch request.

How to wait for setState in useEffect until render?

let [item, setItem] = useState({});
let [comments, setComments] = useState([]);
useEffect(async () => {
await axios
.all([
axios.get(`https://dummyapi.io/data/v1/post/${id}`, {
headers: { "app-id": process.env.REACT_APP_API_KEY }
}),
axios.get(`https://dummyapi.io/data/v1/post/${id}/comment`, {
headers: { "app-id": process.env.REACT_APP_API_KEY }
})
])
.then(
axios.spread((detail, comment) => {
setItem({ ...detail.data })
setComments([...comment.data.data])
})
)
.catch((detail_err, comment_err) => {
console.error(detail_err);
console.error(comment_err);
});
}, []);
i setStated like above.
and I was trying to use the State in return(), but it seems it didn't wait for the data set.
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
because i got an error message like this : Uncaught TypeError: Cannot read properties of undefined (reading 'map').
Since i initialized 'item' just empty {object}, so it can't read 'item.tags', which is set by setState in useEffect.
How can i wait for the data set?
In generic, it would set a state isFetched to determine if the data from api is ready or not. And when the isFetched equal to true, it means the item.tags have value.
const [isFetched, setIsFetched] = useState(false);
useEffect(async () => {
await axios.all(...).then(() => {
...
...
setIsFetched(true);
})
}, [])
// You could return null or an Loader component meaning the api is not ready
if (!isFetched) return null;
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
On the other hand, you could use optional chaining to avoid using map from an undefined value (that is item.tags), the right way is replace item.tags.map to item.tags?.map.
Initially, item is an empty JSON ({}). You should be using the optional chaining operator(?.) to easily get rid of the null or undefined exceptions.
return (
<div>
{item?.tags?.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
let [item, setItem] = useState({});
Your initial state is an empty object, and there will always be at least one render that uses this initial state. Your code thus needs to be able to work correctly when it has this state. For example, you could check if item.tags exists before you try to use it:
if (item.tags) {
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index] />
})}
</div>
);
} else {
return <div>Loading...</div>
}
Alternatively, you could change your initial state so it has the same shape that it will have once loading has finished:
let [item, setItem] = useState({ tags: [] });

react hooks cannot read property map of undefined

I am working on a movie list search app and then later might try to add lazy loading. This is just for POC purpose. Might add it to my portfolio later.
So, I have first created a global api.js where I will put the API calls with a callback and then call an API using callbacks from the components.
const PageOneApi = (callback, errorCallback) => {
axios("https://run.mocky.io/v3/36e4f9ce-2e48-4d44-9cf8-57f6900f8e12")
.then((response) => {
console.log("response from page one api", response);
callback(response);
})
.catch((err) => {
console.log("error from page one api", err);
errorCallback(err);
});
};
export const movieListApi = {
PageOneApi,
};
I have a movieList component:
function MovieList() {
const [pageOneData, setPageOneData] = useState({});
useEffect(async () => {
await movieListApi.PageOneApi(
(res) => {
const pageOneData = res.data.page;
setPageOneData(pageOneData);
console.log("page one data: ", pageOneData);
},
(err) => {
console.log(err);
}
);
}, []);
const movieList = pageOneData.contentItems;
console.log(movieList);
return (
<div>
<h4 className="text-white p-5">{pageOneData.title}</h4>
<div className="grid grid-flow-row grid-cols-3 grid-rows-3 gap-7 m-5">
{movieList.map((movie) => {
console.log("movie", movie);
})}
</div>
</div>
);
}
Now, the problem is that I am unable to iterate through movieList, even though it's an array.
Since pageOneData is initially an empty object, pageOneData.contentItems will be undefined in the first render cycle. causing movieList.map to fail as your useEffect call is made after the initial render and that fires an API which again is async and will take time to fetch the result
You can use default value for movieList to solve the error
const movieList = pageOneData.contentItems || [];
or
Optional chaining (?.)
<div className="grid grid-flow-row grid-cols-3 grid-rows-3 gap-7 m-5">
{movieList?.map((movie) => {
console.log("movie", movie);
})}
</div>
Initially pageOneData is blank object so pageOneData.contentItems will return undefined so at first render react will throw error.
To solve this, you can use optional chaining so try something like below:-
{movieList?.map((movie) => {
console.log("movie", movie);
})}

pass arrow function to component prop

I am trying to pass the result of the handleRedirectUrl() function to the ShortUrlField component as a prop.
I don't know what I am doing wrong, please help me
const handleRedirectUrl = () => {
urlService
.getShortenedUrl(urls.slice(-1)[0].short_url)
.then((returnedUrl) => {
setRedirectedUrl(returnedUrl);
})
.catch((error) => {
handleCreateErrors(error);
})
.finally(() => {
return redirectedUrl;
});
};
//display shortened url
const shortUrlDisplay = renderShortUrl ? (
<ShortUrlField
originalUrlValue={urls.slice(-1)[0].original_url}
shortUrlValue={urls.slice(-1)[0].short_url}
redirectedUrlValue={handleRedirectUrl()}
/>
) : (
<EmptyField />
);
The urlService function
const getShortenedUrl = (urlToGet) => {
const request = axios.get(redirectShortenedUrl + `${urlToGet}`);
return request.then((response) => response.data);
};
Edit 1:
I was not returning anything with my handleRedirectUrl function. Also, I was not passing it properly to the props. I have changed my code to
const handleRedirectUrl = () => {
return urlService
.getShortenedUrl(urls.slice(-1)[0].short_url)
.then((returnedUrl) => {
setRedirectedUrl(returnedUrl);
})
.catch((error) => {
handleCreateErrors(error);
})
.finally(() => {
return redirectedUrl;
});
};
//display shortened url
const shortUrlDisplay = renderShortUrl ? (
<ShortUrlField
originalUrlValue={urls.slice(-1)[0].original_url}
shortUrlValue={urls.slice(-1)[0].short_url}
redirectedUrlValue={handleRedirectUrl}
/>
) : (
<EmptyField />
);
It does not work. the getShortenedUrl function is never called
Edit 2: Added the ShortUrlField component code
import React from "react";
const ShortUrlField = (props) => {
return (
<div>
<p>
<a href={props.originalUrlValue}>{props.originalUrlValue}</a> became{" "}
<a href={props.redirectUrlValue}>{props.shortUrlValue}</a>
</p>
</div>
);
};
export default ShortUrlField;
Edit 3: I made it work!!
Many thanks to #ZsoltMeszaros for pointing out the right path to me.
I have passed a state variable to my conditional rendered component, and added an effect hook that basically sets the state if the component is rendered.
Much thanks to all of you that commented.
I didn't understand where is this setRedirectURL function, but anyway your handleRedirectUrl function returns nothing
const handleRedirectUrl = () => {
return urlService
.getShortenedUrl(urls.slice(-1)[0].short_url)
.then((returnedUrl) => {
setRedirectedUrl(returnedUrl);
})
.catch((error) => {
handleCreateErrors(error);
})
.finally(() => {
return redirectedUrl;
});
};
May this can work as you can see like your axios request, this is returning result of urlService.

React: updating state after deletion

I'm trying to update elements after deletion, without refreshing a page. Currently, if delete a record, need to refresh a page to see the result. As I understand, need to update useState, but I do not understand how to do it. If I loop useEffect it works but slowly, but I think it's not the best idea to loop get response.
Get all records from a database.
const PostsGetUtill = () => {
const [posts, setPosts] = useState([]);
const fetchPosts = () => {
axios.get("api/v1.0/post/get").then(response => {
console.log(response.data);
setPosts(response.data);
}).catch(function (error) {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
console.log(error.config);
});
};
useEffect(() => {
fetchPosts();
}, []); // }, [fetchPosts]); <--- working well with loop
return (
<section className="container-post">
<PostMansonry posts={posts} columns={3} />
</section>
);
};
export default PostsGetUtill;
Sort and map records
export default function PostMansonry({ posts, columns }) {
return (
<section className="masonry" style={{ gridTemplateColumns: `repeat(${columns}, minmax(275px, 1fr))` }}>
{posts.sort((a, b) => a.zonedDateTime < b.zonedDateTime ? 1 : -1).map((posts, index) =>
<MasonryPost {...{ posts, index, key: index }} />)
}
</section>
)
}
Put data to the card
export default function MasonryPost({ posts, index }) {
return (
<div key={index} className="masonry-post">
<div className="card">
<div className="card-body">
<h5 className="card-title">{posts.title}</h5>
<p className="card-text">{posts.description}</p>
<p className="card-text"><small className="text-muted"> {posts.zonedDateTime}</small></p>
<div><button type="button" onClick={(e) => PostsDeleteUtill(posts.post_Id)} className="btn btn-danger">Delete</button></div>
</div>
</div>
</div>
)
}
Deleting
const PostsDeleteUtill = async (post_Id) => {
axios.delete(`api/v1.0/post/delete/${post_Id}`).then(response => {
console.log(response);
}).catch((error) => {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
console.log('error config', error.config);
});
};
export default PostsDeleteUtill;
Basically what you need to do is, in your PostsDeleteUtill function, in the promise return of your axios.delete, you need to update your posts state, which is set in PostsGetUtill.
In order to do that, you have 2 options:
Use a global state (React Context, Redux, etc)
Pass your setPosts handle all the way to your PostsDeleteUtill
I think option 1 is a bit cleaner for your specific case, but if you don't need global state anywhere else in your project, maybe it is fine to have a not so clean solution instead of implementing the whole global state structure for only one thing.
Option 1 pseudo code:
Your PostsGetUtill component would use a global state instead of local state:
const PostsGetUtill = () => {
// Remove this:
// const [posts, setPosts] = useState([]);
const fetchPosts = () => {
axios.get("api/v1.0/post/get").then(response => {
console.log(response.data);
// Instead of a local "setPosts" you would have a global
// "setPosts" (in Redux, this would be a dispatch)
dispatch({type: "PUT_POSTS", action: response.data})
}).catch(function (error) {
// No changes here...
});
};
// This runs only the first time you load this component
useEffect(() => {
fetchPosts();
}, []);
// Use your global state here as well:
return (
<section className="container-post">
<PostMansonry posts={globalState.posts} columns={3} />
</section>
);
};
export default PostsGetUtill;
In your PostsDeleteUtill function:
const PostsDeleteUtill = async (post_Id) => {
axios.delete(`api/v1.0/post/delete/${post_Id}`).then(response => {
// Update global state here. Probably filter the data to remove
// the deleted record
const updatedPosts = globalState.posts.filter(post => post.id !== response.data.id)
}).catch((error) => {
// No changes here
});
};
export default PostsDeleteUtill;
Option 2 pseudo code:
In your PostsGetUtill component, create and pass on a handleRemovePost:
// Your existing code ...
const handleRemovePost = (postID) => {
const filteredPosts = posts.filter(post => post.id !=== postID)
setPosts(filteredPosts)
}
return (
<section className="container-post">
<PostMansonry posts={posts} columns={3} handleRemovePost={handleRemovePost} />
</section>
);
In your PostMansonry, pass on again your handleRemovePost
export default function PostMansonry({ posts, columns, handleRemovePost }) {
return (
// Your existing code ...
<MasonryPost {...{ posts, index, key: index, handleRemovePost }} />)
)
}
Again in your MasonryPost
export default function MasonryPost({ posts, index, handleRemovePost }) {
return (
// Your existing code ...
<button type="button" onClick={(e) => PostsDeleteUtill(posts.post_Id, handleRemovePost)} className="btn btn-danger">Delete</button>
)
}
And finally:
const PostsDeleteUtill = async (post_Id, handleRemovePost) => {
axios.delete(`api/v1.0/post/delete/${post_Id}`).then(response => {
handleRemovePost(response);
})
};
PS: Please note that I only added a pseudo-code as a reference, trying to point out specific parts of the code that needs to be updated. If you need more information about global state, you can check React Context and Redux

Resources