Refactoring from promise to async await and use pagination - reactjs

I'm trying to re factor this code to use async/await
fetchTopRatedMovies(pageNumber).then((newData) =>
setApiData({
...newData,
results: [...apiData.results, ...newData.results]
})
);
I;m trying to use it in a try catch block within a useEffect
This is what I have so far.
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${baseURL}${option}?api_key=${API_KEY}&language=en&page=${pageNumber}&region=GB`
);
const data = await response.json();
setMovieData({
...movieData,
...data,
});
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
setLoading(true);
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [option, pageNumber]);
Problem is I think is this part
const data = await response.json();
setMovieData({
...movieData,
...data,
});
As my state const [movieData, setMovieData] = useState({ page: 0, results: [] });
Is not being updated with the old data only the new data that changes on pageNumber increase.
My main goal is to have a button that adds more data onto the already displayed data.
Full code so far:
export const Directory = () => {
const [option, setOption] = useState('popular');
const [pageNumber, setPageNumber] = useState(1);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [movieData, setMovieData] = useState({ page: 0, results: [] });
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${baseURL}${option}?api_key=${API_KEY}&language=en&page=${pageNumber}&region=GB`
);
const data = await response.json();
setMovieData({
...movieData,
...data,
});
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
setLoading(true);
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [option, pageNumber]);
const { results, page, total_pages } = movieData;
const handleOptionChange = e => {
setOption(e.target.value);
setPageNumber(1);
};
const pageLimit = page === 0 || page < total_pages;
return (
<div>
<select value={option} onChange={e => handleOptionChange(e)}>
<option value='popular'>Popular</option>
<option value='top_rated'>Top Rated</option>
<option value='now_playing'>Now Playing</option>
</select>
<ul>
{results &&
results.map(movie => {
return <li key={movie.id}>{movie.title}</li>;
})}
</ul>
{results && (
<button
disabled={!pageLimit}
onClick={() => setPageNumber(pageNumber + 1)}>
More
</button>
)}
</div>
);
};
Currently only one page results data displays at a time. So for first render page 1. Then when I click the more button. It fetchs page 2 results. However it only displays one page at a time. I want the new results to be added to the already rendered data. So with each button press more results are displayed.
console.log of data = await response.json()
{
"page": 1,
"total_results": 10000,
"total_pages": 500,
"results": [
{
"popularity": 728.376,
"vote_count": 3070,
"video": false,
"poster_path": "/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg",
"id": 419704,
"adult": false,
"backdrop_path": "/5BwqwxMEjeFtdknRV792Svo0K1v.jpg",
"original_language": "en",
"original_title": "Ad Astra",
"genre_ids": [
18,
878
],
"title": "Ad Astra",
"vote_average": 6,
"overview": "The near future, a time when both hope and hardships drive humanity to look to the stars and beyond. While a mysterious phenomenon menaces to destroy life on planet Earth, astronaut Roy McBride undertakes a mission across the immensity of space and its many perils to uncover the truth about a lost expedition that decades before boldly faced emptiness and silence in search of the unknown.",
"release_date": "2019-09-18"
},
{
"popularity": 220.799,
"id": 454626,
"video": false,
"vote_count": 2868,
"vote_average": 7.5,
"title": "Sonic the Hedgehog",
"release_date": "2020-02-14",
"original_language": "en",
"original_title": "Sonic the Hedgehog",
"genre_ids": [
28,
878,
35,
10751
],
"backdrop_path": "/stmYfCUGd8Iy6kAMBr6AmWqx8Bq.jpg",
"adult": false,
"overview": "Based on the global blockbuster videogame franchise from Sega, Sonic the Hedgehog tells the story of the world’s speediest hedgehog as he embraces his new home on Earth. In this live-action adventure comedy, Sonic and his new best friend team up to defend the planet from the evil genius Dr. Robotnik and his plans for world domination.",
"poster_path": "/aQvJ5WPzZgYVDrxLX4R6cLJCEaQ.jpg"
},
{
"popularity": 204.235,
"vote_count": 3202,
"video": false,
"poster_path": "/y95lQLnuNKdPAzw9F9Ab8kJ80c3.jpg",
"id": 38700,
"adult": false,
"backdrop_path": "/upUy2QhMZEmtypPW3PdieKLAHxh.jpg",
"original_language": "en",
"original_title": "Bad Boys for Life",
"genre_ids": [
28,
80,
53
],
"title": "Bad Boys for Life",
"vote_average": 7.2,
"overview": "Marcus and Mike are forced to confront new threats, career changes, and midlife crises as they join the newly created elite team AMMO of the Miami police department to take down the ruthless Armando Armas, the vicious leader of a Miami drug cartel.",
"release_date": "2020-01-17"
}
]
}

Your movieData structure is:
{ page: 0, results: [] }
When you do:
setMovieData({
...movieData,
...data,
})
It just replaces the previous data with the newer one. Because the keys of both new and old data are the same. What you need to do is something like this:
setMovieData({
results: [
...movieData.results, ...data.results
],
page: data.page
})
It will append new data after old data. Hope it helps.
Another mistake is with the following code:
const pageLimit = page === 0 || page < total_pages;
This needs to be managed directly with movieData state. Update your code as following:
return (
<div>
<select value={option} onChange={e => handleOptionChange(e)}>
<option value='popular'>Popular</option>
<option value='top_rated'>Top Rated</option>
<option value='now_playing'>Now Playing</option>
</select>
<ul>
{movieData.results &&
movieData.results.map(movie => {
return <li key={movie.id}>{movie.title}</li>;
})}
</ul>
{movieData.results && (
<button
disabled={!(movieData.page === 0 || movieData.page < total_pages)}
onClick={() => setPageNumber(pageNumber + 1)}>
More
</button>
)}
</div>
);

Related

MUIDataTable server side pagination - "no matching records exist"- when I move to the next page. Even though data is returned on the network tab

I am using MUIDatatable on a next js app. I have implemented server side pagination where I send the offset value and the limit as url params , and the data is returned as it should by the api -on each page change. However, the data is only displayed on the first page. When I navigate to the next page, i shows -"no matching records exist" - despite the data being returned by the api for that specific page.
Also when I click to go back to the first page, the page 2 data displays in a flash on the first page before it defaults to the actual page 1 data. Could someone point me to what I have missed ?
Datatable.jsx
const Datatable = () => {
const [data, setData] = useState([]);
const [isLoaded, setIsLoaded] = useState(false);
const [page, setPage] = useState(0);
const offset = page * 10
const getData = async () => {
try {
const response = await axios.get(`${process.env.url}/provider/api/v1/data&offset=${offset}&limit=10`)
setData(response.data.items)
setIsLoaded(true);
} catch (error) {
console.log(error)
setIsLoaded(true);
}
}
}
const columns = [name: "xxx", label: "XXXX", options: {}]
const options = {
viewColumns: true,
selectableRows: "multiple" ,
fixedSelectColumn: true,
tableBodyHeight: 'auto',
onTableChange: (action, tableState) => {
if (action === "changePage") {
setPage(tableState.page);
setIsLoaded(false);
} else {
console.log("action not handled.");
}
},
serrverSide: true,
count: 100,
textLabels: {
body: {
noMatch: isLoaded?"Sorry, no matching records exist"
:<div className="flex-grow-1 d-flex align-items-center justify-content-end">
<FadeLoader />
</div>,
toolTip: "Sort",
columnHeaderTooltip: column => `Sort for ${column.label}`
},
pagination: {
next: "Next Page",
previous: "Previous Page",
rowsPerPage: "Rows per page:",
displayRows: "of",
},
viewColumns: {
title: "Show Columns",
titleAria: "Show/Hide Table Columns",
},
}
}
useEffect(() => {
getData();
}, [page])
return (
<MUIDataTable
columns={columns}
data={data}
options={options}
/>
)
export default Datatable
setData(response.data.items) overrides the previous data, that is why you only see the data on the first page. You could fix this by changing it to.
setData(prevData => [...prevData, ...response.data.items]);
What worked for me here was I had not set serverSide: true, in the options

Why my React Query does not instant update UI after delete?

The deleted Todo item is still on display after clicking the delete button.
It does not immediately remove on display, but on my db.json file it shows that it has been deleted.
I also test by placing the component inside App.jsx and there was no problem everything works fine but when I nested the component the delete function works yet it does not immediately update
I'm using
json-server,
react vite,
react query
import React from "react";
import { useQuery, useMutation, QueryClient } from "#tanstack/react-query";
import axios from "axios";
export const axiosClient = axios.create({
baseURL: "http://localhost:8500",
});
const queryClient = new QueryClient();
const SingleTask = ({ listId }) => {
const { data: taskTodo } = useQuery(
["tasks", listId],
async () => (await axiosClient.get(`/tasks/${listId}/subtasks`)).data,
{
initialData: [],
}
);
const deleteTask = useMutation(
({id}) => axiosClient.delete(`/subtasks/${id}`),
{
onSettled: () => queryClient.invalidateQueries(["tasks"])
}
);
return (
<>
{taskTodo
?.filter((entry) => entry.status != true)
.map((list) => (
<React.Fragment key={list.id}>
<div className="mt-6">
<div className="flex justify-between items-center text-sm">
<div className="flex gap-2">
<p>{list.title}</p>
</div>
<div className="flex gap-4">
<button onClick={() => {
deleteTask.mutate(list);
}}>
Delete
</button>
</div>
</div>
</div>
</React.Fragment>
))}
</>
);
};
export default SingleTask;
Here is the json data of tasks and subtask
json data
{
"tasks": [
{
"id": 1,
"status": false,
"title": "List One",
"details": ""
},
{
"id": 2,
"status": false,
"title": "List Two",
"details": ""
}
],
"subtasks": [
{
"id": 1,
"taskId": 1,
"status": false,
"title": "Subtask for list one",
"details": ""
},
{
"id": 2,
"taskId": 2,
"status": true,
"title": "Subtask for list two",
"details": ""
}
]
}
That's because you're invalidating your query. After the query gets invalidated, it will do a refetch of your active query (that is ["tasks", listId], unless you specify otherwise). So you have to wait for the refetch to complete in order to see the update, thus it is not immediate.
If you want it to be "immediate" and if you know what the state will look like, you can use optimistic updates for that.
In your case it could be something like this:
const queryClient = useQueryClient()
useMutation(({id}) => axiosClient.delete(`/subtasks/${id}`), {
// When mutate is called:
onMutate: async ({id}) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['tasks', id])
// Snapshot the previous value
const previousTasks = queryClient.getQueryData(['tasks', id])
// Optimistically update to the new value
queryClient.setQueryData(['tasks', id], old => old.filter((t) => t.id !== id))
// Return a context object with the snapshotted value
return { previousTasks }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, { id }, context) => {
queryClient.setQueryData(['tasks', id], context.previousTasks)
},
// Always refetch after error or success:
onSettled: (newData, error, { id }) => {
queryClient.invalidateQueries(['tasks', id])
},
})
UPDATE
Since your query keys are dependant on list.listIds and not list.ids (like I assumed), you would need to update your useMutation function to something like this:
const queryClient = useQueryClient();
useMutation(({ id }) => axiosClient.delete(`/subtasks/${id}`), {
onMutate: async ({ listId, id }) => {
await queryClient.cancelQueries(['tasks', listId]);
const previousTasks = queryClient.getQueryData(['tasks', listId]);
queryClient.setQueryData(['tasks', listId], (old) => old.filter((t) => t.id !== id));
return { previousTasks };
},
onError: (err, { listId }, context) => {
queryClient.setQueryData(['tasks', listId], context.previousTasks);
},
onSettled: (newData, error, { listId }) => {
queryClient.invalidateQueries(['tasks', listId]);
},
});

Nested array with React and Firestore - How to display?

I have a nested array within the individual items in a collection.
{
"id": "RpFRcKLIgELlBLgIOJM4",
"Category": "",
"Method": "",
"User": "rWFZhAKk9eOSIIFoP0DqqvrC6WJ3",
"Foods": [
{
"Weight": 1.065,
"label": "Milk - Long Life (1 Litre) (1.065)",
"value": "Milk-LongLife(1Litre)"
},
{
"label": "Blueberries (0.125)",
"value": "Blueberries",
"Weight": 0.125
}
],
"Name": "456",
"Serves": ""
}
{
"id": "WQ6KBLevFsCdV73j4KU4",
"Category": "",
"Name": "123",
"Foods": [
{
"value": "Wine-White",
"label": "Wine - White"
},
{
"value": "Milk-LongLife(1Litre)",
"label": "Milk - Long Life (1 Litre)"
}
],
"Serves": "",
"User": "rWFZhAKk9eOSIIFoP0DqqvrC6WJ3",
"Method": ""
}
const useItemsMeals = () => {
const user = firebase.auth().currentUser;
const user1 = user.uid;
const [items, setItems] = useState([]); //useState() hook, sets initial state to an empty array
useEffect(() => {
const unsubscribe = firebase
.firestore()
.collection("meals")
.where("User", "==", user1)
.orderBy("Category", "asc")
.onSnapshot(snapshot => {
const listItemsMeals = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setItems(listItemsMeals);
console.log(listItemsMeals);
});
return () => unsubscribe();
}, []);
return items;
};
I am having a tough time trying to display items from the 'Foods' array, am currently using for my return:
const listItemMeals = useItemsMeals();
{listItemMeals.map(item => (
<TableRow hover key={item.id} id={item.id}>
<TableCell>{item.Category}</TableCell>
<TableCell>{item.Name}</TableCell>
<TableCell>{item.Foods}</TableCell>
When doing this it tells me:
Error: Objects are not valid as a React child (found: object with keys {label, value}). If you meant to render a collection of children, use an array instead.
I think I need to map this nested array again somehow - but for the life of me - cannot figure it out!
You're almost there.
Your useItemsMeals functions loads the data from Firestore, and sets it correctly into the items variable in the state to render it. But then you use const listItemMeals = useItemsMeals() in your rendering code, which messes things up.
You should not try to return any value from useItemsMeals, and instead solely rely on the items variable from the state to pass the information between the database and the rendered.
So:
// return items; πŸ‘ˆ remove this
---
// const listItemMeals = useItemsMeals(); πŸ‘ˆ remove this
---
{items.map(item => ( // πŸ‘ˆ read items from the state instead
You need to loop over the Foods array again. Like this
const listItemMeals = useItemsMeals();
{listItemMeals.map(item => (
<TableRow hover key={item.id} id={item.id}>
<TableCell>{item.Category}</TableCell>
<TableCell>{item.Name}</TableCell>
{
item.Foods.map(food=>(
<div> //this can div or a new row tag
<TableCell>{food.weight}</TableCell>
<TableCell>{food.label}</TableCell>
<TableCell>{food.value}</TableCell>
</div>))
}

How can I acces a nested object from my API

I made an API with spring-boot that is able to create Cook objects with Recipes objects in it. The idea is that one cook has a relationship with (multiple) recipe(s). When I perform a get request to my server, I am able to show the entire (Cook) object with nested (Recipe) object(s). I am also able to render Cook object properties to the browser, e.g name, age etc. However, I am not able to render Recipe properties to the browser from within the Cook object. It always gives me an undefined.
My code for the API request that shows the name from a cook correctly :
const [posts, setPosts] = useState([]);
useEffect(()=> {
axios.get(" http://localhost:9090/api/cooks")
.then(response => {
console.log(response.data) //response.data.recipe.x doesn't seem to work.
setPosts(response.data);
})
.catch(error => {
console.log(error)
})
},[])
return (
<div>
{posts.map(post => <li key={post.id}>{post.name}</li>)}; //post.recipe.x also doesn't work
</div>
)
}
const Index = () => {
return (
<div>
<Alt />
</div>
)
}
The raw JSON data from the server looks like this:
{
"id": 1,
"name": "bart",
"surName": "Janssen",
"gender": "male",
"age": 3,
"recipes": [
{
"id": 1,
"recipeName": "Hamburger",
"portion": 1,
"meat": "Ground meat",
"vegetable": "Tomato, Pickles, other greens",
"other": "salt, pepper, mustard, ketchup",
"instructions": "bla bla"
},
{
"id": 2,
"recipeName": "tomato soup",
"portion": 2,
"meat": null,
"vegetable": "tomato",
"other": "salt, stock",
"instructions": "bla bla"
}
]
}
]
However, I really need to access the Recipe properties too.
I tried accessing recipes through response.data[0], response.data[0][0], response.data[0][1], response.data[[0][0]] However, they all seem to give me an undefined back or an error.
I also tried to use JSON.Stringify(response.data), but this didn't give me any successes too.
I really could use some help. Thanks in advance :)
try to use useEffect with async/await
useEffect(async () => {
const result = await axios(
'http://localhost:9090/api/cooks',
);
setData(result.data.recipes);
});
and another moment, you initialize
const [posts, setPosts] = useState([]);
as an array - useState([ ]), but trying to write a data as object -
{
"id": 1,
"name": "bart",
"surName": "Janssen",
....
}
that's why you need to straight extract recipes
setData(result.data.recipes);

star wars api, how do I get values from api?

I'm using the https://swapi.dev/api/people/ and I'm having an interesting issue with an api I haven't encountered before. Here is an example response of the first value form the api using https://swapi.dev/api/people/
[
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "http://swapi.dev/api/planets/1/",
"films": [
"http://swapi.dev/api/films/1/",
"http://swapi.dev/api/films/2/",
"http://swapi.dev/api/films/3/",
"http://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"http://swapi.dev/api/vehicles/14/",
"http://swapi.dev/api/vehicles/30/"
],
"starships": [
"http://swapi.dev/api/starships/12/",
"http://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "http://swapi.dev/api/people/1/"
},
]
The value I want to get is the first value of the films array, which I can achieve however its just a url. I'd need to somehow grab that url, make an api call with it and then get the first film. An example response after calling the api for the first value of films looks like this:
{
"title": "The Empire Strikes Back",
"episode_id": 5,
"opening_crawl": "It is a dark time for the\r\nRebellion. Although the Death\r\nStar has been destroyed,\r\nImperial troops have driven the\r\nRebel forces from their hidden\r\nbase and pursued them across\r\nthe galaxy.\r\n\r\nEvading the dreaded Imperial\r\nStarfleet, a group of freedom\r\nfighters led by Luke Skywalker\r\nhas established a new secret\r\nbase on the remote ice world\r\nof Hoth.\r\n\r\nThe evil lord Darth Vader,\r\nobsessed with finding young\r\nSkywalker, has dispatched\r\nthousands of remote probes into\r\nthe far reaches of space....",
"director": "Irvin Kershner",
"producer": "Gary Kurtz, Rick McCallum",
"release_date": "1980-05-17",
}
I've never used an api that has urls as values, any ideas on how this might work?
Here is my current code:
Page.jsx
import React, {useState, useEffect} from 'react';
import '../css/Page.css';
import axios from 'axios';
const Page = () => {
const [data, setData] = useState([]);
const fetchData = async () => {
try {
const res = await axios.get(
"https://swapi.dev/api/people/"
);
setData(res?.data.results);
} catch (error) {
console.log(error);
}
}
useEffect(() => {
fetchData();
}, [])
const mapData = () => {
if(data && data.length > 0) {
return (
<>
{data.map(item => (
<div className="itemContainer" key={item.name}>
<div className="image"><img src="" alt=""/></div>
<div className="content">
<span className="title">Name: </span><span className="name">{item.name}</span>
<br />
<span className="title">Birthyear: </span><span className="name">{item.birth_year}</span>
<br />
<span className="title">Homeworld: </span><span className="name">{item.homeworld}</span>
</div>
</div>
))}
</>
)
}
}
return (
<div className="container">
{mapData()}
</div>
);
}
export default Page;

Resources